Explorar el Código

feat(系统管理): 添加国际化管理页面

shizhongming hace 2 años
padre
commit
cd82bb8f78
Se han modificado 27 ficheros con 2803 adiciones y 2 borrados
  1. 3 0
      src/components/Form/src/componentMap.ts
  2. 4 0
      src/components/Form/src/types/index.ts
  3. 1 1
      src/components/SmartTable/src/components/SmartTableColumnConfig.tsx
  4. 2 1
      src/components/registerGlobComp.ts
  5. 345 0
      src/hooks/web/useCrud.ts
  6. 31 0
      src/modules/system/views/dept/SysDept.api.ts
  7. 214 0
      src/modules/system/views/dept/SysDeptTreeList.vue
  8. 195 0
      src/modules/system/views/dept/components/SysDeptEdit.vue
  9. 46 0
      src/modules/system/views/dept/components/SysDeptEditModal.vue
  10. 233 0
      src/modules/system/views/dept/components/SysDeptTree.vue
  11. 45 0
      src/modules/system/views/dept/lang/en_US.ts
  12. 50 0
      src/modules/system/views/dept/lang/zh_CN.ts
  13. 59 0
      src/modules/system/views/i18n/I18nMainView.vue
  14. 136 0
      src/modules/system/views/i18n/components/I18nGroupList.vue
  15. 112 0
      src/modules/system/views/i18n/components/I18nItemList.vue
  16. 111 0
      src/modules/system/views/i18n/components/i18n.api.ts
  17. 189 0
      src/modules/system/views/i18n/components/i18n.config.ts
  18. 176 0
      src/modules/system/views/i18n/components/i18nList.vue
  19. 39 0
      src/modules/system/views/i18n/lang/en_US.ts
  20. 39 0
      src/modules/system/views/i18n/lang/zh_CN.ts
  21. 61 0
      src/modules/system/views/userGroup/UserGroupListView.api.ts
  22. 164 0
      src/modules/system/views/userGroup/UserGroupListView.config.ts
  23. 134 0
      src/modules/system/views/userGroup/UserGroupListView.vue
  24. 313 0
      src/modules/system/views/userGroup/UserGroupSupport.ts
  25. 53 0
      src/modules/system/views/userGroup/hooks/useSetUser.ts
  26. 24 0
      src/modules/system/views/userGroup/lang/en_US.ts
  27. 24 0
      src/modules/system/views/userGroup/lang/zh_CN.ts

+ 3 - 0
src/components/Form/src/componentMap.ts

@@ -34,6 +34,7 @@ import { IconPicker } from '@/components/Icon';
 import { CountdownInput } from '@/components/CountDown';
 import { BasicTitle } from '@/components/Basic';
 import { CropperAvatar } from '@/components/Cropper';
+import SmartApiSelectDict from './smart-boot/components/SmartApiSelectDict.vue';
 
 const componentMap = new Map<ComponentType | string, Component>();
 
@@ -79,6 +80,8 @@ componentMap.set('CropperAvatar', CropperAvatar);
 
 componentMap.set('BasicTitle', BasicTitle);
 
+componentMap.set('SmartApiSelectDict', SmartApiSelectDict);
+
 export function add<T extends string, R extends Component>(
   compName: ComponentType | T,
   component: R,

+ 4 - 0
src/components/Form/src/types/index.ts

@@ -124,6 +124,9 @@ interface _CustomComponents {
   InputCountDown: ExtractPropTypes<
     (typeof import('@/components/CountDown/src/CountdownInput.vue'))['default']
   >;
+  SmartApiSelectDict: ExtractPropTypes<
+    (typeof import('@/components/Form/src/smart-boot/components/SmartApiSelectDict.vue'))['default']
+  >;
 }
 
 type CustomComponents<T = _CustomComponents> = {
@@ -173,4 +176,5 @@ export interface ComponentProps {
   Transfer: ExtractPropTypes<(typeof import('ant-design-vue/es/transfer'))['default']>;
   CropperAvatar: CustomComponents['CropperAvatar'];
   BasicTitle: CustomComponents['BasicTitle'];
+  SmartApiSelectDict: CustomComponents['SmartApiSelectDict'] & ComponentProps['ApiSelect'];
 }

+ 1 - 1
src/components/SmartTable/src/components/SmartTableColumnConfig.tsx

@@ -122,7 +122,7 @@ export default defineComponent({
     const buttonEvent = {
       onClick: () => {
         const trigger = props.config.trigger;
-        if (trigger === 'click') {
+        if (!trigger || trigger === 'click') {
           showPanel();
         }
       },

+ 2 - 1
src/components/registerGlobComp.ts

@@ -1,6 +1,6 @@
 import type { App } from 'vue';
 import { Button } from './Button';
-import { Input, Layout, Radio, Tag, Select, Tooltip, Tree } from 'ant-design-vue';
+import { Input, Layout, Radio, Tag, Select, Tooltip, Tree, Tabs } from 'ant-design-vue';
 import VXETable from 'vxe-table';
 
 import { i18n } from '@/locales/setupI18n';
@@ -30,5 +30,6 @@ export function registerGlobComp(app: App) {
     .use(Select)
     .use(Tooltip)
     .use(Tree)
+    .use(Tabs)
     .use(VXETable);
 }

+ 345 - 0
src/hooks/web/useCrud.ts

@@ -0,0 +1,345 @@
+import { ref, reactive, computed, createVNode } from 'vue';
+import type { Ref } from 'vue';
+import { message, Modal } from 'ant-design-vue';
+import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
+
+type Params = { page?: number; limit?: number; sortName?: string; sortOrder?: string };
+
+type Data = { total: number; rows: Array<any> } | Array<any>;
+
+/**
+ * 参数类型
+ */
+type Parameter = {
+  defaultPageSize?: number;
+  paging: boolean;
+  defaultSorter?: { sortName: string; sortOrder: string };
+  // 搜索参数
+  defaultParameter?: { [index: string]: any };
+};
+
+/**
+ * VXE TABLE加载数据hook
+ * @param service 加载数据服务
+ * @param parameter hook 参数
+ */
+export const useVxeTable = (
+  service: (params: Params, searchParameter: any) => Promise<Data>,
+  parameter: Parameter = { paging: true },
+) => {
+  // 数据加载状态
+  const loading = ref(false);
+  // 表格数据
+  const data = ref<Array<any>>([]);
+  // 分页数据
+  const tablePage: any = parameter.paging
+    ? reactive({
+        total: 0,
+        currentPage: 1,
+        pageSize: parameter.defaultPageSize || 500,
+      })
+    : {};
+  const searchModel = ref<any>(Object.assign({}, parameter.defaultParameter || {}));
+  // 排序数据
+  const sortData: any = reactive(parameter.defaultSorter || {});
+  const sortConfig: any = {
+    remote: true,
+  };
+  if (parameter.defaultSorter) {
+    sortConfig.defaultSort = {
+      field: parameter.defaultSorter.sortName,
+      order: parameter.defaultSorter.sortOrder,
+    };
+  }
+
+  /**
+   * 加载数据函数
+   */
+  const loadData = async () => {
+    const allParameter: any = {
+      ...sortData,
+    };
+    if (parameter.paging) {
+      // 添加分页数据
+      Object.assign(allParameter, {
+        limit: tablePage.pageSize,
+        page: tablePage.currentPage,
+      });
+    }
+    loading.value = true;
+    try {
+      const result = await service(allParameter, searchModel.value);
+      if (parameter.paging) {
+        const { total, rows } = result as any;
+        tablePage.total = total;
+        data.value = rows;
+      } else {
+        data.value = result as Array<any>;
+      }
+    } catch (e) {
+      // do nothing
+    } finally {
+      loading.value = false;
+    }
+  };
+  /**
+   * 重置擦欧总
+   */
+  const handleReset = () => {
+    searchModel.value = Object.assign({}, parameter.defaultParameter || {});
+    loadData();
+  };
+
+  /**
+   * 重置分页页数
+   */
+  const handleResetPage = () => {
+    tablePage.currentPage = 1;
+  };
+
+  /**
+   * 排序变化时触发
+   * @param property
+   * @param order
+   */
+  const sortChange = ({ property, order }: any) => {
+    sortData.sortName = property;
+    sortData.sortOrder = order;
+    loadData();
+  };
+  /**
+   * 分页改变时触发
+   * @param currentPage 当前页面
+   * @param pageSize 页面大小
+   */
+  const handlePageChange = ({ currentPage, pageSize }: any) => {
+    if (parameter.paging) {
+      tablePage.currentPage = currentPage;
+      tablePage.pageSize = pageSize;
+      loadData();
+    }
+  };
+
+  return {
+    tableProps: computed(() => {
+      return {
+        loading: loading.value,
+        data: data.value,
+        onSortChange: sortChange,
+        sortConfig: reactive(sortConfig),
+      };
+    }),
+    loadData,
+    handleReset,
+    handleResetPage,
+    searchModel,
+    pageProps: computed(() => {
+      return {
+        currentPage: tablePage.currentPage,
+        pageSize: tablePage.pageSize,
+        total: tablePage.total,
+        onPageChange: handlePageChange,
+      };
+    }),
+  };
+};
+
+type AddEditParameter = {
+  idField?: string;
+  defaultModel?: any;
+};
+
+/**
+ * 添加编辑操作
+ * @param gridRef grid 引用
+ * @param loadHandler 加载数据函数
+ * @param listHandler 查询数据函数
+ * @param saveHandler 保存函数
+ * @param i18nRender 国际化函数
+ * @param parameter 参数
+ */
+export const useAddEdit = (
+  gridRef: Ref,
+  loadHandler: (id: any) => Promise<any>,
+  listHandler: Function | null,
+  saveHandler: (model: any) => Promise<void>,
+  i18nRender: Function,
+  parameter: AddEditParameter = {},
+) => {
+  const formRef = ref();
+  // 是否是加载状态
+  const isAdd = ref(false);
+  // 保存加载状态
+  const saveLoading = ref(false);
+  // 加载状态
+  const getLoading = ref(false);
+  // modal显示状态
+  const modalVisible = ref(false);
+  const addEditModel = ref(Object.assign({}, parameter.defaultModel || {}));
+  // modal标题计算属性
+  const computedTitle = computed(() => {
+    return isAdd.value ? i18nRender('common.button.add') : i18nRender('common.button.edit');
+  });
+
+  /**
+   * 添加编辑操作
+   * @param add 是否是添加操作
+   * @param id ID
+   */
+  const handleAddEdit = async (add: boolean, id: any | null) => {
+    isAdd.value = add;
+    // 显示弹窗
+    modalVisible.value = true;
+    if (add) {
+      addEditModel.value = Object.assign({}, parameter.defaultModel || {});
+    } else {
+      try {
+        getLoading.value = true;
+        addEditModel.value = (await loadHandler(id)) || {};
+      } catch (e) {
+        // doNotiong
+      } finally {
+        getLoading.value = false;
+      }
+    }
+  };
+
+  /**
+   * 设置model函数
+   * @param model model
+   */
+  const handleSetModel = (model: any) => {
+    addEditModel.value = model;
+  };
+
+  /**
+   * 通过checkbox 获取需要编辑的数据
+   */
+  const handleEditByCheckbox = () => {
+    // 获取选中行
+    const selectRows = gridRef.value.getCheckboxRecords();
+    if (selectRows.length !== 1) {
+      message.error(i18nRender('common.notice.choseOne'));
+      return false;
+    }
+    handleAddEdit(false, selectRows[0][parameter.idField!]);
+  };
+
+  /**
+   * 保存修改操作
+   */
+  const handleSaveUpdate = async () => {
+    try {
+      await formRef.value.validate();
+    } catch (e) {
+      return false;
+    }
+    saveLoading.value = true;
+    try {
+      await saveHandler(addEditModel.value);
+    } catch (e) {
+      // do noting
+      return false;
+    } finally {
+      saveLoading.value = false;
+    }
+    // 关闭弹窗
+    modalVisible.value = false;
+    if (listHandler) {
+      listHandler();
+    }
+  };
+
+  const handleCancel = () => {
+    modalVisible.value = false;
+  };
+  return {
+    modalProps: computed(() => {
+      return {
+        title: computedTitle.value,
+        visible: modalVisible.value,
+        confirmLoading: saveLoading.value,
+        onOk: handleSaveUpdate,
+        onCancel: handleCancel,
+      };
+    }),
+    formProps: computed(() => {
+      return {
+        model: addEditModel.value,
+        ref: 'formRef',
+      };
+    }),
+    spinning: getLoading,
+    handleAddEdit,
+    handleEditByCheckbox,
+    formRef,
+    handleSetModel,
+    formModel: addEditModel,
+  };
+};
+
+type DeleteParameter = {
+  idField: string;
+  listHandler?: Function;
+  afterDelete?: Function;
+};
+
+/**
+ * 删除操作
+ */
+export const useVxeDelete = (
+  gridRef: Ref | null,
+  i18nRender: Function,
+  deleteHandler: (idList: Array<any>) => Promise<void>,
+  parameter: DeleteParameter,
+) => {
+  const doDelete = (idList: Array<any>) => {
+    Modal.confirm({
+      title: i18nRender('common.button.confirm'),
+      icon: createVNode(ExclamationCircleOutlined),
+      content: i18nRender('common.notice.deleteConfirm'),
+      onOk: async () => {
+        await deleteHandler(idList);
+        message.success(i18nRender('common.message.deleteSuccess'));
+        if (parameter.afterDelete) {
+          parameter.afterDelete();
+        }
+      },
+    });
+  };
+
+  /**
+   * 删除checkbox选中
+   */
+  const handleDeleteByCheckbox = () => {
+    // 获取选中行
+    const selectRows = gridRef?.value.getCheckboxRecords();
+    if (selectRows.length === 0) {
+      message.error(i18nRender('common.notice.deleteChoose'));
+      return false;
+    }
+    doDelete(selectRows.map((item: any) => item[parameter.idField]));
+  };
+
+  /**
+   * 通过ID删除
+   * @param id
+   */
+  const handleDeleteById = (id: any) => {
+    doDelete([id]);
+  };
+
+  /**
+   * 通过行删除
+   * @param row
+   */
+  const handleDeleteByRow = (row: any) => {
+    doDelete([row[parameter.idField]]);
+  };
+
+  return {
+    handleDeleteByCheckbox,
+    handleDeleteById,
+    handleDeleteByRow,
+  };
+};

+ 31 - 0
src/modules/system/views/dept/SysDept.api.ts

@@ -0,0 +1,31 @@
+import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+
+enum Api {
+  getById = 'sys/dept/getById',
+  saveUpdateBatch = 'sys/dept/saveUpdateBatch',
+  delete = 'sys/dept/batchDeleteById',
+}
+
+export const getByIdApi = (params) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.getById,
+    data: params,
+  });
+};
+
+export const saveUpdateBatchApi = (modelList: any[]) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.saveUpdateBatch,
+    data: modelList,
+  });
+};
+
+export const deleteApi = (ids: number[]) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.delete,
+    data: ids,
+  });
+};

