DESKTOP-USV654P\pc пре 1 година
родитељ
комит
a596449a1f
40 измењених фајлова са 2484 додато и 21 уклоњено
  1. 2 6
      apps/web-baicai/src/adapter/component/index.ts
  2. 1 1
      apps/web-baicai/src/api/model/index.ts
  3. 38 0
      apps/web-baicai/src/api/system/config.ts
  4. 37 0
      apps/web-baicai/src/api/system/department.ts
  5. 2 0
      apps/web-baicai/src/api/system/enum.ts
  6. 4 0
      apps/web-baicai/src/api/system/index.ts
  7. 22 1
      apps/web-baicai/src/api/system/query.ts
  8. 64 0
      apps/web-baicai/src/api/system/role.ts
  9. 4 0
      apps/web-baicai/src/api/system/tenant.ts
  10. 57 0
      apps/web-baicai/src/api/system/user.ts
  11. 13 3
      apps/web-baicai/src/components/form/component-map.ts
  12. 2 2
      apps/web-baicai/src/components/form/components/api-checkbox.vue
  13. 2 2
      apps/web-baicai/src/components/form/components/api-radio.vue
  14. 17 2
      apps/web-baicai/src/components/form/components/api-select.vue
  15. 136 0
      apps/web-baicai/src/components/form/components/api-tree-select.vue
  16. BIN
      apps/web-baicai/src/components/select-card/images/head-default.png
  17. BIN
      apps/web-baicai/src/components/select-card/images/head-female.png
  18. BIN
      apps/web-baicai/src/components/select-card/images/head-male.png
  19. BIN
      apps/web-baicai/src/components/select-card/images/role.png
  20. 1 0
      apps/web-baicai/src/components/select-card/index.ts
  21. 239 0
      apps/web-baicai/src/components/select-card/select-card-item.vue
  22. 314 0
      apps/web-baicai/src/components/select-card/select-card.vue
  23. 26 0
      apps/web-baicai/src/utils/utils.ts
  24. 78 0
      apps/web-baicai/src/views/system/config/components/edit.vue
  25. 115 0
      apps/web-baicai/src/views/system/config/data.config.ts
  26. 88 0
      apps/web-baicai/src/views/system/config/index.vue
  27. 80 0
      apps/web-baicai/src/views/system/department/components/edit.vue
  28. 131 0
      apps/web-baicai/src/views/system/department/data.config.ts
  29. 88 0
      apps/web-baicai/src/views/system/department/index.vue
  30. 1 0
      apps/web-baicai/src/views/system/design/query/components/stepBaseConfig.vue
  31. 0 2
      apps/web-baicai/src/views/system/menu/components/edit.vue
  32. 142 0
      apps/web-baicai/src/views/system/menu/components/grant.vue
  33. 1 1
      apps/web-baicai/src/views/system/menu/data.config.ts
  34. 78 0
      apps/web-baicai/src/views/system/role/components/edit.vue
  35. 113 0
      apps/web-baicai/src/views/system/role/data.config.ts
  36. 166 0
      apps/web-baicai/src/views/system/role/index.vue
  37. 1 1
      apps/web-baicai/src/views/system/tenant/data.config.ts
  38. 78 0
      apps/web-baicai/src/views/system/user/components/edit.vue
  39. 166 0
      apps/web-baicai/src/views/system/user/data.config.ts
  40. 177 0
      apps/web-baicai/src/views/system/user/index.vue

+ 2 - 6
apps/web-baicai/src/adapter/component/index.ts

@@ -38,7 +38,7 @@ import {
   Upload,
 } from 'ant-design-vue';
 
