feat: 接入字典管理页面

This commit is contained in:
sunlei 2026-06-03 22:51:18 +08:00
parent 768bde6e14
commit 89a08aa285
9 changed files with 541 additions and 0 deletions

View File

@ -51,6 +51,10 @@ const SUPPORTED_ADMIN_MENU_NAMES = new Set([
'SystemDeptCreate', 'SystemDeptCreate',
'SystemDeptDelete', 'SystemDeptDelete',
'SystemDeptEdit', 'SystemDeptEdit',
'SystemDict',
'SystemDictCreate',
'SystemDictDelete',
'SystemDictEdit',
'SystemKtTableDemo', 'SystemKtTableDemo',
'SystemKtTableDemoCreate', 'SystemKtTableDemoCreate',
'SystemKtTableDemoDelete', 'SystemKtTableDemoDelete',

View File

@ -0,0 +1,77 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace SystemDictApi {
export interface DictItem {
[key: string]: any;
childrenCode?: null | string;
createTime?: string;
dictCode: string;
id: string;
label: string;
sort: number;
status: 0 | 1;
updateTime?: string;
value: string;
}
export type DictInput = Omit<DictItem, 'createTime' | 'id' | 'updateTime'>;
export interface DictCodeOption {
label: string;
value: string;
}
export interface PageResult<T> {
items: T[];
total: number;
}
}
async function getDictList(params: Recordable<any>) {
return requestClient.get<SystemDictApi.PageResult<SystemDictApi.DictItem>>(
'/dict/list',
{ params },
);
}
async function getDictCodeOptions() {
return requestClient.get<SystemDictApi.DictCodeOption[]>('/dict/codes');
}
async function createDict(data: SystemDictApi.DictInput) {
return requestClient.post<string>('/dict/save', data);
}
async function updateDict(id: string, data: Partial<SystemDictApi.DictInput>) {
return requestClient.post('/dict/update', {
...data,
id,
});
}
async function deleteDict(id: string) {
return requestClient.delete(`/dict/${id}`);
}
async function toggleDictStatus(
id: string,
status: SystemDictApi.DictItem['status'],
) {
return requestClient.post('/dict/toggle', undefined, {
params: {
id,
status,
},
});
}
export {
createDict,
deleteDict,
getDictCodeOptions,
getDictList,
toggleDictStatus,
updateDict,
};

View File

@ -1,4 +1,5 @@
export * from './dept'; export * from './dept';
export * from './dict';
export * from './menu'; export * from './menu';
export * from './role'; export * from './role';
export * from './user'; export * from './user';

View File

@ -27,6 +27,22 @@
"operation": "Operation", "operation": "Operation",
"parentDept": "Parent Department" "parentDept": "Parent Department"
}, },
"dict": {
"childrenCode": "Children Code",
"deleteConfirm": "Confirm deleting \"{0} / {1}\"?",
"dictCode": "Dict Code",
"disableSuccess": "Dictionary item disabled",
"enableSuccess": "Dictionary item enabled",
"label": "Label",
"list": "Dictionary List",
"name": "Dictionary",
"sort": "Sort",
"status": "Status",
"title": "Dictionary Management",
"toggle": "Toggle",
"updateTime": "Update Time",
"value": "Value"
},
"menu": { "menu": {
"title": "Menu Management", "title": "Menu Management",
"parent": "Parent Menu", "parent": "Parent Menu",

View File

@ -28,6 +28,22 @@
"status": "状态", "status": "状态",
"title": "部门管理" "title": "部门管理"
}, },
"dict": {
"childrenCode": "子级编码",
"deleteConfirm": "确认删除「{0} / {1}」吗?",
"dictCode": "字典编码",
"disableSuccess": "字典项已停用",
"enableSuccess": "字典项已启用",
"label": "字典标签",
"list": "字典列表",
"name": "字典",
"sort": "排序",
"status": "状态",
"title": "字典管理",
"toggle": "启停",
"updateTime": "更新时间",
"value": "字典值"
},
"menu": { "menu": {
"list": "菜单列表", "list": "菜单列表",
"activeIcon": "激活图标", "activeIcon": "激活图标",

View File

@ -39,6 +39,15 @@ const routes: RouteRecordRaw[] = [
}, },
component: () => import('#/views/system/menu/list.vue'), component: () => import('#/views/system/menu/list.vue'),
}, },
{
path: '/system/dict',
name: 'SystemDict',
meta: {
icon: 'carbon:data-structured',
title: $t('system.dict.title'),
},
component: () => import('#/views/system/dict/list.vue'),
},
{ {
path: '/system/dept', path: '/system/dept',
name: 'SystemDept', name: 'SystemDept',

View File

@ -0,0 +1,127 @@
import type { VbenFormSchema } from '#/adapter/form';
import { z } from '#/adapter/form';
import { $t } from '#/locales';
export function getStatusOptions() {
return [
{ color: 'success', label: $t('common.enabled'), value: 1 },
{ color: 'default', label: $t('common.disabled'), value: 0 },
];
}
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
componentProps: {
placeholder: '如 COMPONENT_TYPE',
},
fieldName: 'dictCode',
label: $t('system.dict.dictCode'),
rules: z
.string()
.min(1, $t('ui.formRules.required', [$t('system.dict.dictCode')]))
.max(
120,
$t('ui.formRules.maxLength', [$t('system.dict.dictCode'), 120]),
),
},
{
component: 'Input',
fieldName: 'label',
label: $t('system.dict.label'),
rules: z
.string()
.min(1, $t('ui.formRules.required', [$t('system.dict.label')]))
.max(120, $t('ui.formRules.maxLength', [$t('system.dict.label'), 120])),
},
{
component: 'Input',
fieldName: 'value',
label: $t('system.dict.value'),
rules: z
.string()
.min(1, $t('ui.formRules.required', [$t('system.dict.value')]))
.max(120, $t('ui.formRules.maxLength', [$t('system.dict.value'), 120])),
},
{
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '如 CHART',
},
fieldName: 'childrenCode',
label: $t('system.dict.childrenCode'),
},
{
component: 'InputNumber',
componentProps: {
class: 'w-full',
min: 0,
precision: 0,
},
defaultValue: 0,
fieldName: 'sort',
label: $t('system.dict.sort'),
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: getStatusOptions(),
optionType: 'button',
},
defaultValue: 1,
fieldName: 'status',
label: $t('system.dict.status'),
},
];
}
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '如 COMPONENT_TYPE',
},
fieldName: 'dictCode',
label: $t('system.dict.dictCode'),
},
{
component: 'Input',
componentProps: {
allowClear: true,
},
fieldName: 'label',
label: $t('system.dict.label'),
},
{
component: 'Input',
componentProps: {
allowClear: true,
},
fieldName: 'value',
label: $t('system.dict.value'),
},
{
component: 'Input',
componentProps: {
allowClear: true,
},
fieldName: 'childrenCode',
label: $t('system.dict.childrenCode'),
},
{
component: 'Select',
componentProps: {
allowClear: true,
options: getStatusOptions(),
},
fieldName: 'status',
label: $t('system.dict.status'),
},
];
}