+ 214 - 0
src/modules/system/views/dept/SysDeptTreeList.vue

@@ -0,0 +1,214 @@
+<template>
+  <div class="full-height dept-container" style="padding: 10px">
+    <div class="full-height left-tree" style="padding: 10px; background: white">
+      <div>
+        <a-button
+          v-permission="'sys:dept:save'"
+          type="primary"
+          :size="buttonSizeConfig"
+          @click="handleAdd"
+        >
+          <plus-outlined />
+          {{ $t('common.button.add') }}
+        </a-button>
+        <a-button
+          type="primary"
+          v-permission="'sys:dept:save'"
+          :size="buttonSizeConfig"
+          style="margin-left: 5px"
+          @click="handleAddChild"
+        >
+          <plus-outlined />
+          {{ $t('system.views.dept.button.addChild') }}
+        </a-button>
+        <a-button
+          type="primary"
+          style="margin-left: 5px"
+          v-permission="'sys:dept:delete'"
+          danger
+          :size="buttonSizeConfig"
+          @click="handleDelete"
+        >
+          <delete-outlined />
+          {{ $t('common.button.delete') }}
+        </a-button>
+      </div>
+      <sys-dept-tree style="margin-top: 5px" ref="treeRef" show-search @select="handleTreeSelect" />
+    </div>
+    <div class="full-height right-tab">
+      <a-tabs>
+        <a-tab-pane :tab="$t('system.views.dept.title.baseMessage')">
+          <sys-dept-edit ref="formRef" :filter-field="false" :dept-id="currentDeptRef?.deptId" />
+          <div style="text-align: right">
+            <a-button
+              :loading="saveLoading"
+              @click="handleSave"
+              :disabled="currentDeptRef === null || currentDeptRef === undefined"
+              style=" margin-right: 10px;text-align: right"
+              type="primary"
+            >
+              {{ $t('common.button.save') }}
+            </a-button>
+          </div>
+        </a-tab-pane>
+      </a-tabs>
+    </div>
+    <SysDeptEditModal @after-save="reloadDeptTree" @register="registerModal" />
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, ref, unref } from 'vue';
+  import { useI18n } from 'vue-i18n';
+
+  import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
+
+  import { useSizeSetting } from '@/hooks/setting/UseSizeSetting';
+  import { useModal } from '@/components/Modal';
+  import { errorMessage, successMessage } from '@/utils/message/SystemNotice';
+
+  import SysDeptTree from './components/SysDeptTree.vue';
+  import SysDeptEdit from './components/SysDeptEdit.vue';
+  import SysDeptEditModal from './components/SysDeptEditModal.vue';
+  import { deleteApi, saveUpdateBatchApi } from './SysDept.api';
+  import { useVxeDelete } from '@/hooks/web/useCrud';
+
+  /**
+   * 部门管理树
+   */
+  export default defineComponent({
+    name: 'SysDeptTreeList',
+    components: {
+      SysDeptTree,
+      PlusOutlined,
+      DeleteOutlined,
+      SysDeptEdit,
+      SysDeptEditModal,
+    },
+    setup() {
+      const gridRef = ref();
+      const treeRef = ref();
+      const formRef = ref();
+
+      const { t } = useI18n();
+      const parentFieldVisible = ref(false);
+
+      const [registerModal, { openModal }] = useModal();
+
+      /**
+       * 当前选中节点的code
+       */
+      const currentDeptRef = ref<Recordable | null>(null);
+      const handleTreeSelect = (_: Array<number>, { selectedNodes, selected }) => {
+        if (!selected || selectedNodes.length === 0) {
+          currentDeptRef.value = null;
+        }
+        currentDeptRef.value = selectedNodes[0];
+      };
+
+      const reloadDeptTree = () => {
+        treeRef.value.reload();
+      };
+
+      /**
+       * 添加操作函数
+       */
+      const handleAdd = () => {
+        openModal(true, {
+          parentId: 0,
+          parentName: '根',
+        });
+      };
+      /**
+       * 添加下级
+       */
+      const handleAddChild = () => {
+        const currentDept = unref(currentDeptRef);
+        if (!currentDept) {
+          errorMessage(t('system.views.dept.message.selectDeptError'));
+          return false;
+        }
+        const { deptId, deptName } = unref(currentDeptRef) || {};
+        openModal(true, {
+          parentId: deptId,
+          parentName: deptName,
+        });
+      };
+
+      /**
+       * 删除hook
+       */
+      const { handleDeleteById } = useVxeDelete(null, t, deleteApi, {
+        idField: 'deptId',
+        afterDelete: () => reloadDeptTree(),
+      });
+      const handleDelete = () => {
+        const currentDept = unref(currentDeptRef);
+        if (!currentDept) {
+          errorMessage(t('common.notice.deleteChoose'));
+          return false;
+        }
+        handleDeleteById(currentDept.deptId);
+      };
+
+      /**
+       * 保存操作
+       */
+      const saveLoading = ref(false);
+      const handleSave = async () => {
+        const formModel = await unref(formRef).validate();
+        try {
+          saveLoading.value = true;
+          await saveUpdateBatchApi([formModel]);
+          console.log();
+          successMessage(t('common.message.saveSuccess'));
+          await reloadDeptTree();
+        } finally {
+          saveLoading.value = false;
+        }
+      };
+      return {
+        gridRef,
+        treeRef,
+        handleTreeSelect,
+        parentFieldVisible,
+        handleDelete,
+        ...useSizeSetting(),
+        handleAdd,
+        registerModal,
+        reloadDeptTree,
+        currentDeptRef,
+        handleAddChild,
+        formRef,
+        handleSave,
+        saveLoading,
+      };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  @leftWidth: 40%;
+
+  .left-tree {
+    display: inline-block;
+    width: @leftWidth;
+  }
+
+  .right-tab {
+    display: inline-block;
+    width: calc(60% - 10px);
+    margin-left: 10px;
+    padding: 10px;
+    float: right;
+    background: white;
+
+    :deep(.ant-tabs) {
+      height: 100%;
+
+      .ant-tabs-content {
+        height: 100%;
+      }
+    }
+  }
+</style>

+ 195 - 0
src/modules/system/views/dept/components/SysDeptEdit.vue

@@ -0,0 +1,195 @@
+<template>
+  <Spin :spinning="getLoading">
+    <BasicForm @register="registerForm" :size="getFormSize" />
+  </Spin>
+</template>
+
+<script lang="ts" setup>
+  import type { FormSchema } from '@/components/Form';
+  import { Spin } from 'ant-design-vue';
+
+  import { ref, watch } from 'vue';
+  import { useForm, BasicForm } from '@/components/Form';
+  import { propTypes } from '@/utils/propTypes';
+  import { useI18n } from '@/hooks/web/useI18n';
+  import { useSizeSetting } from '@/hooks/setting/UseSizeSetting';
+
+  import { getByIdApi } from '../SysDept.api';
+
+  const props = defineProps({
+    filterField: propTypes.bool,
+    deptId: propTypes.number,
+  });
+
+  const { t } = useI18n();
+  const { getFormSize } = useSizeSetting();
+
+  const getLoading = ref(false);
+
+  watch(
+    () => props.deptId,
+    async (value) => {
+      if (!value) {
+        await resetFields();
+      } else {
+        try {
+          getLoading.value = true;
+          const deptData = await getByIdApi(value);
+          if (deptData.parentDept) {
+            deptData.parentName = deptData.parentDept.deptName;
+          } else {
+            deptData.parentName = '根';
+          }
+          await setFieldsValue(deptData);
+        } finally {
+          getLoading.value = false;
+        }
+      }
+    },
+  );
+
+  const formSchemas: Array<FormSchema & { filter?: boolean }> = [
+    {
+      label: '',
+      field: 'deptId',
+      component: 'Input',
+      show: false,
+    },
+    {
+      label: '',
+      field: 'parentId',
+      component: 'Input',
+      show: false,
+    },
+    {
+      label: t('system.views.dept.title.parent'),
+      field: 'parentName',
+      component: 'Input',
+      componentProps: {
+        disabled: true,
+      },
+      // filter: true,
+    },
+    {
+      label: t('system.views.dept.title.deptCode'),
+      field: 'deptCode',
+      component: 'Input',
+      required: true,
+    },
+    {
+      label: t('system.views.dept.title.deptType'),
+      field: 'deptType',
+      component: 'SmartApiSelectDict',
+      required: true,
+      componentProps: {
+        dictCode: 'SYSTEM_ORGANIZATION_TYPE',
+      },
+    },
+    {
+      label: t('system.views.dept.title.deptName'),
+      field: 'deptName',
+      required: true,
+      component: 'Input',
+    },
+    {
+      label: t('common.table.useYn'),
+      field: 'useYn',
+      component: 'Switch',
+      defaultValue: true,
+    },
+    {
+      label: t('system.views.dept.title.email'),
+      field: 'email',
+      component: 'Input',
+    },
+    {
+      label: t('system.views.dept.title.director'),
+      field: 'director',
+      component: 'Input',
+    },
+    {
+      label: t('system.views.dept.title.phone'),
+      field: 'phone',
+      component: 'Input',
+    },
+    {
+      label: t('common.table.seq'),
+      field: 'seq',
+      component: 'InputNumber',
+      defaultValue: 1,
+      required: true,
+    },
+    {
+      label: t('common.table.remark'),
+      field: 'remark',
+      component: 'InputTextArea',
+    },
+    {
+      label: t('common.table.createUser'),
+      field: 'createBy',
+      component: 'Input',
+      filter: true,
+      componentProps: {
+        disabled: true,
+      },
+    },
+    {
+      label: t('common.table.createTime'),
+      field: 'createTime',
+      component: 'Input',
+      filter: true,
+      componentProps: {
+        disabled: true,
+      },
+    },
+    {
+      label: t('common.table.updateUser'),
+      field: 'updateBy',
+      component: 'Input',
+      filter: true,
+      componentProps: {
+        disabled: true,
+      },
+    },
+    {
+      label: t('common.table.updateTime'),
+      field: 'updateTime',
+      component: 'Input',
+      filter: true,
+      componentProps: {
+        disabled: true,
+      },
+    },
+  ];
+
+  const getFormSchemas = () => {
+    if (!props.filterField) {
+      return formSchemas;
+    }
+    return formSchemas.filter((item) => item.filter === undefined || !item.filter);
+  };
+
+  const [registerForm, { setFieldsValue, validate, clearValidate, resetFields }] = useForm({
+    colon: true,
+    schemas: getFormSchemas(),
+    labelCol: {
+      span: 6,
+    },
+    wrapperCol: {
+      span: 17,
+    },
+    baseColProps: {
+      span: 24,
+    },
+    showActionButtonGroup: false,
+  });
+
+  defineExpose({
+    setFieldsValue,
+    validate,
+    clearValidate,
+    resetFields,
+  });
+</script>
+
+<style scoped></style>

+ 46 - 0
src/modules/system/views/dept/components/SysDeptEditModal.vue

@@ -0,0 +1,46 @@
+<template>
+  <BasicModal
+    @register="registerModal"
+    @ok="handleOk"
+    :okText="$t('common.button.save')"
+    :title="$t('common.button.add')"
+  >
+    <SysDeptEdit ref="formRef" filter-field />
+  </BasicModal>
+</template>
+
+<script lang="ts" setup>
+  import { useModalInner, BasicModal } from '@/components/Modal';
+  import { ref, unref } from 'vue';
+  import { successMessage } from '@/utils/message/SystemNotice';
+  import { useI18n } from '@/hooks/web/useI18n';
+
+  import SysDeptEdit from './SysDeptEdit.vue';
+  import { saveUpdateBatchApi } from '../SysDept.api';
+
+  const emit = defineEmits(['after-save', 'register']);
+
+  const { t } = useI18n();
+
+  const formRef = ref();
+
+  const [registerModal, { changeOkLoading, closeModal }] = useModalInner(async (data) => {
+    await unref(formRef).resetFields({});
+    await unref(formRef).setFieldsValue(data);
+  });
+
+  const handleOk = async () => {
+    const model = await unref(formRef).validate();
+    try {
+      changeOkLoading(true);
+      await saveUpdateBatchApi([model]);
+      successMessage(t('common.message.saveSuccess'));
+      closeModal();
+      emit('after-save');
+    } finally {
+      changeOkLoading(false);
+    }
+  };
+</script>
+
+<style scoped></style>

+ 233 - 0
src/modules/system/views/dept/components/SysDeptTree.vue

@@ -0,0 +1,233 @@
+<template>
+  <div>
+    <div v-if="showSearch" class="search-container">
+      <a-input-search
+        v-model:value="searchValue"
+        :placeholder="$t('system.views.dept.search.deptName')"
+      />
+    </div>
+    <Spin :spinning="loading">
+      <a-tree
+        v-bind="getAttrs"
+        :expanded-keys="expandedKeys"
+        :auto-expand-parent="autoExpandParent"
+        @expand="onExpand"
+        :field-names="fieldNames"
+        :tree-data="computedTreeData"
+      >
+        <template #title="{ deptName }">
+          <span v-if="!showSearch">
+            {{ deptName }}
+          </span>
+          <span v-else-if="deptName.indexOf(searchValue) > -1">
+            {{ deptName.substr(0, deptName.indexOf(searchValue)) }}
+            <span style="color: #f50">{{ searchValue }}</span>
+            {{ deptName.substr(deptName.indexOf(searchValue) + searchValue.length) }}
+          </span>
+          <span v-else>{{ deptName }}</span>
+        </template>
+      </a-tree>
+    </Spin>
+  </div>
+</template>
+
+<script lang="ts">
+  import { computed, defineComponent, onMounted, reactive, ref, toRefs, unref, watch } from 'vue';
+
+  import { Spin } from 'ant-design-vue';
+  import { errorMessage } from '@/utils/message/SystemNotice';
+  import TreeUtils from '@/utils/TreeUtils';
+  import { propTypes } from '@/utils/propTypes';
+  import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+
+  const getParentKey = (key: number, treeData: Array<any>): number => {
+    let parentKey;
+    for (let i = 0; i < treeData.length; i++) {
+      const node = treeData[i];
+      if (node.children) {
+        if (node.children.some((item: any) => item.deptId === key)) {
+          parentKey = node.deptId;
+        } else {
+          const secondParentKey = getParentKey(key, node.children);
+          if (secondParentKey) {
+            parentKey = secondParentKey;
+          }
+        }
+      }
+    }
+    return parentKey;
+  };
+
+  export default defineComponent({
+    name: 'SysDeptTree',
+    components: { Spin },
+    props: {
+      // 是否支持搜索
+      showSearch: propTypes.bool.def(true),
+      // 是否异步加载
+      async: propTypes.bool,
+    },
+    setup(props, { attrs }) {
+      const { async: asyncRef } = toRefs(props);
+      const searchValue = ref<string>('');
+
+      const dataList = ref<Array<any>>([]);
+      const autoExpandParent = ref(false);
+      const expandedKeys = ref<Array<number>>([]);
+      const loading = ref(false);
+
+      const getAttrs = computed(() => {
+        const result: any = {
+          ...attrs,
+        };
+        if (unref(asyncRef)) {
+          result.loadData = handleAsyncLoadData;
+        }
+        return result;
+      });
+
+      /**
+       * 树形数据计算属性
+       */
+      const computedTreeData = computed(() => {
+        const async = unref(asyncRef);
+        if (async) {
+          return unref(dataList);
+        }
+        return (
+          TreeUtils.convertList2Tree(
+            dataList.value,
+            (item) => item.deptId,
+            (item) => item.parentId,
+            0,
+          ) || []
+        );
+      });
+
+      const onExpand = (keys: Array<number>) => {
+        expandedKeys.value = keys;
+        autoExpandParent.value = false;
+      };
+
+      /**
+       * 所有数据
+       */
+      const getAllDataList = computed(() => {
+        const result: any[] = [];
+        if (unref(asyncRef)) {
+          recursionAddChildren(unref(dataList), result);
+        } else {
+          result.push(...unref(dataList));
+        }
+        return result;
+      });
+
+      const recursionAddChildren = (list: any[], allData: any[]) => {
+        list.forEach((item) => {
+          allData.push(item);
+          if (item.children && item.children.length > 0) {
+            recursionAddChildren(item.children, allData);
+          }
+        });
+      };
+
+      watch(searchValue, (value) => {
+        const allData = unref(getAllDataList);
+        expandedKeys.value = allData
+          .map(({ deptName, deptId }: any) => {
+            if (deptName.indexOf(value) > -1) {
+              return getParentKey(deptId, computedTreeData.value);
+            }
+            return null;
+          })
+          .filter((item, i, self) => item && self.indexOf(item) === i) as Array<number>;
+        autoExpandParent.value = true;
+      });
+
+      const handleAsyncLoadData = async (treeNode) => {
+        const dataRef = treeNode.dataRef;
+        dataRef.children = await loadData(dataRef.deptId);
+        dataList.value = [...unref(dataList)];
+      };
+
+      const reload = () => loadData();
+
+      /**
+       * 加载数据函数
+       */
+      const loadData = async (parentId?: number | null) => {
+        const parameter: Recordable = {
+          sortName: 'seq',
+          sortOrder: 'asc',
+        };
+        if (parentId !== undefined && parentId !== null) {
+          parameter.parameter = {
+            'parentId@=': parentId,
+          };
+        }
+        try {
+          loading.value = true;
+          const result = (await defHttp.post({
+            service: ApiServiceEnum.SMART_SYSTEM,
+            url: 'sys/dept/list',
+            data: parameter,
+          })) as any[];
+
+          result.forEach((item) => {
+            if (item.hasChild !== true) {
+              item.isLeaf = true;
+            }
+          });
+          if (unref(asyncRef)) {
+            if (parentId === 0) {
+              dataList.value = result;
+            } else {
+              return result;
+            }
+          } else {
+            dataList.value = result;
+          }
+        } catch (e) {
+          errorMessage(e);
+        } finally {
+          loading.value = false;
+        }
+      };
+
+      /**
+       * 加载数据
+       */
+      onMounted(() => {
+        let parentId: number | undefined = undefined;
+        if (unref(asyncRef)) {
+          parentId = 0;
+        }
+        loadData(parentId);
+      });
+
+      return {
+        computedTreeData,
+        autoExpandParent,
+        onExpand,
+        loadData,
+        loading,
+        expandedKeys,
+        fieldNames: reactive({
+          children: 'children',
+          title: 'deptName',
+          key: 'deptId',
+        }),
+        getAttrs,
+        handleAsyncLoadData,
+        searchValue,
+        reload,
+      };
+    },
+  });
+</script>
+
+<style scoped lang="less">
+  .search-container {
+    margin-bottom: 10px;
+  }
+</style>

+ 45 - 0
src/modules/system/views/dept/lang/en_US.ts

@@ -0,0 +1,45 @@
+/**
+ * 部门表 国际化信息
+ */
+export default {
+  trans: true,
+  key: 'system.views.dept',
+  data: {
+    title: {
+      deptId: '部门ID',
+      parent: 'Parent org',
+      deptCode: 'Org code',
+      deptType: 'Org type',
+      deptName: 'Org name',
+      email: 'Email',
+      director: 'Director',
+      phone: 'Phone',
+      baseMessage: 'Basic message',
+    },
+    validate: {
+      deptId: '请输入部门ID',
+      parentId: '请输入上级ID',
+      deptCode: 'Please enter org code',
+      deptType: 'Please select org type',
+      deptName: 'Please enter org name',
+      email: 'Please enter email',
+      director: 'Please enter director',
+      phone: 'Please enter phone',
+    },
+    rules: {
+      deptCode_NOT_EMPTY: 'Org code cannot be empty',
+      deptType_NOT_EMPTY: 'Please select org type',
+      deptName_NOT_EMPTY: 'Org name cannot be empty',
+    },
+    search: {
+      deptCode: 'Please enter org code',
+      deptName: 'Please enter org name',
+    },
+    button: {
+      addChild: 'Add child',
+    },
+    message: {
+      selectDeptError: 'Please select parent org',
+    },
+  },
+};

+ 50 - 0
src/modules/system/views/dept/lang/zh_CN.ts

@@ -0,0 +1,50 @@
+/**
+ * 部门表 国际化信息
+ */
+export default {
+  trans: true,
+  key: 'system.views.dept',
+  data: {
+    title: {
+      deptId: '部门ID',
+      parent: '上级组织',
+      deptCode: '组织编码',
+      deptType: '组织类型',
+      deptName: '组织名称',
+      email: '邮箱',
+      director: '负责人',
+      phone: '电话',
+      baseMessage: '基本信息',
+    },
+    validate: {
+      deptId: '请输入部门ID',
+      parentId: '请输入上级ID',
+      deptCode: '请输入组织编码',
+      deptType: '请输入组织类型',
+      deptName: '请输入组织名称',
+      email: '请输入邮箱',
+      director: '请输入负责人',
+      phone: '请输入电话',
+      deleteYn: '请输入删除状态',
+      createUserId: '请输入创建人ID',
+      createTime: '请输入创建时间',
+      updateUserId: '请输入更新人员ID',
+      updateTime: '请输入更新时间',
+    },
+    rules: {
+      deptCode_NOT_EMPTY: '组织编码不能为空',
+      deptType_NOT_EMPTY: '请选择组织类型',
+      deptName_NOT_EMPTY: '请输入组织名称',
+    },
+    search: {
+      deptCode: '请输入组织编码',
+      deptName: '请输入组织名称',
+    },
+    button: {
+      addChild: '添加下级',
+    },
+    message: {
+      selectDeptError: '请选择上级组织',
+    },
+  },
+};

+ 59 - 0
src/modules/system/views/i18n/I18nMainView.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="full-height page-container">
+    <a-layout class="full-height i18n-main-container">
+      <a-layout-sider theme="light" width="300px" class="full-height i18n-group-container">
+        <!--    国际化分组    -->
+        <I18nGroupList @current-change="({ row }) => (groupId = row.groupId)" />
+      </a-layout-sider>
+      <a-layout>
+        <a-layout-content class="i18n-container" style=" margin-bottom: 2px;background: #f0f2f5">
+          <I18nList :group-id="groupId" @change="(id) => (i18nId = id)" />
+        </a-layout-content>
+        <a-layout-content class="i18n-item-container">
+          <I18nItemList :i18n-id="i18nId" />
+        </a-layout-content>
+      </a-layout>
+    </a-layout>
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, ref } from 'vue';
+
+  import I18nGroupList from './components/I18nGroupList.vue';
+  import I18nList from './components/i18nList.vue';
+  import I18nItemList from './components/I18nItemList.vue';
+
+  export default defineComponent({
+    name: 'I18nMainView',
+    components: {
+      I18nGroupList,
+      I18nList,
+      I18nItemList,
+    },
+    setup() {
+      const groupId = ref<number>();
+      const i18nId = ref<number>();
+      return {
+        groupId,
+        i18nId,
+      };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  .i18n-main-container {
+    .i18n-group-container {
+      margin-right: 5px;
+    }
+
+    .i18n-container {
+      height: 60%;
+    }
+
+    .i18n-item-container {
+      height: 40%;
+    }
+  }
+</style>

+ 136 - 0
src/modules/system/views/i18n/components/I18nGroupList.vue

@@ -0,0 +1,136 @@
+<template>
+  <div class="full-height" :class="prefixCls">
+    <div class="table-container">
+      <SmartTable @register="registerTable" v-bind="$attrs">
+        <template #table-groupName="{ row }">
+          <span @contextmenu="(e) => handleContext(e, row)">{{ row.groupName }}</span>
+        </template>
+      </SmartTable>
+    </div>
+    <div class="button-container">
+      <a-button
+        v-permission="SystemPermissions.i18n.add"
+        class="button"
+        block
+        type="primary"
+        @click="() => showAddModal()"
+      >
+        {{ $t('common.button.add') }}
+      </a-button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { SmartTable, useSmartTable } from '@/components/SmartTable';
+  import { useDesign } from '@/hooks/web/useDesign';
+  import { useContextMenu } from '@/hooks/web/useContextMenu';
+  import { useI18n } from '@/hooks/web/useI18n';
+  import { listGroupApi, getGroupByIdApi, saveUpdateGroupApi, deleteGroupApi } from './i18n.api';
+  import { SystemPermissions } from '@/modules/system/constants/SystemConstants';
+
+  const { t } = useI18n();
+
+  const { prefixCls } = useDesign('smart-system-i18nGroup');
+
+  const [createContextMenu] = useContextMenu();
+  const handleContext = (e: MouseEvent, row: Recordable) => {
+    createContextMenu({
+      event: e,
+      items: [
+        {
+          label: t('common.button.edit'),
+          icon: 'ant-design:edit-outlined',
+          handler: () => {
+            editByRowModal(row);
+          },
+        },
+        {
+          label: t('common.button.delete'),
+          icon: 'ant-design:delete-outlined',
+          handler: () => {
+            deleteByRow(row);
+          },
+        },
+      ],
+    });
+  };
+
+  const [registerTable, { editByRowModal, deleteByRow, showAddModal }] = useSmartTable({
+    size: 'small',
+    stripe: true,
+    rowConfig: { isCurrent: true },
+    height: 'auto',
+    cellClassName: 'cursor-pointer',
+    columns: [
+      {
+        title: '{system.views.i18n.group.groupName}',
+        field: 'groupName',
+        slots: {
+          default: 'table-groupName',
+        },
+      },
+    ],
+    addEditConfig: {
+      formConfig: {
+        colon: true,
+        schemas: [
+          {
+            label: '',
+            component: 'Input',
+            field: 'groupId',
+            show: false,
+          },
+          {
+            label: t('system.views.i18n.group.groupName'),
+            component: 'Input',
+            field: 'groupName',
+            required: true,
+          },
+          {
+            label: t('system.views.i18n.group.seq'),
+            component: 'InputNumber',
+            field: 'seq',
+            defaultValue: 1,
+            required: true,
+          },
+        ],
+      },
+    },
+    proxyConfig: {
+      ajax: {
+        query: listGroupApi,
+        getById: (row) => getGroupByIdApi(row.groupId),
+        save: ({ body: { insertRecords, updateRecords } }) =>
+          saveUpdateGroupApi([...insertRecords, ...updateRecords][0]),
+        delete: ({ body: { removeRecords } }) => {
+          return deleteGroupApi(removeRecords.map((item) => item.groupId));
+        },
+      },
+    },
+  });
+</script>
+
+<style lang="less">
+  @buttonContainerHeight: 60px;
+  @prefix-cls: ~'@{namespace}-smart-system-i18nGroup';
+  .@{prefix-cls} {
+    .smart-table-container {
+      padding: 0;
+    }
+
+    .table-container {
+      height: calc(100% - @buttonContainerHeight);
+    }
+
+    .button-container {
+      height: @buttonContainerHeight;
+      line-height: @buttonContainerHeight;
+      text-align: center;
+
+      .button {
+        width: 90%;
+      }
+    }
+  }
+</style>

+ 112 - 0
src/modules/system/views/i18n/components/I18nItemList.vue

@@ -0,0 +1,112 @@
+<template>
+  <div class="full-height">
+    <SmartTable @register="registerTable" :size="getTableSize">
+      <template #table-operation="{ row }">
+        <SmartVxeTableAction :actions="getTableActions(row)" />
+      </template>
+    </SmartTable>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import {
+    useSmartTable,
+    SmartTable,
+    SmartVxeTableAction,
+    type ActionItem,
+  } from '@/components/SmartTable';
+  import { useSizeSetting } from '@/hooks/setting/UseSizeSetting';
+
+  import { getI18nItemListTableColumn, getI18nItemListAddEditFormSchema } from './i18n.config';
+  import {
+    listI18nItemApi,
+    batchDeleteI18nItemByIdApi,
+    saveUpdateI18nItemApi,
+    getI18nItemByIdApi,
+  } from './i18n.api';
+  import { propTypes } from '@/utils/propTypes';
+  import { SystemPermissions } from '@/modules/system/constants/SystemConstants';
+  import { useI18n } from '@/hooks/web/useI18n';
+  import { watch } from 'vue';
+
+  const props = defineProps({
+    i18nId: propTypes.number,
+  });
+
+  const { t } = useI18n();
+  const { getTableSize } = useSizeSetting();
+
+  const getTableActions = (row): ActionItem[] => {
+    return [
+      {
+        auth: SystemPermissions.i18n.update,
+        label: t('common.button.edit'),
+        onClick: () => {
+          editByRowModal(row);
+        },
+      },
+    ];
+  };
+
+  watch(
+    () => props.i18nId,
+    () => query(),
+  );
+
+  const [registerTable, { editByRowModal, query }] = useSmartTable({
+    id: 'smart-system-i18n-i18nItemList',
+    height: 'auto',
+    stripe: true,
+    border: true,
+    columns: getI18nItemListTableColumn(),
+    columnConfig: {
+      resizable: true,
+    },
+    rowConfig: {
+      isCurrent: true,
+    },
+    customConfig: { storage: true },
+    addEditConfig: {
+      formConfig: {
+        schemas: getI18nItemListAddEditFormSchema(t),
+      },
+    },
+    proxyConfig: {
+      ajax: {
+        query: async ({ ajaxParameter }) => {
+          if (!props.i18nId) {
+            return [];
+          }
+          return listI18nItemApi({
+            ...ajaxParameter,
+            parameter: {
+              ...ajaxParameter?.parameter,
+              'i18nId@=': props.i18nId,
+            },
+          });
+        },
+        delete: ({ body: { removeRecords } }) =>
+          batchDeleteI18nItemByIdApi(removeRecords.map((item) => item.i18nItemId)),
+        getById: ({ i18nItemId }) => getI18nItemByIdApi(i18nItemId),
+        save: ({ body: { insertRecords, updateRecords } }) => {
+          const model = [...insertRecords, ...updateRecords][0];
+          return saveUpdateI18nItemApi({
+            ...model,
+            i18nId: props.i18nId,
+          });
+        },
+      },
+    },
+    toolbarConfig: {
+      refresh: true,
+      resizable: true,
+      zoom: true,
+      column: {
+        columnOrder: true,
+      },
+      buttons: [{ code: 'ModalAdd' }, { code: 'delete' }],
+    },
+  });
+</script>
+
+<style scoped lang="less"></style>

+ 111 - 0
src/modules/system/views/i18n/components/i18n.api.ts

@@ -0,0 +1,111 @@
+import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+
+enum Api {
+  listI18n = 'sys/i18n/list',
+  getI18nById = 'sys/i18n/getById',
+  i18nSaveUpdate = 'sys/i18n/saveUpdate',
+  i18nDelete = 'sys/i18n/batchDeleteById',
+  getGroupById = 'sys/i18n/getGroupById',
+  listGroup = 'sys/i18n/listGroup',
+  saveUpdateGroup = 'sys/i18n/saveOrUpdateGroup',
+  deleteGroup = 'sys/i18n/deleteGroup',
+  getI18nItemById = 'sys/i18nItem/getById',
+  saveUpdateI18nItem = 'sys/i18nItem/saveUpdate',
+  batchDeleteI18nItemById = 'sys/i18nItem/batchDeleteById',
+  listI18nItem = 'sys/i18nItem/list',
+}
+
+export const listI18nApi = (params: Recordable) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.listI18n,
+    data: params,
+  });
+};
+
+export const getI18nByIdApi = (id: number) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.getI18nById,
+    data: id,
+  });
+};
+
+export const i18nSaveUpdateApi = (model: any) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.i18nSaveUpdate,
+    data: model,
+  });
+};
+
+export const i18nDeleteApi = (deleteData: any[]) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.i18nDelete,
+    data: deleteData.map((item) => item.i18nId),
+  });
+};
+
+export const getGroupByIdApi = (groupId: number) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.getGroupById,
+    data: groupId,
+  });
+};
+
+export const listGroupApi = () => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.listGroup,
+  });
+};
+
+export const saveUpdateGroupApi = (model: Recordable) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.saveUpdateGroup,
+    data: model,
+  });
+};
+
+export const deleteGroupApi = (ids: number[]) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.deleteGroup,
+    data: ids,
+  });
+};
+
+export const getI18nItemByIdApi = (id: number) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.getI18nItemById,
+    data: id,
+  });
+};
+
+export const saveUpdateI18nItemApi = (data: Recordable) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.saveUpdateI18nItem,
+    data,
+  });
+};
+
+export const batchDeleteI18nItemByIdApi = (idList) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.batchDeleteI18nItemById,
+    data: idList,
+  });
+};
+
+export const listI18nItemApi = (data) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.listI18nItem,
+    data: data,
+  });
+};