-import { componentMap } from '#/components/form/component-map';
+import { registerComponent } from '#/components/form/component-map';
 
 const withDefaultPlaceholder = <T extends Component>(
   component: T,
@@ -114,11 +114,7 @@ async function initComponentAdapter() {
   };
 
   // 自动注册自定义组件
-  componentMap.keys().forEach((key) => {
-    components[key as ComponentType] = componentMap.get(
-      key as CustomComponentType,
-    ) as Component;
-  });
+  registerComponent(components);
 
   // 将组件注册到全局共享状态中
   globalShareState.setComponents(components);

+ 1 - 1
apps/web-baicai/src/api/model/index.ts

@@ -31,7 +31,7 @@ export interface BasicTreeOptionResult extends BasicOptionResult {
 
 export interface RelationRequest {
   id: number;
-  relationIds: number[];
+  relationIds: number[] | string[];
 }
 
 export const statusOptions: BasicOptionResult[] = [

+ 38 - 0
apps/web-baicai/src/api/system/config.ts

@@ -0,0 +1,38 @@
+import type { BasicFetchResult, BasicPageParams } from '#/api/model';
+
+import { requestClient } from '#/api/request';
+
+export namespace ConfigApi {
+  export interface PageParams extends BasicPageParams {
+    name?: string;
+    code?: string;
+  }
+
+  export interface BasicRecordItem {
+    name: string;
+    code: string;
+  }
+
+  export interface RecordItem extends BasicRecordItem {
+    id: number;
+  }
+
+  export type AppPageResult = BasicFetchResult<RecordItem>;
+
+  export const getPage = (params: PageParams) =>
+    requestClient.get<AppPageResult>('/config/page', { params });
+
+  export const getDetail = (id: number) =>
+    requestClient.get<RecordItem>('/config/entity', {
+      params: { id },
+    });
+
+  export const addDetail = (data: BasicRecordItem) =>
+    requestClient.post('/config', data);
+
+  export const editDetail = (data: RecordItem) =>
+    requestClient.put('/config', data);
+
+  export const deleteDetail = (id: number) =>
+    requestClient.delete('/config', { data: { id } });
+}

+ 37 - 0
apps/web-baicai/src/api/system/department.ts

@@ -0,0 +1,37 @@
+import { requestClient } from '#/api/request';
+
+export namespace DepartmentApi {
+  export interface PageParams {
+    code?: string;
+    name?: string;
+    orgType?: string;
+  }
+
+  export interface BasicRecordItem {
+    parentId: number;
+    name: string;
+    code: string;
+    orgType: string;
+  }
+
+  export interface RecordItem extends BasicRecordItem {
+    id: number;
+  }
+
+  export const getTree = (params: PageParams) =>
+    requestClient.get<RecordItem[]>('/department/tree', { params });
+
+  export const getDetail = (id: number) =>
+    requestClient.get<RecordItem>('/department/entity', {
+      params: { id },
+    });
+
+  export const addDetail = (data: BasicRecordItem) =>
+    requestClient.post('/department', data);
+
+  export const editDetail = (data: RecordItem) =>
+    requestClient.put('/department', data);
+
+  export const deleteDetail = (id: number) =>
+    requestClient.delete('/department', { data: { id } });
+}

+ 2 - 0
apps/web-baicai/src/api/system/enum.ts

@@ -4,8 +4,10 @@ import { requestClient } from '#/api/request';
 
 export namespace EnumApi {
   export enum EnumType {
+    Gender = 'Gender',
     MenuType = 'MenuType',
     PathType = 'PathType',
+    QueryType = 'QueryType',
     Status = 'Status',
     TenantType = 'TenantType',
   }

+ 4 - 0
apps/web-baicai/src/api/system/index.ts

@@ -1,5 +1,9 @@
+export * from './config';
 export * from './database';
+export * from './department';
 export * from './enum';
 export * from './menu';
 export * from './query';
+export * from './role';
 export * from './tenant';
+export * from './user';

+ 22 - 1
apps/web-baicai/src/api/system/query.ts

@@ -1,4 +1,9 @@
-import type { BasicFetchResult, BasicPageParams } from '#/api/model';
+import type {
+  BasicFetchResult,
+  BasicOptionResult,
+  BasicPageParams,
+  BasicTreeOptionResult,
+} from '#/api/model';
 
 import { requestClient } from '#/api/request';
 
@@ -61,4 +66,20 @@ export namespace QueryApi {
 
   export const postExecuteReal = (data: Record<string, any>) =>
     requestClient.post('/query/execute-real', data);
+  /**
+   * @param data
+   * { id: number, conditions:{ field: string,op:EnumApi.EnumType.QueryType,value:any } }
+   * @returns
+   */
+  export const postOptions = (data: Record<string, any>) =>
+    requestClient.post<BasicOptionResult[]>('/query/execute-options', data);
+  /**
+   *
+   * @param id
+   * @returns
+   */
+  export const getExecuteTree = (id: number) =>
+    requestClient.get<BasicTreeOptionResult[]>('/query/execute-tree', {
+      params: { id },
+    });
 }

+ 64 - 0
apps/web-baicai/src/api/system/role.ts

@@ -0,0 +1,64 @@
+import type {
+  BasicFetchResult,
+  BasicPageParams,
+  RelationRequest,
+  StatusParams,
+} from '#/api/model';
+
+import { requestClient } from '#/api/request';
+
+export namespace RoleApi {
+  export interface PageParams extends BasicPageParams {
+    code?: string;
+    name?: string;
+  }
+
+  export interface BasicRecordItem {
+    code: string;
+    name: string;
+  }
+
+  export interface RecordItem extends BasicRecordItem {
+    id: number;
+  }
+
+  export type AppPageResult = BasicFetchResult<RecordItem>;
+
+  export interface StatisticsItem {
+    id: number;
+    name: string;
+    count: number;
+  }
+
+  export const getPage = (params: PageParams) =>
+    requestClient.get<AppPageResult>('/role/page', { params });
+
+  export const getDetail = (id: number) =>
+    requestClient.get<RecordItem>('/role/entity', {
+      params: { id },
+    });
+
+  export const addDetail = (data: BasicRecordItem) =>
+    requestClient.post('/role', data);
+
+  export const editDetail = (data: RecordItem) =>
+    requestClient.put('/role', data);
+
+  export const deleteDetail = (id: number) =>
+    requestClient.delete('/role', { data: { id } });
+  export const updateStatus = (data: StatusParams) =>
+    requestClient.put('/role/status', data);
+
+  export const getMenuIds = (id: number) =>
+    requestClient.get<number[]>('/role/menu-ids', { params: { id } });
+  export const getUserIds = (id: number) =>
+    requestClient.get<number[]>('/role/user-ids', { params: { id } });
+
+  export const updateGrant = (data: RelationRequest) =>
+    requestClient.post('/role/grant', data);
+  export const updateGrantUser = (data: RelationRequest) =>
+    requestClient.post('/role/grant-user', data);
+
+  export const getStatistics = () =>
+    requestClient.get<StatisticsItem[]>('/role/statistics');
+}

+ 4 - 0
apps/web-baicai/src/api/system/tenant.ts

@@ -2,6 +2,7 @@ import type {
   BasicFetchResult,
   BasicOptionResult,
   BasicPageParams,
+  RelationRequest,
 } from '#/api/model';
 
 import { requestClient } from '#/api/request';
@@ -53,6 +54,9 @@ export namespace TenantApi {
   export const getMenuIds = (id: number) =>
     requestClient.get<number[]>('/tenant/menu-ids', { params: { id } });
 
+  export const updateGrant = (data: RelationRequest) =>
+    requestClient.post('/tenant/grant', data);
+
   export const createDb = (id: number) =>
     requestClient.post('/tenant/db', { id });
 }

+ 57 - 0
apps/web-baicai/src/api/system/user.ts

@@ -0,0 +1,57 @@
+import type {
+  BasicFetchResult,
+  BasicPageParams,
+  RelationRequest,
+  StatusParams,
+} from '#/api/model';
+
+import { requestClient } from '#/api/request';
+
+export namespace UserApi {
+  export interface PageParams extends BasicPageParams {
+    account?: string;
+    name?: string;
+    phone?: string;
+  }
+
+  export interface BasicRecordItem {
+    account: string;
+    realName: string;
+    nickName: string;
+    phone: string;
+    sex: number;
+  }
+
+  export interface RecordItem extends BasicRecordItem {
+    id: number;
+  }
+
+  export type AppPageResult = BasicFetchResult<RecordItem>;
+
+  export const getPage = (params: PageParams) =>
+    requestClient.get<AppPageResult>('/user/page', { params });
+
+  export const getDetail = (id: number) =>
+    requestClient.get<RecordItem>('/user/entity', {
+      params: { id },
+    });
+
+  export const addDetail = (data: BasicRecordItem) =>
+    requestClient.post('/user', data);
+
+  export const editDetail = (data: RecordItem) =>
+    requestClient.put('/user', data);
+
+  export const deleteDetail = (id: number) =>
+    requestClient.delete('/user', { data: { id } });
+
+  export const getRoleIds = (id: number) =>
+    requestClient.get<number[]>('/user/role-ids', { params: { id } });
+
+  export const updateStatus = (data: StatusParams) =>
+    requestClient.put('/user/status', data);
+  export const resetPassword = (ids: number[]) =>
+    requestClient.post('/user/reset-password', ids);
+  export const updateGrantRole = (data: RelationRequest) =>
+    requestClient.post('/user/grant', data);
+}

+ 13 - 3
apps/web-baicai/src/components/form/component-map.ts

@@ -4,7 +4,7 @@ import type { Component } from 'vue';
 
 import { toPascalCase } from '#/utils';
 
-const componentMap = new Map<CustomComponentType, Component>();
+const componentMap = new Map<CustomComponentType | string, Component>();
 // import.meta.glob() 直接引入所有的模块 Vite 独有的功能
 const modules = import.meta.glob('./components/**/*.vue', { eager: true });
 // 加入到路由集合中
@@ -17,11 +17,21 @@ Object.keys(modules).forEach((key) => {
 });
 
 export function add(compName: string, component: Component) {
-  componentMap.set(compName as CustomComponentType, component);
+  componentMap.set(compName, component);
 }
 
 export function del(compName: string) {
-  componentMap.delete(compName as CustomComponentType);
+  componentMap.delete(compName);
 }
 
+/**
+ * 注册组件
+ * @param components
+ */
+export const registerComponent = (components: any) => {
+  componentMap.forEach((value, key) => {
+    components[key] = value as Component;
+  });
+};
+
 export { componentMap };

+ 2 - 2
apps/web-baicai/src/components/form/components/api-checkbox.vue

@@ -78,11 +78,11 @@ const fetch = async () => {
           if (props.api.type === 'enum') {
             return EnumApi.getList(params);
           } else if (props.api.type === 'api') {
-            return QueryApi.postExecuteReal(params);
+            return QueryApi.postOptions(params);
           } else
             return (requestClient as any)[props.api.method](
               props.api.url as any,
-              params,
+              props.api.method === 'get' ? { params } : params,
             );
         }
       : props.api.url;

+ 2 - 2
apps/web-baicai/src/components/form/components/api-radio.vue

@@ -82,11 +82,11 @@ const fetch = async () => {
           if (props.api.type === 'enum') {
             return EnumApi.getList(params);
           } else if (props.api.type === 'api') {
-            return QueryApi.postExecuteReal(params);
+            return QueryApi.postOptions(params);
           } else
             return (requestClient as any)[props.api.method](
               props.api.url as any,
-              params,
+              props.api.method === 'get' ? { params } : params,
             );
         }
       : props.api.url;

+ 17 - 2
apps/web-baicai/src/components/form/components/api-select.vue

@@ -44,6 +44,10 @@ const props = defineProps({
     type: Boolean,
     default: true,
   },
+  showSearch: {
+    type: Boolean,
+    default: false,
+  },
 });
 const emit = defineEmits(['update:value', 'optionsChange']);
 const modelValue = useVModel(props, 'value', emit, {
@@ -80,11 +84,11 @@ const fetch = async () => {
           if (props.api.type === 'enum') {
             return EnumApi.getList(params);
           } else if (props.api.type === 'api') {
-            return QueryApi.postExecuteReal(params);
+            return QueryApi.postOptions(params);
           } else
             return (requestClient as any)[props.api.method](
               props.api.url as any,
-              params,
+              props.api.method === 'get' ? { params } : params,
             );
         }
       : props.api.url;
@@ -110,6 +114,15 @@ const handleFetch = async () => {
     isFirstLoad.value = false;
   }
 };
+
+const handelFilterOption = (input: string, option: any) => {
+  const { fieldNames } = props;
+  return (
+    option[fieldNames.value].toLowerCase().includes(input.toLowerCase()) ||
+    option[fieldNames.label].toLowerCase().includes(input.toLowerCase())
+  );
+};
+
 watchEffect(() => {
   props.immediate && fetch();
 });
@@ -128,6 +141,8 @@ watch(
     v-model:value="modelValue"
     :options="getOptions"
     v-bind="$attrs"
+    :filter-option="handelFilterOption"
+    :show-search="showSearch"
     class="w-full"
     @dropdown-visible-change="handleFetch"
   >

+ 136 - 0
apps/web-baicai/src/components/form/components/api-tree-select.vue

@@ -0,0 +1,136 @@
+<script setup lang="ts">
+import type { Recordable } from '@vben/types';
+import type { SelectValue } from 'ant-design-vue/es/select';
+
+import type { ApiConfig } from '../types';
+
+import { computed, type PropType, ref, unref, watch, watchEffect } from 'vue';
+
+import { isFunction } from '@vben/utils';
+
+import { useVModel } from '@vueuse/core';
+import { TreeSelect } from 'ant-design-vue';
+
+import { EnumApi, QueryApi } from '#/api';
+import { requestClient } from '#/api/request';
+import { Icon } from '#/components/icon';
+import { get } from '#/utils';
+
+const props = defineProps({
+  value: {
+    type: [String, Number, Array] as PropType<SelectValue>,
+    default: undefined,
+  },
+  api: {
+    type: Object as PropType<ApiConfig>,
+    default: () => ({
+      type: 'none',
+      method: 'get',
+      params: {},
+      result: '',
+      url: null,
+    }),
+  },
+  fieldNames: {
+    type: Object as PropType<{
+      children: string;
+      label: string;
+      value: string;
+    }>,
+    default: () => ({ label: 'label', value: 'value', children: 'children' }),
+  },
+  immediate: {
+    type: Boolean,
+    default: true,
+  },
+  showSearch: {
+    type: Boolean,
+    default: false,
+  },
+});
+const emit = defineEmits(['update:value', 'optionsChange']);
+const modelValue = useVModel(props, 'value', emit, {
+  defaultValue: props.value,
+  passive: true,
+});
+const treeData = ref<Recordable<any>[]>([]);
+const loading = ref(false);
+const isFirstLoad = ref(true);
+const getTreeData = computed(() => {
+  return unref(treeData);
+});
+
+const emitChange = () => {
+  emit('optionsChange', unref(treeData));
+};
+
+const fetch = async () => {
+  const api: any =
+    typeof props.api.url === 'string' || !props.api.url
+      ? (params: any) => {
+          if (props.api.type === 'enum') {
+            return EnumApi.getList(params);
+          } else if (props.api.type === 'api') {
+            return QueryApi.getExecuteTree(params);
+          } else {
+            return (requestClient as any)[props.api.method](
+              props.api.url as any,
+              props.api.method === 'get' ? { params } : params,
+            );
+          }
+        }
+      : props.api.url;
+  if (!api || !isFunction(api)) return;
+  try {
+    loading.value = true;
+    const res = await api(props.api.params);
+    if (Array.isArray(res)) {
+      treeData.value = (res as Recordable<any>[]) || [];
+    } else {
+      treeData.value = props.api.result ? get(res, props.api.result) : [];
+    }
+    emitChange();
+  } catch (error) {
+    console.warn(error);
+  } finally {
+    loading.value = false;
+  }
+};
+async function handleFetch() {
+  if (!props.immediate && unref(isFirstLoad)) {
+    await fetch();
+    isFirstLoad.value = false;
+  }
+}
+watchEffect(() => {
+  props.immediate && fetch();
+});
+
+watch(
+  () => props.api.params,
+  () => {
+    !isFirstLoad.value && fetch();
+  },
+  { deep: true },
+);
+</script>
+
+<template>
+  <TreeSelect
+    v-model:value="modelValue"
+    :field-names="fieldNames"
+    :show-search="showSearch"
+    :tree-data="getTreeData"
+    :tree-node-filter-prop="fieldNames.label"
+    v-bind="$attrs"
+    class="w-full"
+    @dropdown-visible-change="handleFetch"
+  >
+    <template v-for="item in Object.keys($slots)" #[item]="data">
+      <slot :name="item" v-bind="data || {}"></slot>
+    </template>
+    <template v-if="loading" #suffixIcon>
+      <Icon icon="ant-design:loading-outlined" spin />
+    </template>
+  </TreeSelect>
+</template>

BIN
apps/web-baicai/src/components/select-card/images/head-default.png


BIN
apps/web-baicai/src/components/select-card/images/head-female.png


BIN
apps/web-baicai/src/components/select-card/images/head-male.png


BIN
apps/web-baicai/src/components/select-card/images/role.png


+ 1 - 0
apps/web-baicai/src/components/select-card/index.ts

@@ -0,0 +1 @@
+export { default as SelectCard } from './select-card.vue';

+ 239 - 0
apps/web-baicai/src/components/select-card/select-card-item.vue

@@ -0,0 +1,239 @@
+<script setup lang="ts">
+import type { Recordable } from '@vben/types';
+
+import { computed, type PropType, useSlots } from 'vue';
+
+import { Tooltip } from 'ant-design-vue';
+
+import { Icon } from '#/components/icon';
+
+import defaultImg from './images/head-default.png';
+import FemaleImg from './images/head-female.png';
+import MaleImg from './images/head-male.png';
+import RoleImg from './images/role.png';
+
+defineOptions({
+  name: 'SelectCardItem',
+});
+
+const props = defineProps({
+  model: {
+    type: Object as PropType<Recordable<any>>,
+    default: () => ({}),
+  },
+  fieldNames: {
+    type: Array as PropType<Array<any>>,
+    default: () => [
+      { title: '名称', value: 'title', maxLength: 12 },
+      { title: '电话', value: 'phone', maxLength: 8 },
+    ],
+  },
+  config: {
+    type: Object as PropType<{
+      bgcolor: string;
+      color: string;
+      fillColor: string;
+      type: string;
+    }>,
+    default: () => ({
+      type: 'none',
+      fillColor: '#f1ecfe',
+      bgcolor: '#f5f1fd',
+      color: '#b389ff',
+    }),
+  },
+  disabled: {
+    type: Boolean as PropType<boolean>,
+    default: false,
+  },
+  showTree: {
+    type: Boolean as PropType<boolean>,
+    default: false,
+  },
+});
+
+const hasCheckSlot = computed(() => {
+  return !!useSlots().check;
+});
+
+const getImage = computed(() => {
+  switch (props.config.type) {
+    case 'role': {
+      return RoleImg;
+    }
+    case 'user': {
+      return props.model.sex === 1 ? MaleImg : FemaleImg;
+    }
+    default: {
+      return defaultImg;
+    }
+  }
+});
+
+const getFillColor = computed(() => {
+  if (props.config.type === 'user') {
+    return props.model.sex === 1 ? '#e9f0fe' : '#ffedf5';
+  } else {
+    return props.config.fillColor || '#f1ecfe';
+  }
+});
+
+const getFontColor = computed(() => {
+  if (props.config.type === 'user') {
+    return props.model.sex === 1 ? '#3c7eff' : '#ffd1d7';
+  } else {
+    return props.config.color || '#b389ff';
+  }
+});
+
+const getBgcolor = computed(() => {
+  if (props.config.type === 'user') {
+    return props.model.sex === 1 ? '#f3f8ff' : '#fef6fa';
+  } else {
+    return props.config.bgcolor || '#f5f1fd';
+  }
+});
+
+const itemleftwidth = computed(() => {
+  return props.showTree ? '30%' : '25%';
+});
+</script>
+
+<template>
+  <div class="select-card-item">
+    <div class="select-card-item-box">
+      <div class="select-card-item-left">
+        <img :src="getImage" />
+      </div>
+      <div class="z-10">
+        <div
+          v-for="(item, index) in fieldNames"
+          :key="index"
+          class="select-card-item-right flex items-center"
+        >
+          <div class="select-card-item-right-title">
+            {{ item.title }}
+          </div>
+          <Tooltip
+            v-if="
+              model[item.value] &&
+              model[item.value].length > model[item.value].maxLength
+            "
+            :title="model[item.value]"
+          >
+            <div class="select-card-item-right-name">
+              {{
+                `${model[item.value].slice(0, model[item.value].maxLength)}...`
+              }}
+            </div>
+          </Tooltip>
+          <div v-else class="select-card-item-right-name">
+            {{ model[item.value] || '-' }}
+          </div>
+        </div>
+      </div>
+      <!-- <div v-if="props.disabled">
+        <div class="fixed-checked"> 禁用 </div>
+      </div> -->
+      <div v-if="hasCheckSlot" class="fixed-checked">
+        <slot name="check"></slot>
+      </div>
+      <div class="fixed-icon">
+        <Icon :color="getFillColor" class="size-16" icon="fa6-solid:user-tie" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="less" scoped>
+.select-card-item {
+  width: 30%;
+  margin-bottom: 20px;
+  margin-left: 20px;
+  overflow: hidden;
+  border-radius: 8px;
+  border-color: transparent;
+  background: v-bind(getBgcolor);
+
+  &:hover {
+    border: 1px dotted v-bind(getFontColor);
+  }
+
+  &-box {
+    display: flex;
+    position: relative;
+    margin: 14px;
+  }
+
+  &-left {
+    width: v-bind(itemleftwidth);
+    margin-right: 14px;
+
+    img {
+      width: 100%;
+      height: 100%;
+    }
+  }
+
+  &-right {
+    &-title {
+      margin: 10px 10px 4px 0;
+      opacity: 0.8;
+      color: #999;
+      font-size: 12px;
+      font-weight: bold;
+    }
+
+    &-name {
+      margin: 8px 0 4px;
+      opacity: 0.8;
+      color: #303133;
+      font-size: 14px;
+      font-weight: bold;
+    }
+  }
+
+  .fixed-checked {
+    position: absolute;
+    z-index: 1;
+    right: -6px;
+    bottom: -4px;
+  }
+
+  .fixed-icon {
+    position: absolute;
+    z-index: 0;
+    top: -24px;
+    right: -24px;
+    transform: rotate(48deg);
+  }
+}
+
+:deep(.ant-checkbox-inner) {
+  border-color: v-bind(getFontColor);
+}
+
+:deep(.ant-checkbox-checked .ant-checkbox-inner) {
+  border-color: v-bind(getFontColor);
+  background-color: v-bind(getFontColor);
+}
+
+:deep(.ant-checkbox-checked::after),
+:deep(.ant-checkbox-wrapper:hover .ant-checkbox-inner, .ant-checkbox:hover),
+:deep(.ant-checkbox-inner),
+:deep(.ant-checkbox:hover),
+:deep(.ant-checkbox-input:focus + .ant-checkbox-inner) {
+  border-color: v-bind(getFontColor);
+}
+
+.picked {
+  border-width: 1px;
+  border-style: dotted;
+  border-color: v-bind(getFontColor);
+}
+
+.not-picked {
+  border-width: 1px;
+  border-style: dotted;
+}
+</style>

+ 314 - 0
apps/web-baicai/src/components/select-card/select-card.vue

@@ -0,0 +1,314 @@
+<script lang="ts" setup>
+import type { Recordable } from '@vben/types';
+
+import type { ApiConfig } from '../form/types';
+
+import { type PropType, reactive, ref } from 'vue';
+
+import { useVbenForm, useVbenModal } from '@vben/common-ui';
+import { isFunction } from '@vben/utils';
+
+import { Checkbox, message, Pagination } from 'ant-design-vue';
+
+import { EnumApi, QueryApi } from '#/api';
+import { requestClient } from '#/api/request';
+import { get } from '#/utils';
+
+import SelectCardItem from './select-card-item.vue';
+
+defineOptions({
+  name: 'SelectCard',
+});
+
+const props = defineProps({
+  multiple: {
+    type: Boolean as PropType<boolean>,
+    default: true,
+  },
+  title: {
+    type: String as PropType<string>,
+    default: '选择数据',
+  },
+  showTree: {
+    type: Boolean as PropType<boolean>,
+    default: false,
+  },
+  immediate: {
+    type: Boolean,
+    default: true,
+  },
+});
+const emit = defineEmits(['success']);
+const cardData = ref<Recordable<any>[]>([]);
+
+const state = reactive<{
+  api: ApiConfig;
+  config: {
+    bgcolor: string;
+    color: string;
+    fillColor: string;
+    type: string;
+  };
+  disabledIds: number[] | string[];
+  fieldNames: Recordable<any>[];
+  key: number | string;
+  keyName: string;
+  page: { pageIndex: number; pageSize: number; total: number };
+  searchName: string;
+  selectedIds: number[];
+  selectedList: Recordable<any>[];
+}>({
+  page: {
+    pageIndex: 1,
+    pageSize: 9,
+    total: 0,
+  },
+  selectedIds: [],
+  selectedList: [],
+  disabledIds: [],
+  api: {
+    type: 'none',
+    method: 'get',
+    params: {},
+    result: 'items',
+    url: null,
+  },
+  keyName: 'id',
+  key: '',
+  searchName: '',
+  fieldNames: [
+    { title: '名称', value: 'title', maxLength: 12 },
+    { title: '电话', value: 'phone', maxLength: 8 },
+  ],
+  config: {
+    type: 'none',
+    fillColor: '#f1ecfe',
+    bgcolor: '#f5f1fd',
+    color: '#b389ff',
+  },
+});
+
+const fetch = async () => {
+  const api: any =
+    typeof state.api.url === 'string' || !state.api.url
+      ? (params: any) => {
+          Object.assign(params, state.page);
+          if (state.api.type === 'enum') {
+            return EnumApi.getList(params);
+          } else if (state.api.type === 'api') {
+            return QueryApi.postOptions(params);
+          } else
+            return (requestClient as any)[state.api.method](
+              state.api.url as any,
+              state.api.method === 'get' ? { params } : params,
+            );
+        }
+      : state.api.url;
+  if (!api || !isFunction(api)) return;
+  try {
+    const res = await api(state.api.params);
+    if (Array.isArray(res)) {
+      cardData.value = res;
+    } else {
+      cardData.value = state.api.result ? get(res, state.api.result) : [];
+      state.page.total = res.total || 0;
+    }
+  } catch (error) {
+    console.warn(error);
+  } finally {
+    // setState({ loading: false });
+  }
+};
+
+const [Form, { resetForm }] = useVbenForm({
+  layout: 'horizontal',
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+  },
+  schema: [
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'keyword',
+      label: '关键字',
+    },
+  ],
+  submitButtonOptions: {
+    content: '查询',
+  },
+  wrapperClass: 'grid-cols-2',
+  handleSubmit: async (values: Record<string, any>) => {
+    if (state.searchName) {
+      state.page.pageIndex = 1;
+      state.api.params[state.searchName] = values.keyword;
+      await fetch();
+    }
+  },
+  handleReset: async () => {
+    resetForm();
+    state.page.pageIndex = 1;
+    state.api.params[state.searchName] = '';
+    await fetch();
+  },
+});
+
+const [Modal, { close, setState, getData }] = useVbenModal({
+  onConfirm: async () => {
+    try {
+      close();
+      emit('success', {
+        key: state.key,
+        ids: [...state.selectedIds],
+        list: [...state.selectedList],
+      });
+    } catch {
+      message.error('操作失败');
+    } finally {
+      setState({ confirmLoading: false });
+    }
+  },
+  onOpenChange: async (isOpen: boolean) => {
+    if (isOpen) {
+      setState({ loading: true });
+      const data = getData<Record<string, any>>();
+      state.key = data.baseData.key;
+      state.searchName = data.baseData.searchName || '';
+      state.selectedIds = data.baseData.selectedIds || [];
+      state.disabledIds = data.baseData.disabledIds || [];
+      state.fieldNames = data.baseData.fieldNames || [];
+      Object.assign(state.api, data.baseData.api);
+      Object.assign(state.config, data.baseData.config);
+      state.page.pageIndex = 1;
+      await fetch();
+      setState({ loading: false });
+    }
+  },
+});
+
+const hancelClick = (item: any) => {
+  if (
+    state.disabledIds &&
+    Array.isArray(state.disabledIds) &&
+    state.disabledIds.includes(item[state.keyName] as never)
+  ) {
+    return;
+  }
+
+  if (props.multiple) {
+    if (state.selectedIds.includes(item[state.keyName])) {
+      state.selectedIds.splice(
+        state.selectedIds.indexOf(item[state.keyName]),
+        1,
+      );
+      state.selectedList.splice(
+        state.selectedList.findIndex(
+          (ele) => ele[state.keyName] === item[state.keyName],
+        ),
+        1,
+      );
+    } else {
+      state.selectedIds.push(item[state.keyName]);
+      state.selectedList.push(item);
+    }
+  } else {
+    if (state.selectedIds.includes(item[state.keyName])) {
+      state.selectedIds = [];
+      state.selectedList = [];
+    } else {
+      state.selectedIds = [item[state.keyName]];
+    }
+  }
+};
+
+const handelPageChange = async (pageIndex: number) => {
+  setState({ loading: true });
+  state.page.pageIndex = pageIndex;
+  await fetch();
+  setState({ loading: false });
+};
+</script>
+
+<template>
+  <Modal :title="`选择${title}`" class="w-[1000px]">
+    <div class="select-card flex justify-between">
+      <div class="ml-4 flex-1">
+        <div class="select-card-header">
+          <div class="flex justify-between">
+            <div class="text-foreground text-lg font-semibold">
+              {{ title }}列表
+            </div>
+          </div>
+          <Form class="mt-4" />
+        </div>
+        <div class="select-card-body">
+          <SelectCardItem
+            v-for="(item, index) in cardData"
+            :key="index"
+            :class="
+              state.selectedIds.includes(item[state.keyName])
+                ? 'picked'
+                : 'not-picked'
+            "
+            :config="state.config"
+            :disabled="
+              state.disabledIds &&
+              state.disabledIds.includes(item[state.keyName] as never)
+                ? true
+                : false
+            "
+            :field-names="state.fieldNames"
+            :model="item"
+            @click="hancelClick(item)"
+          >
+            <template #check>
+              <Checkbox
+                :checked="state.selectedIds.includes(item[state.keyName])"
+                size="small"
+              />
+            </template>
+          </SelectCardItem>
+          <div class="select-card-pagination">
+            <Pagination
+              v-model:current="state.page.pageIndex"
+              :page-size="state.page.pageSize"
+              :total="state.page.total"
+              show-less-items
+              show-quick-jumper
+              size="small"
+              @change="handelPageChange"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </Modal>
+</template>
+
+<style lang="less" scoped>
+.select-card {
+  display: flex;
+  height: 500px;
+  margin: 10px;
+  overflow: auto;
+  position: relative;
+
+  &-body {
+    display: flex;
+    flex-wrap: wrap;
+    padding: 10px 0;
+    overflow-y: auto;
+  }
+
+  &-pagination {
+    // width: 100%;
+    position: absolute;
+    text-align: right;
+    bottom: 10px;
+    right: 10px;
+  }
+}
+</style>

+ 26 - 0
apps/web-baicai/src/utils/utils.ts

@@ -67,3 +67,29 @@ export const get = (object: any, path: string) => {
   // 返回最终找到的值
   return current;
 };
+
+/**
+ * 获取所有叶子节点
+ * @param treeData
+ */
+export const getLeafNodeIds = (treeData: any) => {
+  const leafNodeIds: any = [];
+
+  function traverse(node: any) {
+    // 如果当前节点没有 children 或 children 是空数组,则它是叶子节点
+    if (!node.children || node.children.length === 0) {
+      leafNodeIds.push(node.id);
+    } else {
+      // 否则,递归遍历每个子节点
+      for (const child of node.children) {
+        traverse(child);
+      }
+    }
+  }
+
+  // 遍历树的根节点
+  for (const root of treeData) {
+    traverse(root);
+  }
+  return leafNodeIds;
+};

+ 78 - 0
apps/web-baicai/src/views/system/config/components/edit.vue

@@ -0,0 +1,78 @@
+<script lang="ts" setup>
+import { onMounted, ref, unref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter';
+import { ConfigApi } from '#/api';
+
+import { formOptions } from '../data.config';
+
+defineOptions({
+  name: 'ConfigEdit',
+});
+const emit = defineEmits(['success']);
+const modelRef = ref<Record<string, any>>({});
+const isUpdate = ref(true);
+
+const [Form, { validate, setValues, getValues }] = useVbenForm({
+  showDefaultActions: false,
+  ...formOptions,
+});
+
+const [Modal, { close, setState, getData }] = useVbenModal({
+  fullscreenButton: false,
+  draggable: true,
+  onCancel() {
+    close();
+  },
+  onConfirm: async () => {
+    try {
+      const { valid } = await validate();
+      if (valid) {
+        const values = await getValues();
+        setState({ confirmLoading: true });
+        const postParams = unref(modelRef);
+        Object.assign(postParams, values);
+        await (unref(isUpdate)
+          ? ConfigApi.editDetail(postParams as ConfigApi.RecordItem)
+          : ConfigApi.addDetail(postParams as ConfigApi.BasicRecordItem));
+        message.success('操作成功');
+
+        close();
+        emit('success');
+      }
+    } catch {
+      message.error('操作失败');
+    } finally {
+      setState({ confirmLoading: false });
+    }
+  },
+  onOpenChange: async (isOpen: boolean) => {
+    if (isOpen) {
+      setState({ loading: true });
+      const data = getData<Record<string, any>>();
+      isUpdate.value = !!data.isUpdate;
+      modelRef.value = { ...data.baseData };
+      setState({ title: unref(isUpdate) ? '编辑系统配置' : '新增系统配置' });
+
+      if (unref(isUpdate)) {
+        const entity = await ConfigApi.getDetail(data.baseData.id);
+        modelRef.value = { ...entity };
+        setValues(entity);
+      }
+      setState({ loading: false });
+    }
+  },
+  title: '新增系统配置',
+});
+
+onMounted(async () => {});
+</script>
+<template>
+  <Modal class="w-[1000px]">
+    <Form />
+  </Modal>
+</template>

+ 115 - 0
apps/web-baicai/src/views/system/config/data.config.ts

@@ -0,0 +1,115 @@
+import type { VbenFormProps, VxeGridProps } from '#/adapter';
+
+import { ConfigApi } from '#/api';
+
+export const searchFormOptions: VbenFormProps = {
+  schema: [
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: '名称',
+    },
+    {
+      component: 'Input',
+      fieldName: 'code',
+      label: '编码',
+    },
+  ],
+};
+
+export const gridOptions: VxeGridProps<ConfigApi.RecordItem> = {
+  toolbarConfig: {
+    refresh: true,
+    print: false,
+    export: false,
+    zoom: true,
+    custom: true,
+  },
+  columns: [
+    { title: '序号', type: 'seq', width: 50 },
+    {
+      align: 'left',
+      field: 'code',
+      title: '编码',
+      width: 150,
+    },
+    { align: 'left', field: 'name', title: '名称', width: 200 },
+    { align: 'left', field: 'value', title: '参数值', width: 200 },
+    { align: 'left', field: 'remark', title: '备注' },
+    {
+      field: 'action',
+      fixed: 'right',
+      slots: { default: 'action' },
+      title: '操作',
+      width: 110,
+    },
+  ],
+  height: 'auto',
+  keepSource: true,
+  proxyConfig: {
+    ajax: {
+      query: async ({ page }, formValues) => {
+        return await ConfigApi.getPage({
+          pageIndex: page.currentPage,
+          pageSize: page.pageSize,
+          ...formValues,
+        });
+      },
+    },
+  },
+};
+
+export const formOptions: VbenFormProps = {
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+  },
+  schema: [
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入编号',
+      },
+      fieldName: 'code',
+      label: '编号',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入名称',
+      },
+      fieldName: 'name',
+      label: '名称',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入参数值',
+      },
+      fieldName: 'value',
+      label: '参数值',
+      rules: 'required',
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入排序',
+      },
+      fieldName: 'sort',
+      label: '排序',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'remark',
+      label: '备注',
+    },
+  ],
+  wrapperClass: 'grid-cols-1',
+};

