Browse Source

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

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

3
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
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'];
}

2
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();
}
},

3
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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: '请输入用户组名称',
},
},
},
},
};
Loading…
Cancel
Save