+ 189 - 0
src/modules/system/views/i18n/components/i18n.config.ts

@@ -0,0 +1,189 @@
+import type { SmartColumn } from '@/components/SmartTable';
+import type { FormSchema } from '@/components/Form';
+import dayjs from 'dayjs';
+import { tableUseYnClass } from '@/components/SmartTable';
+
+export const getI18nTableColumns = (): SmartColumn[] => {
+  return [
+    {
+      type: 'checkbox',
+      width: 60,
+      align: 'center',
+      fixed: 'left',
+    },
+    {
+      field: 'platform',
+      title: '{system.views.i18n.i18n.titlePlatform}',
+      width: 120,
+    },
+    {
+      field: 'i18nCode',
+      title: '{system.views.i18n.i18n.titleI18nCode}',
+      minWidth: 260,
+    },
+    {
+      field: 'remark',
+      title: '{common.table.remark}',
+      width: 200,
+    },
+    {
+      field: 'createTime',
+      title: '{common.table.createTime}',
+      width: 160,
+    },
+    {
+      field: 'createUser',
+      title: '{common.table.createUser}',
+      width: 120,
+    },
+    {
+      field: 'updateTime',
+      title: '{common.table.updateTime}',
+      width: 160,
+    },
+    {
+      field: 'updateUser',
+      title: '{common.table.updateUser}',
+      width: 120,
+    },
+    {
+      ...tableUseYnClass(),
+      sortable: true,
+    },
+    {
+      field: 'seq',
+      title: '{common.table.seq}',
+      width: 120,
+      sortable: true,
+    },
+    {
+      field: 'i18nId',
+      title: '{common.table.operation}',
+      width: 120,
+      fixed: 'right',
+      slots: {
+        default: 'table-operation',
+      },
+    },
+  ];
+};
+
+export const getI18nAddEditSchemas = (t: Function): FormSchema[] => {
+  return [
+    {
+      label: '',
+      field: 'groupId',
+      component: 'Input',
+      show: false,
+    },
+    {
+      label: '',
+      field: 'i18nId',
+      component: 'Input',
+      show: false,
+    },
+    {
+      label: t('system.views.i18n.i18n.titlePlatform'),
+      field: 'platform',
+      component: 'Select',
+      required: true,
+      componentProps: {
+        options: [
+          {
+            label: 'backstage',
+            value: 'backstage',
+          },
+        ],
+      },
+    },
+    {
+      label: t('system.views.i18n.i18n.titleI18nCode'),
+      field: 'i18nCode',
+      component: 'Input',
+      required: true,
+    },
+    {
+      label: t('common.table.remark'),
+      field: 'remark',
+      component: 'InputTextArea',
+    },
+    {
+      label: t('common.table.seq'),
+      field: 'seq',
+      component: 'InputNumber',
+      componentProps: {
+        style: { width: '100%' },
+      },
+      required: true,
+    },
+  ];
+};
+
+export const getI18nItemListTableColumn = (): SmartColumn[] => {
+  return [
+    {
+      type: 'checkbox',
+      width: 60,
+      align: 'center',
+      fixed: 'left',
+    },
+    {
+      title: '{system.views.i18n.i18nItem.titleLocale}',
+      field: 'locale',
+      width: 120,
+    },
+    {
+      title: '{system.views.i18n.i18nItem.titleValue}',
+      field: 'value',
+      minWidth: 160,
+    },
+    {
+      field: 'createTime',
+      title: '{common.table.createTime}',
+      width: 160,
+      formatter: ({ cellValue }: any) => {
+        if (cellValue) {
+          return dayjs(cellValue).format('YYYY-MM-DD HH:mm:ss');
+        }
+        return '';
+      },
+    },
+    {
+      field: 'createUser',
+      title: '{common.table.createUser}',
+      width: 120,
+    },
+    {
+      field: 'i18nItemId',
+      title: '{common.table.operation}',
+      width: 120,
+      fixed: 'right',
+      slots: {
+        default: 'table-operation',
+      },
+    },
+  ];
+};
+
+export const getI18nItemListAddEditFormSchema = (t: Function): FormSchema[] => {
+  return [
+    {
+      label: '',
+      field: 'i18nItemId',
+      component: 'Input',
+      show: false,
+    },
+    {
+      label: t('system.views.i18n.i18nItem.titleLocale'),
+      field: 'locale',
+      component: 'Input',
+      required: true,
+    },
+    {
+      label: t('system.views.i18n.i18nItem.titleValue'),
+      field: 'value',
+      component: 'InputTextArea',
+      required: true,
+    },
+  ];
+};