+ 88 - 0
apps/web-baicai/src/views/system/config/index.vue

@@ -0,0 +1,88 @@
+<script lang="ts" setup>
+import { useAccess } from '@vben/access';
+import { Page, useVbenModal } from '@vben/common-ui';
+
+import { Button, message, Modal } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter';
+import { ConfigApi } from '#/api';
+import { TableAction } from '#/components/table-action';
+
+import FormEdit from './components/edit.vue';
+import { gridOptions, searchFormOptions } from './data.config';
+
+const { hasAccessByCodes } = useAccess();
+
+const [Grid, { reload }] = useVbenVxeGrid({
+  formOptions: searchFormOptions,
+  gridOptions,
+});
+
+const [FormEditModal, formEditApi] = useVbenModal({
+  connectedComponent: FormEdit,
+});
+
+const handleDelete = (id: number) => {
+  Modal.confirm({
+    iconType: 'info',
+    title: '删除提示',
+    content: `确定要删除选择的记录吗?`,
+    cancelText: `关闭`,
+    onOk: async () => {
+      await ConfigApi.deleteDetail(id);
+      message.success('数据删除成功');
+      reload();
+    },
+  });
+};
+
+const handleEdit = (record: any, isUpdate: boolean) => {
+  formEditApi.setData({
+    isUpdate,
+    baseData: { id: record.id },
+  });
+
+  formEditApi.open();
+};
+
+const handelSuccess = () => {
+  reload();
+};
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormEditModal :close-on-click-modal="false" @success="handelSuccess" />
+    <Grid>
+      <template #toolbar-tools>
+        <Button
+          class="mr-2"
+          type="primary"
+          v-access:code="'config:add'"
+          @click="() => handleEdit({}, false)"
+        >
+          新增参数配置
+        </Button>
+      </template>
+      <template #action="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: '编辑',
+              type: 'text',
+              disabled: !hasAccessByCodes(['config:edit']),
+              onClick: handleEdit.bind(null, row, true),
+            },
+            {
+              label: '删除',
+              type: 'text',
+              danger: true,
+              disabled: !hasAccessByCodes(['config:delete']),
+              onClick: handleDelete.bind(null, row.id),
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 80 - 0
apps/web-baicai/src/views/system/department/components/edit.vue

@@ -0,0 +1,80 @@
+<script lang="ts" setup>
+import { onMounted, ref, unref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter';
+import { DepartmentApi } from '#/api';
+
+import { formOptions } from '../data.config';
+
+defineOptions({
+  name: 'ConfigEdit',
+});
+const emit = defineEmits(['success']);
+const modelRef = ref<Record<string, any>>({});
+const isUpdate = ref(true);
+
+const [Form, { validate, setValues, getValues }] = useVbenForm({
+  showDefaultActions: false,
+  ...formOptions,
+});
+
+const [Modal, { close, setState, getData }] = useVbenModal({
+  fullscreenButton: false,
+  draggable: true,
+  onCancel() {
+    close();
+  },
+  onConfirm: async () => {
+    try {
+      const { valid } = await validate();
+      if (valid) {
+        const values = await getValues();
+        setState({ confirmLoading: true });
+        const postParams = unref(modelRef);
+        Object.assign(postParams, values);
+        await (unref(isUpdate)
+          ? DepartmentApi.editDetail(postParams as DepartmentApi.RecordItem)
+          : DepartmentApi.addDetail(
+              postParams as DepartmentApi.BasicRecordItem,
+            ));
+        message.success('操作成功');
+
+        close();
+        emit('success');
+      }
+    } catch {
+      message.error('操作失败');
+    } finally {
+      setState({ confirmLoading: false });
+    }
+  },
+  onOpenChange: async (isOpen: boolean) => {
+    if (isOpen) {
+      setState({ loading: true });
+      const data = getData<Record<string, any>>();
+      isUpdate.value = !!data.isUpdate;
+      modelRef.value = { ...data.baseData };
+      setState({ title: unref(isUpdate) ? '编辑系统配置' : '新增系统配置' });
+
+      if (unref(isUpdate)) {
+        const entity = await DepartmentApi.getDetail(data.baseData.id);
+        modelRef.value = { ...entity };
+        setValues(entity);
+      }
+      setState({ loading: false });
+    }
+  },
+  title: '新增系统配置',
+});
+
+onMounted(async () => {});
+</script>
+<template>
+  <Modal class="w-[1000px]">
+    <Form />
+  </Modal>
+</template>

+ 131 - 0
apps/web-baicai/src/views/system/department/data.config.ts

@@ -0,0 +1,131 @@
+import type { VbenFormProps, VxeGridProps } from '#/adapter';
+
+import { DepartmentApi } from '#/api';
+import { formatterStatus } from '#/api/model';
+
+export const searchFormOptions: VbenFormProps = {
+  schema: [
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: '名称',
+    },
+    {
+      component: 'Input',
+      fieldName: 'code',
+      label: '编码',
+    },
+  ],
+};
+
+export const gridOptions: VxeGridProps<DepartmentApi.RecordItem> = {
+  toolbarConfig: {
+    refresh: true,
+    print: false,
+    export: false,
+    zoom: true,
+    custom: true,
+  },
+  treeConfig: {
+    rowField: 'id',
+    childrenField: 'children',
+  },
+  columns: [
+    { title: '序号', type: 'seq', width: 50 },
+    { align: 'left', field: 'name', title: '名称', width: 200, treeNode: true },
+    {
+      align: 'left',
+      field: 'code',
+      title: '编码',
+      width: 150,
+    },
+    { align: 'left', field: 'remark', title: '备注' },
+    {
+      field: 'status',
+      title: '状态',
+      width: 60,
+      formatter: formatterStatus,
+    },
+    {
+      field: 'action',
+      fixed: 'right',
+      slots: { default: 'action' },
+      title: '操作',
+      width: 110,
+    },
+  ],
+  height: 'auto',
+  keepSource: true,
+  pagerConfig: {
+    enabled: false,
+  },
+  proxyConfig: {
+    ajax: {
+      query: async (_, formValues) => {
+        return await DepartmentApi.getTree({
+          ...formValues,
+        });
+      },
+    },
+  },
+};
+
+export const formOptions: VbenFormProps = {
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+  },
+  schema: [
+    {
+      component: 'ApiTreeSelect',
+      componentProps: {
+        placeholder: '请输入上级',
+        api: {
+          url: DepartmentApi.getTree,
+        },
+        showSearch: true,
+        fieldNames: { label: 'name', value: 'id' },
+      },
+      fieldName: 'parentId',
+      label: '上级',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入编号',
+      },
+      fieldName: 'code',
+      label: '编号',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入名称',
+      },
+      fieldName: 'name',
+      label: '名称',
+      rules: 'required',
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入排序',
+      },
+      fieldName: 'sort',
+      label: '排序',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'remark',
+      label: '备注',
+    },
+  ],
+  wrapperClass: 'grid-cols-1',
+};