View File

@ -0,0 +1,203 @@
<script lang="ts" setup>
import type { TableColumnType } from 'antdv-next';
import type { SystemDictApi } from '#/api/system/dict';
import type {
KtTableApi,
KtTableButton,
KtTableContext,
KtTableRowAction,
} from '#/components/ktTable';
import { h } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { message, Tag } from 'antdv-next';
import { deleteDict, getDictList, toggleDictStatus } from '#/api/system/dict';
import { KtTable, useKtTable } from '#/components/ktTable';
import { clearDictCache } from '#/hooks/useDict';
import { $t } from '#/locales';
import { getStatusOptions, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
const statusOptions = getStatusOptions();
const columns: Array<TableColumnType<SystemDictApi.DictItem>> = [
{
dataIndex: 'dictCode',
fixed: 'left',
key: 'dictCode',
title: $t('system.dict.dictCode'),
width: 220,
},
{
dataIndex: 'label',
key: 'label',
title: $t('system.dict.label'),
width: 180,
},
{
dataIndex: 'value',
key: 'value',
title: $t('system.dict.value'),
width: 180,
},
{
dataIndex: 'childrenCode',
key: 'childrenCode',
title: $t('system.dict.childrenCode'),
width: 180,
},
{
align: 'center',
dataIndex: 'sort',
key: 'sort',
title: $t('system.dict.sort'),
width: 100,
},
{
align: 'center',
dataIndex: 'status',
key: 'status',
title: $t('system.dict.status'),
width: 100,
},
{
dataIndex: 'updateTime',
key: 'updateTime',
title: $t('system.dict.updateTime'),
width: 200,
},
];
const api: KtTableApi<SystemDictApi.DictItem> = {
list: async (params) => await getDictList(params),
};
const buttons: Array<KtTableButton<SystemDictApi.DictItem>> = [
{
icon: () => h(Plus, { class: 'kt-table__button-icon' }),
key: 'create',
label: $t('ui.actionTitle.create', [$t('system.dict.name')]),
onClick: onCreate,
permissionCodes: ['System:Dict:Create'],
type: 'primary',
},
];
const rowActions: Array<KtTableRowAction<SystemDictApi.DictItem>> = [
{
key: 'toggle',
label: $t('system.dict.toggle'),
onClick: onToggle,
permissionCodes: ['System:Dict:Edit'],
},
{
key: 'edit',
label: $t('common.edit'),
onClick: onEdit,
permissionCodes: ['System:Dict:Edit'],
},
{
confirm: (row) =>
$t('system.dict.deleteConfirm', [row.dictCode, row.label]),
danger: true,
key: 'delete',
label: $t('common.delete'),
onClick: onDelete,
permissionCodes: ['System:Dict:Delete'],
},
];
const [registerTable, tableApi] = useKtTable<SystemDictApi.DictItem>({
api,
buttons,
columns,
formOptions: {
schema: useGridFormSchema(),
},
rowActions,
tableTitle: $t('system.dict.list'),
});
function getStatusOption(status: SystemDictApi.DictItem['status']) {
return statusOptions.find((item) => item.value === status);
}
function onCreate() {
formModalApi.setData(undefined).open();
}
function onEdit(row: SystemDictApi.DictItem) {
formModalApi.setData(row).open();
}
async function onToggle(
row: SystemDictApi.DictItem,
context: KtTableContext<SystemDictApi.DictItem>,
) {
const nextStatus = row.status === 1 ? 0 : 1;
await toggleDictStatus(row.id, nextStatus);
clearDictCache(row.dictCode);
message.success(
nextStatus === 1
? $t('system.dict.enableSuccess')
: $t('system.dict.disableSuccess'),
);
await context.reload();
}
async function onDelete(
row: SystemDictApi.DictItem,
context?: KtTableContext<SystemDictApi.DictItem>,
) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.label]),
duration: 0,
key: 'action_process_msg',
});
try {
await deleteDict(row.id);
clearDictCache(row.dictCode);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.label]),
key: 'action_process_msg',
});
await (context || tableApi).reload();
} catch {
hideLoading();
}
}
function onRefresh() {
tableApi.reload();
}
</script>
<template>
<Page auto-content-height>
<FormModal @success="onRefresh" />
<KtTable @register="registerTable">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'childrenCode'">
{{ record.childrenCode || '-' }}
</template>
<template v-else-if="column.key === 'status'">
<Tag :color="getStatusOption(record.status)?.color">
{{ getStatusOption(record.status)?.label || record.status }}
</Tag>
</template>
</template>
</KtTable>
</Page>
</template>