+ 176 - 0
src/modules/system/views/i18n/components/i18nList.vue

@@ -0,0 +1,176 @@
+<template>
+  <div class="full-height">
+    <SmartTable
+      @register="registerTable"
+      :size="getTableSize"
+      @current-change="handleCurrentChange"
+    >
+      <template #table-operation="{ row }">
+        <SmartVxeTableAction :actions="getTableActions(row)" />
+      </template>
+    </SmartTable>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { useSmartTable, SmartTable, SmartVxeTableAction } from '@/components/SmartTable';
+  import { propTypes } from '@/utils/propTypes';
+  import { useI18n } from '@/hooks/web/useI18n';
+  import { useSizeSetting } from '@/hooks/setting/UseSizeSetting';
+  import { message, Modal } from 'ant-design-vue';
+  import { createVNode, watch } from 'vue';
+  import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
+  import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+
+  import { getI18nTableColumns, getI18nAddEditSchemas } from './i18n.config';
+  import { listI18nApi, getI18nByIdApi, i18nSaveUpdateApi, i18nDeleteApi } from './i18n.api';
+  import { SystemPermissions } from '@/modules/system/constants/SystemConstants';
+
+  const props = defineProps({
+    groupId: propTypes.number,
+  });
+  const emit = defineEmits(['change']);
+
+  const { t } = useI18n();
+  const { getTableSize } = useSizeSetting();
+
+  watch(
+    () => props.groupId,
+    async () => {
+      await query();
+      emit('change', null);
+    },
+  );
+
+  const permissions = SystemPermissions.i18n;
+
+  const getTableActions = (row: any) => {
+    return [
+      {
+        label: t('common.button.edit'),
+        preIcon: 'ant-design:edit-out-lined',
+        auth: permissions.update,
+        onClick: () => editByRowModal(row),
+      },
+    ];
+  };
+
+  const handleCurrentChange = ({ row }) => {
+    emit('change', row.i18nId);
+  };
+
+  const [registerTable, { query, editByRowModal }] = useSmartTable({
+    id: 'smart-system-i18n-i18nList',
+    height: 'auto',
+    stripe: true,
+    columns: getI18nTableColumns(),
+    useSearchForm: true,
+    pagerConfig: true,
+    border: true,
+    showOverflow: 'tooltip',
+    rowConfig: {
+      isCurrent: true,
+    },
+    customConfig: { storage: true },
+    columnConfig: {
+      resizable: true,
+    },
+    sortConfig: {
+      remote: true,
+      defaultSort: { field: 'seq', order: 'asc' },
+    },
+    searchFormConfig: {
+      searchWithSymbol: true,
+      schemas: [
+        {
+          label: t('system.views.i18n.i18n.titleI18nCode'),
+          field: 'i18nCode',
+          component: 'Input',
+          searchSymbol: 'like',
+        },
+      ],
+      compact: true,
+      colon: true,
+      layout: 'inline',
+      actionColOptions: { span: undefined },
+    },
+    addEditConfig: {
+      formConfig: {
+        baseColProps: {
+          span: 24,
+        },
+        schemas: getI18nAddEditSchemas(t),
+        labelCol: { span: 5 },
+        wrapperCol: { span: 17 },
+      },
+    },
+    toolbarConfig: {
+      refresh: true,
+      resizable: true,
+      zoom: true,
+      column: {
+        columnOrder: true,
+      },
+      buttons: [
+        {
+          name: t('system.views.i18n.i18n.button.reload'),
+          customRender: 'ant',
+          auth: permissions.reload,
+          props: {
+            preIcon: 'ant-design:reload-outlined',
+            type: 'primary',
+            onClick: () => handleReload(),
+            size: 'small',
+          },
+        },
+        { code: 'ModalAdd' },
+        { code: 'ModalEdit' },
+        { code: 'delete' },
+      ],
+    },
+    proxyConfig: {
+      ajax: {
+        query: ({ ajaxParameter }) => {
+          const parameter = {
+            ...ajaxParameter,
+            parameter: {
+              ...ajaxParameter?.parameter,
+              'groupId@=': props.groupId,
+            },
+          };
+          return listI18nApi(parameter);
+        },
+        getById: (model) => getI18nByIdApi(model.i18nId),
+        save: ({ body: { insertRecords, updateRecords } }) => {
+          if (insertRecords?.length > 0) {
+            insertRecords.forEach((item) => {
+              item.groupId = props.groupId;
+            });
+          }
+          return i18nSaveUpdateApi([...insertRecords, ...updateRecords][0]);
+        },
+        delete: ({ body: { removeRecords } }) => i18nDeleteApi(removeRecords),
+      },
+    },
+  });
+
+  /**
+   * 刷新国际化信息
+   */
+  const handleReload = async () => {
+    Modal.confirm({
+      title: t('system.views.i18n.i18n.message.reloadConfirm'),
+      content: t('system.views.i18n.i18n.message.reloadContent'),
+      icon: createVNode(ExclamationCircleOutlined),
+      onOk: async () => {
+        await defHttp.post({
+          service: ApiServiceEnum.SMART_SYSTEM,
+          url: 'sys/i18n/reload',
+        });
+        message.success(t('system.views.i18n.i18n.message.reloadSuccess'));
+      },
+    });
+  };
+</script>
+
+<style lang="less" scoped></style>