+ 88 - 0
apps/web-baicai/src/views/system/department/index.vue

@@ -0,0 +1,88 @@
+<script lang="ts" setup>
+import { useAccess } from '@vben/access';
+import { Page, useVbenModal } from '@vben/common-ui';
+
+import { Button, message, Modal } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter';
+import { DepartmentApi } from '#/api';
+import { TableAction } from '#/components/table-action';
+
+import FormEdit from './components/edit.vue';
+import { gridOptions, searchFormOptions } from './data.config';
+
+const { hasAccessByCodes } = useAccess();
+
+const [Grid, { reload }] = useVbenVxeGrid({
+  formOptions: searchFormOptions,
+  gridOptions,
+});
+
+const [FormEditModal, formEditApi] = useVbenModal({
+  connectedComponent: FormEdit,
+});
+
+const handleDelete = (id: number) => {
+  Modal.confirm({
+    iconType: 'info',
+    title: '删除提示',
+    content: `确定要删除选择的记录吗?`,
+    cancelText: `关闭`,
+    onOk: async () => {
+      await DepartmentApi.deleteDetail(id);
+      message.success('数据删除成功');
+      reload();
+    },
+  });
+};
+
+const handleEdit = (record: any, isUpdate: boolean) => {
+  formEditApi.setData({
+    isUpdate,
+    baseData: { id: record.id },
+  });
+
+  formEditApi.open();
+};
+
+const handelSuccess = () => {
+  reload();
+};
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormEditModal :close-on-click-modal="false" @success="handelSuccess" />
+    <Grid>
+      <template #toolbar-tools>
+        <Button
+          class="mr-2"
+          type="primary"
+          v-access:code="'department:add'"
+          @click="() => handleEdit({}, false)"
+        >
+          新增组织
+        </Button>
+      </template>
+      <template #action="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: '编辑',
+              type: 'text',
+              disabled: !hasAccessByCodes(['department:edit']),
+              onClick: handleEdit.bind(null, row, true),
+            },
+            {
+              label: '删除',
+              type: 'text',
+              danger: true,
+              disabled: !hasAccessByCodes(['department:delete']),
+              onClick: handleDelete.bind(null, row.id),
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 1 - 0
apps/web-baicai/src/views/system/design/query/components/stepBaseConfig.vue

@@ -36,6 +36,7 @@ const [Form, { validate, setValues, getValues }] = useVbenForm({
         api: {
           url: TenantApi.getOptions,
         },
+        showSearch: true,
         numberToString: true,
       },
       fieldName: 'configId',

+ 0 - 2
apps/web-baicai/src/views/system/menu/components/edit.vue

@@ -23,8 +23,6 @@ const [Form, { validate, setValues, getValues }] = useVbenForm({
 });
 
 const [Drawer, { close, setState, getData }] = useVbenDrawer({
-  // fullscreenButton: false,
-  // draggable: true,
   onCancel() {
     close();
   },

+ 142 - 0
apps/web-baicai/src/views/system/menu/components/grant.vue

@@ -0,0 +1,142 @@
+<script lang="ts" setup>
+import type { Recordable } from '@vben/types';
+
+import type { RelationRequest } from '#/api/model';
+
+import { onMounted, reactive, ref, unref } from 'vue';
+
+import { useVbenDrawer } from '@vben/common-ui';
+
+import { Button, message, Tree, type TreeProps } from 'ant-design-vue';
+
+import { MenuApi, RoleApi, TenantApi } from '#/api';
+import { Icon } from '#/components/icon';
+import { getLeafNodeIds } from '#/utils';
+
+defineOptions({
+  name: 'MenuGrant',
+});
+const emit = defineEmits(['success']);
+
+const modelRef = ref<Recordable<any>>({});
+
+const state = reactive<{
+  checkedKeys: number[] | string[];
+  expandedKeys: number[] | string[];
+  lastLeafKeys: number[] | string[];
+  selectedKeys: number[] | string[];
+}>({
+  selectedKeys: [],
+  expandedKeys: [],
+  checkedKeys: [],
+  lastLeafKeys: [],
+});
+
+const treeData = ref<TreeProps['treeData']>([]);
+const isExpand = ref(false);
+
+// 所有节点key
+const allNodeIds = ref([]);
+// 当前展开的key
+const currentExpandedKeys = ref([]);
+
+const getRoleDetailData = async () => {
+  const data =
+    modelRef.value.type === 'tenant'
+      ? await TenantApi.getMenuIds(unref(modelRef).id)
+      : await RoleApi.getMenuIds(unref(modelRef).id);
+  state.checkedKeys = data || [];
+  state.expandedKeys = data || [];
+  state.selectedKeys = data.filter((item: number | string) => {
+    return state.lastLeafKeys.includes(item as never);
+  });
+};
+
+const [Drawer, { close, setState, getData }] = useVbenDrawer({
+  onConfirm: async () => {
+    try {
+      setState({ confirmLoading: true });
+      const postData: RelationRequest = {
+        id: unref(modelRef).id,
+        relationIds: state.checkedKeys,
+      };
+      modelRef.value.type === 'tenant'
+        ? await TenantApi.updateGrant(postData)
+        : await RoleApi.updateGrant(postData);
+      message.success('操作成功');
+      close();
+      emit('success');
+    } catch {
+      message.error('操作失败');
+    } finally {
+      setState({ confirmLoading: false });
+    }
+  },
+  onOpenChange: async (isOpen: boolean) => {
+    if (isOpen) {
+      setState({ loading: true });
+      const data = getData<Recordable<any>>();
+      modelRef.value = { ...data.baseData };
+      await getRoleDetailData();
+      setState({ loading: false });
+    }
+  },
+});
+
+const fetch = async () => {
+  treeData.value = (await MenuApi.getList(
+    {},
+  )) as unknown as TreeProps['treeData'];
+
+  state.lastLeafKeys = getLeafNodeIds(unref(treeData));
+};
+
+onMounted(async () => {
+  await fetch();
+});
+/**
+ * 点击复选框触发处理
+ * @param mCheckedKeys
+ */
+const handleCheck = (mCheckedKeys: any, e: any) => {
+  state.selectedKeys = mCheckedKeys;
+  state.checkedKeys = [...mCheckedKeys, ...e.halfCheckedKeys];
+};
+
+const handleExpand = (expandedKeys: any) => {
+  state.expandedKeys = expandedKeys;
+};
+// 展开折叠按钮事件
+const handleExpandAndCollapse = () => {
+  isExpand.value = !isExpand.value;
+  currentExpandedKeys.value = isExpand.value ? allNodeIds.value : [];
+};
+</script>
+<template>
+  <Drawer class="w-[1000px]" title="授权菜单">
+    <Button type="primary" @click="handleExpandAndCollapse">
+      {{ isExpand ? '折叠' : '展开' }}
+    </Button>
+    <Tree
+      v-model:checked-keys="state.selectedKeys"
+      :block-node="true"
+      :checkable="true"
+      :default-expand-all="true"
+      :expanded-keys="state.expandedKeys"
+      :field-names="{
+        title: 'title',
+        key: 'id',
+      }"
+      :tree-data="treeData"
+      @check="handleCheck"
+      @expand="handleExpand"
+    >
+      <template #title="{ icon, title }">
+        <div class="flex items-center">
+          <Icon v-if="icon" :icon="icon" class="mr-1" />
+          <span>{{ title }}</span>
+        </div>
+      </template>
+    </Tree>
+  </Drawer>
+</template>

+ 1 - 1
apps/web-baicai/src/views/system/menu/data.config.ts

@@ -95,7 +95,7 @@ export const gridOptions: VxeGridProps<MenuApi.RecordItem> = {
       fixed: 'right',
       slots: { default: 'action' },
       title: '操作',
-      width: 160,
+      width: 170,
     },
   ],
   height: 'auto',

+ 78 - 0
apps/web-baicai/src/views/system/role/components/edit.vue

@@ -0,0 +1,78 @@
+<script lang="ts" setup>
+import { onMounted, ref, unref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter';
+import { RoleApi } from '#/api';
+
+import { formOptions } from '../data.config';
+
+defineOptions({
+  name: 'RoleEdit',
+});
+const emit = defineEmits(['success']);
+const modelRef = ref<Record<string, any>>({});
+const isUpdate = ref(true);
+
+const [Form, { validate, setValues, getValues }] = useVbenForm({
+  showDefaultActions: false,
+  ...formOptions,
+});
+
+const [Modal, { close, setState, getData }] = useVbenModal({
+  fullscreenButton: false,
+  draggable: true,
+  onCancel() {
+    close();
+  },
+  onConfirm: async () => {
+    try {
+      const { valid } = await validate();
+      if (valid) {
+        const values = await getValues();
+        setState({ confirmLoading: true });
+        const postParams = unref(modelRef);
+        Object.assign(postParams, values);
+        await (unref(isUpdate)
+          ? RoleApi.editDetail(postParams as RoleApi.RecordItem)
+          : RoleApi.addDetail(postParams as RoleApi.BasicRecordItem));
+        message.success('操作成功');
+
+        close();
+        emit('success');
+      }
+    } catch {
+      message.error('操作失败');
+    } finally {
+      setState({ confirmLoading: false });
+    }
+  },
+  onOpenChange: async (isOpen: boolean) => {
+    if (isOpen) {
+      setState({ loading: true });
+      const data = getData<Record<string, any>>();
+      isUpdate.value = !!data.isUpdate;
+      modelRef.value = { ...data.baseData };
+      setState({ title: unref(isUpdate) ? '编辑角色' : '新增角色' });
+
+      if (unref(isUpdate)) {
+        const entity = await RoleApi.getDetail(data.baseData.id);
+        modelRef.value = { ...entity };
+        setValues(entity);
+      }
+      setState({ loading: false });
+    }
+  },
+  title: '新增角色',
+});
+
+onMounted(async () => {});
+</script>
+<template>
+  <Modal class="w-[1000px]">
+    <Form />
+  </Modal>
+</template>

+ 113 - 0
apps/web-baicai/src/views/system/role/data.config.ts

@@ -0,0 +1,113 @@
+import type { VbenFormProps, VxeGridProps } from '#/adapter';
+
+import { RoleApi } from '#/api';
+import { formatterStatus } from '#/api/model';
+
+export const searchFormOptions: VbenFormProps = {
+  schema: [
+    {
+      component: 'Input',
+      fieldName: 'code',
+      label: '编号',
+    },
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: '名称',
+    },
+  ],
+};
+
+export const gridOptions: VxeGridProps<RoleApi.RecordItem> = {
+  toolbarConfig: {
+    refresh: true,
+    print: false,
+    export: false,
+    zoom: true,
+    custom: true,
+  },
+  columns: [
+    { title: '序号', type: 'seq', width: 50 },
+    {
+      align: 'left',
+      field: 'code',
+      title: '编号',
+      width: 150,
+    },
+    { align: 'left', field: 'name', title: '名称', width: 200 },
+    { align: 'left', field: 'sort', title: '排序', width: 70 },
+    { align: 'left', field: 'remark', title: '备注' },
+    {
+      field: 'status',
+      title: '状态',
+      width: 60,
+      formatter: formatterStatus,
+    },
+    {
+      field: 'action',
+      fixed: 'right',
+      slots: { default: 'action' },
+      title: '操作',
+      width: 170,
+    },
+  ],
+  height: 'auto',
+  keepSource: true,
+  proxyConfig: {
+    ajax: {
+      query: async ({ page }, formValues) => {
+        return await RoleApi.getPage({
+          pageIndex: page.currentPage,
+          pageSize: page.pageSize,
+          ...formValues,
+        });
+      },
+    },
+  },
+};
+
+export const formOptions: VbenFormProps = {
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+  },
+  schema: [
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入编号',
+      },
+      fieldName: 'code',
+      label: '编号',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入名称',
+      },
+      fieldName: 'name',
+      label: '名称',
+      rules: 'required',
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入排序',
+      },
+      fieldName: 'sort',
+      label: '排序',
+      rules: 'required',
+    },
+    {
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'remark',
+      label: '备注',
+    },
+  ],
+  wrapperClass: 'grid-cols-1',
+};