View File

@ -0,0 +1,88 @@
<script lang="ts" setup>
import type { SystemDictApi } from '#/api/system/dict';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button } from 'antdv-next';
import { useVbenForm } from '#/adapter/form';
import { createDict, updateDict } from '#/api/system/dict';
import { clearDictCache } from '#/hooks/useDict';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits<{
success: [];
}>();
const formData = ref<SystemDictApi.DictItem>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', [$t('system.dict.name')])
: $t('ui.actionTitle.create', [$t('system.dict.name')]);
});
const [Form, formApi] = useVbenForm({
layout: 'vertical',
schema: useFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) return;
modalApi.lock();
const data = (await formApi.getValues()) as SystemDictApi.DictInput;
try {
const currentId = formData.value?.id;
await (currentId ? updateDict(currentId, data) : createDict(data));
if (formData.value?.dictCode) clearDictCache(formData.value.dictCode);
if (data.dictCode) clearDictCache(data.dictCode);
modalApi.close();
emit('success');
} finally {
modalApi.lock(false);
}
},
onOpenChange(isOpen) {
if (!isOpen) return;
const data = modalApi.getData<SystemDictApi.DictItem>();
formData.value = data || undefined;
resetForm();
},
});
function getFormValues() {
return {
childrenCode: formData.value?.childrenCode || undefined,
dictCode: formData.value?.dictCode || '',
label: formData.value?.label || '',
sort: formData.value?.sort ?? 0,
status: formData.value?.status ?? 1,
value: formData.value?.value || '',
};
}
function resetForm() {
formApi.resetForm();
formApi.setValues(getFormValues());
}
</script>
<template>
<Modal :title="getTitle">
<Form class="mx-4" />
<template #prepend-footer>
<div class="flex-auto">
<Button type="primary" danger @click="resetForm">
{{ $t('common.reset') }}
</Button>
</div>
</template>
</Modal>
</template>