+ 39 - 0
src/modules/system/views/i18n/lang/en_US.ts

@@ -0,0 +1,39 @@
+export default {
+  system: {
+    views: {
+      i18n: {
+        group: {
+          groupName: 'Group name',
+          seq: 'Seq',
+          groupNameValidate: 'Please enter i18n group',
+          seqValidate: 'Please enter seq',
+        },
+        i18n: {
+          titlePlatform: 'Platform',
+          titleI18nCode: 'I18n code',
+          platformValidate: 'Please select a platform',
+          i18nCodeValidate: 'Please enter i18n code',
+          groupIdValidate: 'Please click Group first, and then add',
+          platform: {
+            backstage: 'Backstage',
+          },
+          button: {
+            reload: 'Reload',
+          },
+          message: {
+            reloadConfirm: 'Are you sure you want to reload I18N?',
+            reloadContent: 'You need to log in again after reloading',
+            reloadSuccess: 'Reload success',
+          },
+        },
+        i18nItem: {
+          titleLocale: 'Locale',
+          titleValue: 'Value',
+          localeValidate: '请输入语言',
+          valueValidate: 'Please enter locale',
+          i18nIdValidate: 'Please select i18n before adding',
+        },
+      },
+    },
+  },
+};

+ 39 - 0
src/modules/system/views/i18n/lang/zh_CN.ts