+ 166 - 0
apps/web-baicai/src/views/system/role/index.vue

@@ -0,0 +1,166 @@
+<script lang="ts" setup>
+import { useAccess } from '@vben/access';
+import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
+
+import { Button, message, Modal } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter';
+import { RoleApi, UserApi } from '#/api';
+import { SelectCard } from '#/components/select-card';
+import { TableAction } from '#/components/table-action';
+import MenuGrant from '#/views/system/menu/components/grant.vue';
+
+import FormEdit from './components/edit.vue';
+import { gridOptions, searchFormOptions } from './data.config';
+
+const { hasAccessByCodes } = useAccess();
+
+const [Grid, { reload }] = useVbenVxeGrid({
+  formOptions: searchFormOptions,
+  gridOptions,
+});
+
+const [FormEditModal, formEditApi] = useVbenModal({
+  connectedComponent: FormEdit,
+});
+
+const [MenuGrantModal, menuGrantApi] = useVbenDrawer({
+  connectedComponent: MenuGrant,
+});
+
+const [SelectCardModal, selectCardApi] = useVbenModal({
+  connectedComponent: SelectCard,
+});
+const handleDelete = (id: number) => {
+  Modal.confirm({
+    iconType: 'info',
+    title: '删除提示',
+    content: `确定要删除选择的记录吗?`,
+    cancelText: `关闭`,
+    onOk: async () => {
+      await RoleApi.deleteDetail(id);
+      message.success('数据删除成功');
+      reload();
+    },
+  });
+};
+
+const handleUpdateStatus = async (record: any) => {
+  await RoleApi.updateStatus({
+    id: record.id,
+    status: record.status === 1 ? 2 : 1,
+  });
+  reload();
+};
+
+const handleEdit = (record: any, isUpdate: boolean) => {
+  formEditApi.setData({
+    isUpdate,
+    baseData: { id: record.id },
+  });
+
+  formEditApi.open();
+};
+
+const handleGrantMenu = async (id: number) => {
+  menuGrantApi.setData({
+    baseData: { id, type: 'role' },
+  });
+
+  menuGrantApi.open();
+};
+
+const handleGrantUser = async (id: number) => {
+  const selectedIds = await RoleApi.getUserIds(id);
+  selectCardApi.setData({
+    baseData: {
+      key: id,
+      selectedIds,
+      fieldNames: [
+        { label: '电话', value: 'phone', maxLength: 12 },
+        { label: '姓名', value: 'realName', maxLength: 8 },
+      ],
+      config: {
+        type: 'user',
+      },
+      api: {
+        url: UserApi.getPage,
+      },
+      searchName: 'name',
+    },
+  });
+
+  selectCardApi.open();
+};
+
+const handelSuccess = () => {
+  reload();
+};
+
+const handelUserSuccess = async (data: any) => {
+  await RoleApi.updateGrantUser({ id: data.key, relationIds: data.ids });
+  message.success('授权成功');
+};
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormEditModal :close-on-click-modal="false" @success="handelSuccess" />
+    <MenuGrantModal :close-on-click-modal="false" />
+    <SelectCardModal
+      :close-on-click-modal="false"
+      title="用户"
+      @success="handelUserSuccess"
+    />
+    <Grid>
+      <template #toolbar-tools>
+        <Button
+          class="mr-2"
+          type="primary"
+          v-access:code="'role:add'"
+          @click="() => handleEdit({}, false)"
+        >
+          新增角色
+        </Button>
+      </template>
+      <template #action="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: '编辑',
+              type: 'text',
+              disabled: !hasAccessByCodes(['role:edit']),
+              onClick: handleEdit.bind(null, row, true),
+            },
+            {
+              label: '授权菜单',
+              type: 'text',
+              disabled: !hasAccessByCodes(['role:grantMenu']),
+              onClick: handleGrantMenu.bind(null, row.id),
+            },
+          ]"
+          :drop-down-actions="[
+            {
+              label: '添加成员',
+              type: 'link',
+              disabled: !hasAccessByCodes(['role:grantUser']),
+              onClick: handleGrantUser.bind(null, row.id),
+            },
+            {
+              label: row.status === 1 ? '禁用' : '启用',
+              type: 'link',
+              disabled: !hasAccessByCodes(['role:setStatus']),
+              onClick: handleUpdateStatus.bind(null, row),
+            },
+            {
+              label: '删除',
+              type: 'link',
+              disabled: !hasAccessByCodes(['role:delete']),
+              onClick: handleDelete.bind(null, row.id),
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 1 - 1
apps/web-baicai/src/views/system/tenant/data.config.ts

@@ -55,7 +55,7 @@ export const gridOptions: VxeGridProps<TenantApi.RecordItem> = {
       fixed: 'right',
       slots: { default: 'action' },
       title: '操作',
-      width: 160,
+      width: 170,
     },
   ],
   height: 'auto',