@@ -0,0 +1,39 @@
+export default {
+  system: {
+    views: {
+      i18n: {
+        group: {
+          groupName: '国际化组',
+          seq: '序号',
+          groupNameValidate: '请输入国际化组',
+          seqValidate: '请输入序号',
+        },
+        i18n: {
+          titlePlatform: '平台',
+          titleI18nCode: '国际化编码',
+          platformValidate: '请选择平台',
+          i18nCodeValidate: '请输入国际化编码',
+          groupIdValidate: '请先点击分组,再执行添加操作',
+          platform: {
+            backstage: '后台',
+          },
+          button: {
+            reload: '重新载入',
+          },
+          message: {
+            reloadConfirm: '确定要刷新国际化信息吗?',
+            reloadContent: '刷新后需要重新登陆',
+            reloadSuccess: '重新加载成功',
+          },
+        },
+        i18nItem: {
+          titleLocale: '语言',
+          titleValue: 'value',
+          localeValidate: '请输入语言',
+          valueValidate: '请输入value',
+          i18nIdValidate: '请先选择国际化信息,再执行添加操作',
+        },
+      },
+    },
+  },
+};

+ 61 - 0
src/modules/system/views/userGroup/UserGroupListView.api.ts

@@ -0,0 +1,61 @@
+import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+
+enum Api {
+  list = 'sys/userGroup/list',
+  batchSaveUpdate = 'sys/userGroup/batchSaveUpdate',
+  delete = 'sys/userGroup/batchDeleteById',
+  getById = 'sys/userGroup/getById',
+  listUserIdByGroupId = 'sys/userGroup/listUserIdById',
+  setUser = 'sys/userGroup/saveUserGroupByGroupId',
+}
+
+export const listApi = (parameter) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.list,
+    data: parameter,
+  });
+};
+
+export const batchSaveUpdateApi = (dataList: any[]) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.batchSaveUpdate,
+    data: dataList,
+  });
+};
+
+export const deleteApi = (dataList: any[]) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.delete,
+    data: dataList.map((item) => item.groupId),
+  });
+};
+
+export const getByIdApi = (data) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.getById,
+    data: data.groupId,
+  });
+};
+
+export const listUserIdByGroupIdApi = (groupId: number) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.listUserIdByGroupId,
+    data: groupId,
+  });
+};
+
+export const setUserApi = (groupId: number, userIdList: number[]) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.setUser,
+    data: {
+      groupId,
+      userIdList,
+    },
+  });
+};

+ 164 - 0
src/modules/system/views/userGroup/UserGroupListView.config.ts

@@ -0,0 +1,164 @@
+import type { SmartColumn, SmartSearchFormSchema } from '@/components/SmartTable';
+import { tableUseYnClass } from '@/components/SmartTable';
+import type { FormSchema } from '@/components/Form';
+
+export const getTableColumns = (): SmartColumn[] => {
+  return [
+    {
+      type: 'checkbox',
+      width: 60,
+      align: 'center',
+      fixed: 'left',
+    },
+    {
+      title: '{system.views.userGroup.table.groupCode}',
+      field: 'groupCode',
+      fixed: 'left',
+      width: 160,
+    },
+    {
+      title: '{system.views.userGroup.table.groupName}',
+      field: 'groupName',
+      fixed: 'left',
+      width: 120,
+    },
+    {
+      ...tableUseYnClass(),
+      sortable: true,
+    },
+    {
+      title: '{common.table.remark}',
+      field: 'remark',
+      minWidth: 160,
+    },
+    {
+      title: '{common.table.seq}',
+      field: 'seq',
+      width: 100,
+      sortable: true,
+    },
+    {
+      title: '{common.table.createTime}',
+      field: 'createTime',
+      width: 165,
+      sortable: true,
+    },
+    {
+      title: '{common.table.createUser}',
+      field: 'createUserId',
+      width: 120,
+      formatter: ({ row }: any) => {
+        if (row.createUser) {
+          return row.createUser.fullName;
+        }
+        return '';
+      },
+    },
+    {
+      title: '{common.table.updateTime}',
+      field: 'updateTime',
+      width: 165,
+      sortable: true,
+    },
+    {
+      title: '{common.table.updateUser}',
+      field: 'updateUserId',
+      width: 120,
+      formatter: ({ row }: any) => {
+        if (row.updateUser) {
+          return row.updateUser.fullName;
+        }
+        return '';
+      },
+    },
+    {
+      title: '{common.table.operation}',
+      field: 'operation',
+      width: 120,
+      fixed: 'right',
+      slots: {
+        default: 'table-operation',
+      },
+    },
+  ];
+};
+
+export const getSearchSchemas = (t: Function): SmartSearchFormSchema[] => {
+  return [
+    {
+      label: t('system.views.userGroup.table.groupCode'),
+      field: 'groupCode',
+      component: 'Input',
+      searchSymbol: 'like',
+    },
+    {
+      label: t('system.views.userGroup.table.groupName'),
+      field: 'groupName',
+      component: 'Input',
+      searchSymbol: 'like',
+    },
+    {
+      label: t('system.views.userGroup.search.useYnTitle'),
+      field: 'useYn',
+      component: 'Select',
+      defaultValue: 1,
+      searchSymbol: '=',
+      componentProps: {
+        style: {
+          width: '100px',
+        },
+        options: [
+          {
+            label: t('common.form.use'),
+            value: 1,
+          },
+          {
+            label: t('common.form.noUse'),
+            value: 0,
+          },
+        ],
+      },
+    },
+  ];
+};
+
+export const getAddEditFormSchemas = (t: Function): FormSchema[] => {
+  return [
+    {
+      label: '',
+      field: 'groupId',
+      component: 'Input',
+      show: false,
+    },
+    {
+      label: t('system.views.userGroup.table.groupCode'),
+      field: 'groupCode',
+      component: 'Input',
+      required: true,
+    },
+    {
+      label: t('system.views.userGroup.table.groupName'),
+      field: 'groupName',
+      component: 'Input',
+      required: true,
+    },
+    {
+      label: t('common.table.useYn'),
+      field: 'useYn',
+      component: 'Switch',
+      defaultValue: true,
+    },
+    {
+      label: t('common.table.seq'),
+      field: 'seq',
+      component: 'InputNumber',
+      defaultValue: 1,
+      required: true,
+    },
+    {
+      label: t('common.table.remark'),
+      field: 'remark',
+      component: 'InputTextArea',
+    },
+  ];
+};

+ 134 - 0
src/modules/system/views/userGroup/UserGroupListView.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="full-height page-container">
+    <SmartTable @register="registerTable" :size="getTableSize">
+      <template #table-operation="{ row }">
+        <SmartVxeTableAction :actions="getTableActions(row)" />
+      </template>
+    </SmartTable>
+    <SmartUserSelectModal
+      @register="registerSetUserModal"
+      width="1500px"
+      showSelect
+      @selected="handleUserSelected"
+      :select-values="selectUserList"
+      :title="$t('system.views.userGroup.button.setUser')"
+    />
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import {
+    useSmartTable,
+    SmartTable,
+    SmartVxeTableAction,
+    ActionItem,
+  } from '@/components/SmartTable';
+  import { useSizeSetting } from '@/hooks/setting/UseSizeSetting';
+  import { useI18n } from '@/hooks/web/useI18n';
+  import { SmartUserSelectModal } from '@/components/Form';
+
+  import {
+    getAddEditFormSchemas,
+    getSearchSchemas,
+    getTableColumns,
+  } from './UserGroupListView.config';
+  import { listApi, deleteApi, batchSaveUpdateApi, getByIdApi } from './UserGroupListView.api';
+  import { SystemPermissions } from '@/modules/system/constants/SystemConstants';
+  import { useSetUser } from './hooks/useSetUser';
+
+  const { getTableSize } = useSizeSetting();
+  const { t } = useI18n();
+  const permissions = SystemPermissions.userGroup;
+  const { registerSetUserModal, handleShowSetUser, handleUserSelected, selectUserList } =
+    useSetUser(t);
+
+  const getTableActions = (row: any): ActionItem[] => {
+    return [
+      {
+        label: t('common.button.edit'),
+        preIcon: 'ant-design:edit-out-lined',
+        auth: permissions.update,
+        onClick: () => editByRowModal(row),
+      },
+      {
+        label: t('system.views.userGroup.button.setUser'),
+        preIcon: 'ant-design:user-add-outlined',
+        auth: permissions.setUser,
+        onClick: () => {
+          handleShowSetUser(row);
+        },
+      },
+    ];
+  };
+
+  const [registerTable, { editByRowModal }] = useSmartTable({
+    columns: getTableColumns(),
+    height: 'auto',
+    stripe: true,
+    highlightHoverRow: true,
+    pagerConfig: true,
+    useSearchForm: true,
+    border: true,
+    sortConfig: {
+      remote: true,
+      defaultSort: {
+        field: 'seq',
+        order: 'asc',
+      },
+    },
+    searchFormConfig: {
+      layout: 'inline',
+      schemas: getSearchSchemas(t),
+      autoSubmitOnEnter: true,
+      colon: true,
+      searchWithSymbol: true,
+      actionColOptions: {
+        span: undefined,
+      },
+    },
+    addEditConfig: {
+      formConfig: {
+        colon: true,
+        schemas: getAddEditFormSchemas(t),
+        labelCol: {
+          span: 5,
+        },
+        wrapperCol: {
+          span: 18,
+        },
+        baseColProps: {
+          span: 24,
+        },
+      },
+    },
+    proxyConfig: {
+      ajax: {
+        query: ({ ajaxParameter }) => listApi(ajaxParameter),
+        delete: ({ body: { removeRecords } }) => deleteApi(removeRecords),
+        getById: (data) => getByIdApi(data),
+        save: ({ body: { insertRecords, updateRecords } }) =>
+          batchSaveUpdateApi([...insertRecords, ...updateRecords]),
+      },
+    },
+    columnConfig: {
+      resizable: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      custom: true,
+      zoom: true,
+      buttons: [
+        {
+          code: 'ModalAdd',
+          auth: permissions.add,
+        },
+        {
+          code: 'delete',
+          auth: permissions.delete,
+        },
+      ],
+    },
+  });
+</script>
+
+<style scoped></style>

+ 313 - 0
src/modules/system/views/userGroup/UserGroupSupport.ts