+ 78 - 0
apps/web-baicai/src/views/system/user/components/edit.vue

@@ -0,0 +1,78 @@
+<script lang="ts" setup>
+import { onMounted, ref, unref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter';
+import { UserApi } from '#/api';
+
+import { formOptions } from '../data.config';
+
+defineOptions({
+  name: 'UserEdit',
+});
+const emit = defineEmits(['success']);
+const modelRef = ref<Record<string, any>>({});
+const isUpdate = ref(true);
+
+const [Form, { validate, setValues, getValues }] = useVbenForm({
+  showDefaultActions: false,
+  ...formOptions,
+});
+
+const [Modal, { close, setState, getData }] = useVbenModal({
+  fullscreenButton: false,
+  draggable: true,
+  onCancel() {
+    close();
+  },
+  onConfirm: async () => {
+    try {
+      const { valid } = await validate();
+      if (valid) {
+        const values = await getValues();
+        setState({ confirmLoading: true });
+        const postParams = unref(modelRef);
+        Object.assign(postParams, values);
+        await (unref(isUpdate)
+          ? UserApi.editDetail(postParams as UserApi.RecordItem)
+          : UserApi.addDetail(postParams as UserApi.BasicRecordItem));
+        message.success('操作成功');
+
+        close();
+        emit('success');
+      }
+    } catch {
+      message.error('操作失败');
+    } finally {
+      setState({ confirmLoading: false });
+    }
+  },
+  onOpenChange: async (isOpen: boolean) => {
+    if (isOpen) {
+      setState({ loading: true });
+      const data = getData<Record<string, any>>();
+      isUpdate.value = !!data.isUpdate;
+      modelRef.value = { ...data.baseData };
+      setState({ title: unref(isUpdate) ? '编辑用户' : '新增用户' });
+
+      if (unref(isUpdate)) {
+        const entity = await UserApi.getDetail(data.baseData.id);
+        modelRef.value = { ...entity };
+        setValues(entity);
+      }
+      setState({ loading: false });
+    }
+  },
+  title: '新增用户',
+});
+
+onMounted(async () => {});
+</script>
+<template>
+  <Modal class="w-[1000px]">
+    <Form />
+  </Modal>
+</template>

+ 166 - 0
apps/web-baicai/src/views/system/user/data.config.ts

@@ -0,0 +1,166 @@
+import type { VbenFormProps, VxeGridProps } from '#/adapter';
+
+import { EnumApi, UserApi } from '#/api';
+import { formatterStatus } from '#/api/model';
+
+export const searchFormOptions: VbenFormProps = {
+  schema: [
+    {
+      component: 'Input',
+      fieldName: 'account',
+      label: '账号',
+    },
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: '姓名',
+    },
+  ],
+};
+
+export const gridOptions: VxeGridProps<UserApi.RecordItem> = {
+  toolbarConfig: {
+    refresh: true,
+    print: false,
+    export: false,
+    zoom: true,
+    custom: true,
+  },
+  columns: [
+    { title: '序号', type: 'seq', width: 50 },
+    {
+      align: 'left',
+      field: 'account',
+      title: '账号',
+      width: 200,
+    },
+    { align: 'left', field: 'realName', title: '姓名', width: 120 },
+    { align: 'left', field: 'remark', title: '备注' },
+    {
+      field: 'status',
+      title: '状态',
+      width: 60,
+      formatter: formatterStatus,
+    },
+    {
+      field: 'action',
+      fixed: 'right',
+      slots: { default: 'action' },
+      title: '操作',
+      width: 170,
+    },
+  ],
+  height: 'auto',
+  keepSource: true,
+  proxyConfig: {
+    ajax: {
+      query: async ({ page }, formValues) => {
+        return await UserApi.getPage({
+          pageIndex: page.currentPage,
+          pageSize: page.pageSize,
+          ...formValues,
+        });
+      },
+    },
+  },
+};
+
+export const formOptions: VbenFormProps = {
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+  },
+  schema: [
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入组织',
+      },
+      fieldName: 'departmentId',
+      label: '组织',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入账号',
+      },
+      fieldName: 'account',
+      label: '账号',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入密码',
+      },
+      fieldName: 'password',
+      label: '密码',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入姓名',
+      },
+      fieldName: 'realName',
+      label: '姓名',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入昵称',
+      },
+      fieldName: 'nickName',
+      label: '昵称',
+    },
+    {
+      component: 'ApiRadio',
+      componentProps: {
+        placeholder: '请输入',
+        api: {
+          type: 'enum',
+          params: EnumApi.EnumType.Gender,
+        },
+      },
+      fieldName: 'sex',
+      label: '性别',
+    },
+    {
+      component: 'DatePicker',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'birthday',
+      label: '生日',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入电话',
+      },
+      fieldName: 'phone',
+      label: '电话',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入邮箱',
+      },
+      fieldName: 'email',
+      label: '邮箱',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'remark',
+      label: '备注',
+      formItemClass: 'col-span-2 items-baseline',
+    },
+  ],
+  wrapperClass: 'grid-cols-2',
+};