@@ -0,0 +1,313 @@
+import { ref, onMounted, reactive, createVNode, unref } from 'vue';
+import type { Ref } from 'vue';
+import { useI18n } from 'vue-i18n';
+
+import { message, Modal } from 'ant-design-vue';
+import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
+import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+
+const defaultSearchModel = {
+  groupCode: '',
+  groupName: '',
+  useYn: 1,
+};
+
+/**
+ * 加载数据
+ */
+export const vueLoadData = () => {
+  const sortData = reactive({
+    sortName: 'seq',
+    sortOrder: 'asc',
+  });
+  // 数据
+  const data = ref([]);
+  // 数据加载状态
+  const loading = ref(false);
+  // 搜索表单
+  const searchModel = ref<any>(Object.assign({}, defaultSearchModel));
+  // 分页信息
+  const tablePage = reactive({
+    total: 0,
+    currentPage: 1,
+    pageSize: 500,
+  });
+  /**
+   * 加载数据函数
+   */
+  const loadData = async () => {
+    // 构建参数
+    const allParameter: any = {
+      ...sortData,
+      limit: tablePage.pageSize,
+      page: tablePage.currentPage,
+    };
+    // 自定义参数
+    const customParameter: any = {
+      QUERY_CREATE_UPDATE_USER: true,
+    };
+    Object.keys(searchModel.value).forEach((key) => {
+      const value = searchModel.value[key];
+      if (value !== null && value !== '') {
+        if (typeof value === 'string') {
+          if (value.trim() !== '') {
+            customParameter[key + '@like'] = value;
+          }
+        } else {
+          customParameter[key + '@='] = value;
+        }
+      }
+    });
+    allParameter.parameter = customParameter;
+    loading.value = true;
+    try {
+      const { rows, total } = await defHttp.post({
+        service: ApiServiceEnum.SMART_SYSTEM,
+        url: 'sys/userGroup/list',
+        data: allParameter,
+      });
+      tablePage.total = total;
+      data.value = rows;
+    } finally {
+      loading.value = false;
+    }
+  };
+  /**
+   * 排序变化时触发
+   */
+  const handleSortChange = ({ property, order }: any) => {
+    sortData.sortOrder = order;
+    sortData.sortName = property;
+    loadData();
+  };
+  /**
+   * 分页触发事件
+   */
+  const handlePageChange = ({ currentPage, pageSize }: any) => {
+    tablePage.currentPage = currentPage;
+    tablePage.pageSize = pageSize;
+    loadData();
+  };
+  /**
+   * 重置搜索表单
+   */
+  const resetSearch = () => {
+    searchModel.value = Object.assign({}, defaultSearchModel);
+  };
+  onMounted(loadData);
+  return {
+    data,
+    loading,
+    searchModel,
+    tablePage,
+    handlePageChange,
+    resetSearch,
+    loadData,
+    handleSortChange,
+  };
+};
+
+const defaultAddEditModel = {
+  useYn: true,
+  seq: 1,
+};
+
+/**
+ * 添加修改编码验证规则
+ */
+const addEditFormRules = (t: Function) => {
+  return {
+    groupCode: [
+      { required: true, trigger: 'blur', message: t('system.views.userGroup.validate.groupCode') },
+    ],
+    groupName: [
+      { required: true, trigger: 'blur', message: t('system.views.userGroup.validate.groupName') },
+    ],
+  };
+};
+
+export const vueAddUpdate = (loadData: any) => {
+  const t = useI18n().t;
+  // 保存加载状态
+  const saveLoading = ref(false);
+  // 查询加载状态
+  const getLoading = ref(false);
+  // 弹窗状态
+  const addEditModalVisible = ref(false);
+  // 是否是添加
+  const isAdd = ref(false);
+  // 添加修改表单
+  const addEditModel = ref<any>(Object.assign({}, defaultAddEditModel));
+  /**
+   * 添加操作
+   */
+  const handleShowAdd = () => {
+    addEditModel.value = Object.assign({}, defaultAddEditModel);
+    isAdd.value = true;
+    addEditModalVisible.value = true;
+  };
+  /**
+   * 编辑操作
+   * @param id
+   */
+  const handleShowEdit = async (id: number) => {
+    isAdd.value = false;
+    addEditModalVisible.value = true;
+    getLoading.value = true;
+    try {
+      addEditModel.value = await defHttp.post({
+        service: ApiServiceEnum.SMART_SYSTEM,
+        url: 'sys/userGroup/getById',
+        data: id,
+      });
+    } finally {
+      getLoading.value = false;
+    }
+  };
+  /**
+   * 保存操作
+   */
+  const handleSave = async () => {
+    saveLoading.value = true;
+    try {
+      await defHttp.post({
+        service: ApiServiceEnum.SMART_SYSTEM,
+        url: 'sys/userGroup/saveUpdate',
+        data: unref(addEditModel),
+      });
+      addEditModalVisible.value = false;
+      loadData();
+    } finally {
+      saveLoading.value = false;
+    }
+  };
+  return {
+    saveLoading,
+    getLoading,
+    addEditModalVisible,
+    isAdd,
+    addEditModel,
+    handleShowAdd,
+    handleShowEdit,
+    handleSave,
+    formRules: reactive(addEditFormRules(t)),
+  };
+};
+
+/**
+ * 删除操作
+ */
+export const vueDelete = (gridRef: Ref, loadData: any) => {
+  /**
+   * 删除操作
+   */
+  const handleDelete = () => {
+    // 获取选中行
+    const selectRows = gridRef.value.getCheckboxRecords();
+    if (selectRows.length === 0) {
+      message.error('请选择要删除的数据');
+      return false;
+    }
+    Modal.confirm({
+      title: '确认',
+      icon: createVNode(ExclamationCircleOutlined),
+      content: '确定要删除吗?',
+      onOk: async () => {
+        await defHttp.post({
+          service: ApiServiceEnum.SMART_SYSTEM,
+          url: 'sys/userGroup/batchDeleteById',
+          data: selectRows.map((item: any) => item.groupId),
+        });
+        loadData();
+      },
+    });
+  };
+  return {
+    handleDelete,
+  };
+};
+
+/**
+ * 设置用户
+ */
+export const vueSetUser = () => {
+  const loadUserLoading = ref(false);
+  const setUserLoading = ref(false);
+  const setUserModalVisible = ref(false);
+  // 所有用户数据
+  const allUserData = ref<Array<any>>([]);
+  // 选中的key
+  const targetKeys = ref<Array<string>>([]);
+  let selectRow: any = null;
+  /**
+   * 设置用户操作
+   */
+  const handleShowSetUser = async (row: any) => {
+    selectRow = row;
+    setUserModalVisible.value = true;
+    loadUserLoading.value = true;
+    try {
+      loadUserLoading.value = true;
+      // 加载所有用户数据
+      if (allUserData.value.length === 0) {
+        const result: Array<any> = await defHttp.post({
+          service: ApiServiceEnum.SMART_SYSTEM,
+          url: 'sys/user/list',
+          data: {
+            sortName: 'seq',
+            parameter: {
+              'useYn@=': true,
+            },
+          },
+        });
+        allUserData.value = result.map(({ userId, fullName, username }: any) => {
+          return {
+            key: userId + '',
+            title: `${fullName}[${username}]`,
+          };
+        });
+      }
+      // 加载用户关联数据
+      const result: Array<number> = await defHttp.post({
+        service: ApiServiceEnum.SMART_SYSTEM,
+        url: 'sys/userGroup/listUserIdById',
+        data: selectRow.groupId,
+      });
+      targetKeys.value = result.map((item) => item + '');
+    } finally {
+      loadUserLoading.value = false;
+    }
+  };
+  const handleTransChange = (targetKeyList: Array<string>) => {
+    targetKeys.value = targetKeyList;
+  };
+  /**
+   * 设置用户操作
+   */
+  const handleSetUser = async () => {
+    try {
+      setUserLoading.value = true;
+      await defHttp.post({
+        service: ApiServiceEnum.SMART_SYSTEM,
+        url: 'sys/userGroup/saveUserGroupByGroupId',
+        data: {
+          groupId: selectRow.groupId,
+          userIdList: targetKeys.value,
+        },
+      });
+      setUserModalVisible.value = false;
+    } finally {
+      setUserLoading.value = false;
+    }
+  };
+  return {
+    loadUserLoading,
+    setUserLoading,
+    handleShowSetUser,
+    setUserModalVisible,
+    handleSetUser,
+    allUserData,
+    targetKeys,
+    handleTransChange,
+  };
+};

+ 53 - 0
src/modules/system/views/userGroup/hooks/useSetUser.ts

@@ -0,0 +1,53 @@
+import { useModal } from '@/components/Modal';
+import { ref, unref } from 'vue';
+import { message } from 'ant-design-vue';
+
+import { listUserIdByGroupIdApi, setUserApi } from '../UserGroupListView.api';
+
+/**
+ * 设置用户组人员信息
+ */
+export const useSetUser = (t: Function) => {
+  const [registerSetUserModal, { openModal, setModalProps, closeModal }] = useModal();
+  const currentUserGroup = ref<Recordable | null>(null);
+  const selectUserList = ref<number[]>([]);
+
+  /**
+   * 显示设置用户弹窗
+   * @param userGroup
+   */
+  const handleShowSetUser = async (userGroup: Recordable) => {
+    currentUserGroup.value = userGroup;
+    openModal(true);
+    try {
+      setModalProps({ loading: true });
+      // 获取已关联的用户信息
+      selectUserList.value = await listUserIdByGroupIdApi(userGroup.groupId);
+    } finally {
+      setModalProps({ loading: false });
+    }
+  };
+
+  /**
+   * 设置用户
+   * @param userIdList
+   */
+  const handleUserSelected = async (userIdList: number[]) => {
+    selectUserList.value = userIdList;
+    try {
+      setModalProps({ confirmLoading: true });
+      await setUserApi(unref(currentUserGroup)!.groupId, userIdList);
+      message.success(t('common.message.OperationSucceeded'));
+      closeModal();
+    } finally {
+      setModalProps({ confirmLoading: false });
+    }
+  };
+
+  return {
+    registerSetUserModal,
+    handleShowSetUser,
+    handleUserSelected,
+    selectUserList,
+  };
+};

+ 24 - 0
src/modules/system/views/userGroup/lang/en_US.ts

@@ -0,0 +1,24 @@
+export default {
+  system: {
+    views: {
+      userGroup: {
+        title: 'User group',
+        table: {
+          groupCode: 'Group code',
+          groupName: 'Group name',
+          remark: 'Remark',
+        },
+        button: {
+          setUser: 'Set user',
+        },
+        search: {
+          useYnTitle: 'Use yn',
+        },
+        validate: {
+          groupCode: 'Please enter group code',
+          groupName: 'Please enter group name',
+        },
+      },
+    },
+  },
+};

+ 24 - 0
src/modules/system/views/userGroup/lang/zh_CN.ts

@@ -0,0 +1,24 @@
+export default {
+  system: {
+    views: {
+      userGroup: {
+        title: '用户组',
+        table: {
+          groupCode: '用户组编码',
+          groupName: '用户组名称',
+          remark: '备注',
+        },
+        button: {
+          setUser: '设置用户',
+        },
+        search: {
+          useYnTitle: '启用标志',
+        },
+        validate: {
+          groupCode: '请输入用户组编码',
+          groupName: '请输入用户组名称',
+        },
+      },
+    },
+  },
+};