+ 177 - 0
apps/web-baicai/src/views/system/user/index.vue

@@ -0,0 +1,177 @@
+<script lang="ts" setup>
+import { useAccess } from '@vben/access';
+import { Page, useVbenModal } from '@vben/common-ui';
+
+import { useClipboard } from '@vueuse/core';
+import { Button, message, Modal } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter';
+import { RoleApi, UserApi } from '#/api';
+import { SelectCard } from '#/components/select-card';
+import { TableAction } from '#/components/table-action';
+
+import FormEdit from './components/edit.vue';
+import { gridOptions, searchFormOptions } from './data.config';
+
+const { hasAccessByCodes } = useAccess();
+
+const { copy } = useClipboard({ legacy: true });
+
+const [Grid, { reload }] = useVbenVxeGrid({
+  formOptions: searchFormOptions,
+  gridOptions,
+});
+
+const [FormEditModal, formEditApi] = useVbenModal({
+  connectedComponent: FormEdit,
+});
+
+const [SelectCardModal, selectCardApi] = useVbenModal({
+  connectedComponent: SelectCard,
+});
+
+const handelResetPassword = (id: number) => {
+  Modal.confirm({
+    iconType: 'info',
+    title: '重置密码提示',
+    content: `确定要重置选择的记录吗?`,
+    cancelText: `关闭`,
+    onOk: async () => {
+      const data = await UserApi.resetPassword([id]);
+      Modal.success({
+        title: '重置成功',
+        content: `新密码为:${data}`,
+        okText: `复制密码`,
+        onOk: () => {
+          copy(data);
+          message.success('密码复制成功!');
+        },
+      });
+    },
+  });
+};
+
+const handleDelete = (id: number) => {
+  Modal.confirm({
+    iconType: 'info',
+    title: '删除提示',
+    content: `确定要删除选择的记录吗?`,
+    cancelText: `关闭`,
+    onOk: async () => {
+      await UserApi.deleteDetail(id);
+      message.success('数据删除成功');
+      reload();
+    },
+  });
+};
+
+const handleUpdateStatus = async (record: any) => {
+  await UserApi.updateStatus({
+    id: record.id,
+    status: record.status === 1 ? 2 : 1,
+  });
+  reload();
+};
+
+const handleEdit = (record: any, isUpdate: boolean) => {
+  formEditApi.setData({
+    isUpdate,
+    baseData: { id: record.id },
+  });
+
+  formEditApi.open();
+};
+
+const handelGrantRole = async (id: number) => {
+  const selectedIds = await UserApi.getRoleIds(id);
+  selectCardApi.setData({
+    baseData: {
+      key: id,
+      selectedIds,
+      fieldNames: [
+        { label: '编号', value: 'code', maxLength: 12 },
+        { label: '名称', value: 'name', maxLength: 8 },
+      ],
+      config: {
+        type: 'role',
+      },
+      api: {
+        url: RoleApi.getPage,
+      },
+      searchName: 'name',
+    },
+  });
+
+  selectCardApi.open();
+};
+
+const handelSuccess = () => {
+  reload();
+};
+
+const handelRoleSuccess = async (data: any) => {
+  await UserApi.updateGrantRole({ id: data.key, relationIds: data.ids });
+  message.success('授权成功');
+};
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormEditModal :close-on-click-modal="false" @success="handelSuccess" />
+    <SelectCardModal
+      :close-on-click-modal="false"
+      title="角色"
+      @success="handelRoleSuccess"
+    />
+    <Grid>
+      <template #toolbar-tools>
+        <Button
+          class="mr-2"
+          type="primary"
+          v-access:code="'user:add'"
+          @click="() => handleEdit({}, false)"
+        >
+          新增用户
+        </Button>
+      </template>
+      <template #action="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: '编辑',
+              type: 'text',
+              disabled: !hasAccessByCodes(['user:edit']),
+              onClick: handleEdit.bind(null, row, true),
+            },
+            {
+              label: '重置密码',
+              type: 'text',
+              disabled: !hasAccessByCodes(['user:resetPwd']),
+              onClick: handelResetPassword.bind(null, row.id),
+            },
+          ]"
+          :drop-down-actions="[
+            {
+              label: row.status === 1 ? '禁用' : '启用',
+              type: 'link',
+              disabled: !hasAccessByCodes(['user:setStatus']),
+              onClick: handleUpdateStatus.bind(null, row),
+            },
+            {
+              label: '角色管理',
+              type: 'link',
+              disabled: !hasAccessByCodes(['user:grantRole']),
+              onClick: handelGrantRole.bind(null, row.id),
+            },
+            {
+              label: '删除',
+              type: 'link',
+              disabled: !hasAccessByCodes(['user:delete']),
+              onClick: handleDelete.bind(null, row.id),
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>