From 3c8455e8ac0263e86ede1f7a60dc6f3788a4c5f5 Mon Sep 17 00:00:00 2001 From: sunlei Date: Thu, 4 Jun 2026 03:13:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E4=BC=98=E5=8C=96=E5=AD=97?= =?UTF-8?q?=E5=85=B8=E7=AE=A1=E7=90=86=E4=B8=8EQQBot=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-antdv-next/src/api/qqbot/index.ts | 13 + apps/web-antdv-next/src/api/system/dict.ts | 16 ++ .../src/components/ktTable/KtTable.tsx | 54 +++- .../components/ktTable/config/ktTableProps.ts | 18 ++ .../src/components/ktTable/styles/table.scss | 16 ++ .../src/components/ktTable/types.ts | 17 ++ .../src/views/qqbot/account/config.tsx | 4 +- .../src/views/qqbot/account/list.tsx | 228 +++++++++++++++-- .../src/views/system/dict/data.ts | 14 + .../src/views/system/dict/list.vue | 242 +++++++++++++++--- .../src/views/system/dict/modules/form.vue | 4 +- 11 files changed, 550 insertions(+), 76 deletions(-) diff --git a/apps/web-antdv-next/src/api/qqbot/index.ts b/apps/web-antdv-next/src/api/qqbot/index.ts index 85f86a6..5c9ae36 100644 --- a/apps/web-antdv-next/src/api/qqbot/index.ts +++ b/apps/web-antdv-next/src/api/qqbot/index.ts @@ -43,10 +43,23 @@ export namespace QqbotApi { lastError?: string; lastHeartbeatAt?: string; name: string; + napcat?: AccountNapcatRuntime | null; remark?: string; selfId: string; } + export interface AccountNapcatRuntime { + bindStatus?: 'bound' | 'disabled' | 'pending'; + containerId?: string; + containerName?: string; + containerStatus?: 'creating' | 'error' | 'running' | 'stopped'; + lastCheckedAt?: string; + lastError?: string; + lastLoginAt?: string; + lastStartedAt?: string; + webuiPort?: null | number; + } + export interface AccountBody { accessToken?: string; connectionMode?: 'reverse-ws'; diff --git a/apps/web-antdv-next/src/api/system/dict.ts b/apps/web-antdv-next/src/api/system/dict.ts index e11f9a7..f8a5605 100644 --- a/apps/web-antdv-next/src/api/system/dict.ts +++ b/apps/web-antdv-next/src/api/system/dict.ts @@ -21,6 +21,14 @@ export namespace SystemDictApi { treeKey: string; } + export interface DictGroup { + dictCode: string; + id: string; + itemCount: number; + label: string; + value: string; + } + export type DictInput = Omit; export interface DictCodeOption { @@ -47,6 +55,13 @@ async function getDictTree(params: Recordable) { }); } +async function getDictGroups(params: Recordable) { + return requestClient.get>( + '/dict/groups', + { params }, + ); +} + async function getDictCodeOptions() { return requestClient.get('/dict/codes'); } @@ -82,6 +97,7 @@ export { createDict, deleteDict, getDictCodeOptions, + getDictGroups, getDictList, getDictTree, toggleDictStatus, diff --git a/apps/web-antdv-next/src/components/ktTable/KtTable.tsx b/apps/web-antdv-next/src/components/ktTable/KtTable.tsx index 1efc542..316d038 100644 --- a/apps/web-antdv-next/src/components/ktTable/KtTable.tsx +++ b/apps/web-antdv-next/src/components/ktTable/KtTable.tsx @@ -469,27 +469,61 @@ export default defineComponent({ } /** - * 为可调整行高的行追加 class 和高度 CSS 变量。 + * 为表格行追加交互、选中和可调整行高属性。 * * @param record 当前行数据。 */ function resolveRowProps(record: KtTableRecord) { - if (!props.rowResizable) return {}; + const recordKey = String(resolveRecordKey(record)); + const classNames = [ + props.rowResizable ? 'kt-table__row--resizable' : '', + props.onRowClick ? 'kt-table__row--clickable' : '', + props.activeRowKey !== undefined && + String(props.activeRowKey) === recordKey + ? 'kt-table__row--active' + : '', + resolveCustomRowClassName(record), + ].filter(Boolean); + const height = rowHeights[recordKey]; + const rowProps: KtTableRecord = {}; - const height = rowHeights[String(resolveRecordKey(record))]; + if (classNames.length > 0) { + rowProps.class = classNames.join(' '); + } - return { - class: 'kt-table__row--resizable', - onMousedown: (event: MouseEvent) => { + if (props.rowResizable) { + rowProps.onMousedown = (event: MouseEvent) => { handleRowResizeMouseDown(event, record); - }, - style: height + }; + rowProps.style = height ? { '--kt-table-row-height': `${height}px`, height: `${height}px`, } - : undefined, - }; + : undefined; + } + + if (props.onRowClick) { + rowProps.onClick = () => { + props.onRowClick?.(record, context); + }; + } + + return rowProps; + } + + /** + * 解析业务侧传入的行 class。 + * + * @param record 当前行数据。 + */ + function resolveCustomRowClassName(record: KtTableRecord) { + if (!props.rowClassName) return ''; + if (typeof props.rowClassName === 'function') { + return props.rowClassName(record, context) || ''; + } + + return props.rowClassName; } /** diff --git a/apps/web-antdv-next/src/components/ktTable/config/ktTableProps.ts b/apps/web-antdv-next/src/components/ktTable/config/ktTableProps.ts index 7e354f9..71cd018 100644 --- a/apps/web-antdv-next/src/components/ktTable/config/ktTableProps.ts +++ b/apps/web-antdv-next/src/components/ktTable/config/ktTableProps.ts @@ -35,6 +35,7 @@ export const DEFAULT_TABLE_SETTING: Required = { export const KT_TABLE_PROP_KEYS = [ 'afterFetch', 'api', + 'activeRowKey', 'beforeFetch', 'buttons', 'columns', @@ -43,10 +44,12 @@ export const KT_TABLE_PROP_KEYS = [ 'hooks', 'immediate', 'modules', + 'onRowClick', 'pageSize', 'pageSizeOptions', 'rowActions', 'rowActionVisibleCount', + 'rowClassName', 'rowResizeMaxHeight', 'rowResizeMinHeight', 'rowResizable', @@ -74,6 +77,7 @@ export function createDefaultTableProps(): KtTableResolvedProps< return { afterFetch: undefined, api: undefined, + activeRowKey: undefined, beforeFetch: undefined, buttons: [], columns: [], @@ -82,10 +86,12 @@ export function createDefaultTableProps(): KtTableResolvedProps< hooks: [], immediate: true, modules: [], + onRowClick: undefined, pageSize: KT_TABLE_DEFAULT_PAGE_SIZE, pageSizeOptions: KT_TABLE_DEFAULT_PAGE_SIZE_OPTIONS, rowActions: [], rowActionVisibleCount: KT_TABLE_ROW_ACTION_VISIBLE_COUNT, + rowClassName: undefined, rowResizeMaxHeight: KT_TABLE_DEFAULT_ROW_RESIZE_MAX_HEIGHT, rowResizeMinHeight: KT_TABLE_DEFAULT_ROW_RESIZE_MIN_HEIGHT, rowResizable: false, @@ -109,6 +115,10 @@ export const ktTableProps = { default: undefined, type: Function as PropType, }, + activeRowKey: { + default: undefined, + type: [Number, String] as PropType, + }, api: { default: undefined, type: Object as PropType, @@ -145,6 +155,10 @@ export const ktTableProps = { default: () => [], type: Array as PropType, }, + onRowClick: { + default: undefined, + type: Function as PropType, + }, pageSize: { default: KT_TABLE_DEFAULT_PAGE_SIZE, type: Number, @@ -161,6 +175,10 @@ export const ktTableProps = { default: KT_TABLE_ROW_ACTION_VISIBLE_COUNT, type: Number, }, + rowClassName: { + default: undefined, + type: [Function, String] as PropType, + }, rowResizeMaxHeight: { default: KT_TABLE_DEFAULT_ROW_RESIZE_MAX_HEIGHT, type: Number, diff --git a/apps/web-antdv-next/src/components/ktTable/styles/table.scss b/apps/web-antdv-next/src/components/ktTable/styles/table.scss index 1249145..3330544 100644 --- a/apps/web-antdv-next/src/components/ktTable/styles/table.scss +++ b/apps/web-antdv-next/src/components/ktTable/styles/table.scss @@ -97,6 +97,22 @@ background: hsl(var(--accent)) !important; } + .ant-table-tbody > tr#{kt.$block}__row--clickable { + cursor: pointer; + } + + .ant-table-tbody > tr#{kt.$block}__row--active > td, + .ant-table-tbody > tr#{kt.$block}__row--active > td.ant-table-cell-fix-left, + .ant-table-tbody + > tr#{kt.$block}__row--active + > td.ant-table-cell-fix-right, + .ant-table-tbody + > tr#{kt.$block}__row--active + > td.ant-table-cell-fix-start, + .ant-table-tbody > tr#{kt.$block}__row--active > td.ant-table-cell-fix-end { + background: hsl(var(--accent)) !important; + } + .ant-table-summary .ant-table-cell-fix-left, .ant-table-summary .ant-table-cell-fix-right, .ant-table-summary .ant-table-cell-fix-start, diff --git a/apps/web-antdv-next/src/components/ktTable/types.ts b/apps/web-antdv-next/src/components/ktTable/types.ts index 9ee0507..49625c6 100644 --- a/apps/web-antdv-next/src/components/ktTable/types.ts +++ b/apps/web-antdv-next/src/components/ktTable/types.ts @@ -9,6 +9,7 @@ import type { } from '@vben/common-ui'; export type KtTableRecord = Record; +export type KtTableRowKey = number | string; export type KtTableSize = 'large' | 'middle' | 'small'; @@ -217,6 +218,7 @@ export interface KtTableProps< result: KtTablePageResult | Row[], context: KtTableContext, ) => KtTablePageResult | Promise | Row[]> | Row[]; + activeRowKey?: KtTableRowKey; beforeFetch?: ( params: KtTableRecord & SearchValues, context: KtTableContext, @@ -231,10 +233,17 @@ export interface KtTableProps< hooks?: Array>; immediate?: boolean; modules?: Array>; + onRowClick?: ( + row: Row, + context: KtTableContext, + ) => Promise | void; pageSize?: number; pageSizeOptions?: string[]; rowActions?: Array>; rowActionVisibleCount?: number; + rowClassName?: + | ((row: Row, context: KtTableContext) => string) + | string; rowResizeMaxHeight?: number; rowResizeMinHeight?: number; rowResizable?: boolean; @@ -256,15 +265,23 @@ export type KtTableResolvedProps< Row extends KtTableRecord = KtTableRecord, SearchValues extends KtTableRecord = KtTableRecord, > = KtTableProps & { + activeRowKey?: KtTableRowKey; buttons: Array>; columns: Array>; hooks: Array>; immediate: boolean; modules: Array>; + onRowClick?: ( + row: Row, + context: KtTableContext, + ) => Promise | void; pageSize: number; pageSizeOptions: string[]; rowActions: Array>; rowActionVisibleCount: number; + rowClassName?: + | ((row: Row, context: KtTableContext) => string) + | string; rowKey: ((row: Row) => string) | keyof Row | string; rowResizable: boolean; rowResizeMaxHeight: number; diff --git a/apps/web-antdv-next/src/views/qqbot/account/config.tsx b/apps/web-antdv-next/src/views/qqbot/account/config.tsx index c62572b..9297936 100644 --- a/apps/web-antdv-next/src/views/qqbot/account/config.tsx +++ b/apps/web-antdv-next/src/views/qqbot/account/config.tsx @@ -104,7 +104,9 @@ export default defineComponent({ : 'default' } > - {account.value.connectStatus === 'online' ? '在线' : '离线'} + {account.value.connectStatus === 'online' + ? 'OneBot 在线' + : 'OneBot 离线'} ) : null} diff --git a/apps/web-antdv-next/src/views/qqbot/account/list.tsx b/apps/web-antdv-next/src/views/qqbot/account/list.tsx index 189b75e..c0e83ac 100644 --- a/apps/web-antdv-next/src/views/qqbot/account/list.tsx +++ b/apps/web-antdv-next/src/views/qqbot/account/list.tsx @@ -34,6 +34,7 @@ import { KtTable, useKtTable } from '#/components/ktTable'; const AKtTable = KtTable as any; const AButton = Button as any; const ATypographyLink = Typography.Link as any; +const ATypographyText = Typography.Text as any; export default defineComponent({ name: 'QqBotAccountList', @@ -41,6 +42,7 @@ export default defineComponent({ const editingId = ref(); const router = useRouter(); const scanLoading = ref(false); + const scanQrcodeImageFailed = ref(false); const scanQrcodeText = ref(''); const scanState = reactive<{ containerId?: string; @@ -61,6 +63,14 @@ export default defineComponent({ margin: 2, scale: 8, }); + const scanQrcodeImageSrc = computed(() => { + const qrcode = scanQrcodeText.value.trim(); + if (!qrcode) return ''; + if (!scanQrcodeImageFailed.value && isQrcodeImageCandidate(qrcode)) { + return normalizeQrcodeImageSrc(qrcode); + } + return scanQrcode.value; + }); let scanTimer: number | undefined; const [AccountForm, accountFormApi] = useVbenForm({ @@ -112,31 +122,31 @@ export default defineComponent({ }); const columns: Array> = [ - { dataIndex: 'selfId', key: 'selfId', title: 'Self ID', width: 160 }, - { dataIndex: 'name', key: 'name', title: '账号名称', width: 180 }, + { dataIndex: 'selfId', key: 'selfId', title: 'Self ID', width: 140 }, + { dataIndex: 'name', key: 'name', title: '账号名称', width: 150 }, { dataIndex: 'connectStatus', - key: 'connectStatus', - title: '连接状态', - width: 120, + key: 'accountOnlineStatus', + title: '账号在线', + width: 130, }, { - dataIndex: 'clientRole', - key: 'clientRole', - title: '连接角色', - width: 120, + dataIndex: 'napcat', + key: 'napcatRuntime', + title: 'NapCat 容器', + width: 220, }, { dataIndex: 'lastHeartbeatAt', key: 'lastHeartbeatAt', - title: '最后心跳', + title: '最近活动', width: 190, }, { dataIndex: 'lastError', - key: 'lastError', - title: '错误信息', - width: 260, + key: 'runtimeSummary', + title: '运行说明', + width: 220, }, ]; @@ -230,12 +240,12 @@ export default defineComponent({ componentProps: { allowClear: true, options: [ - { label: '在线', value: 'online' }, - { label: '离线', value: 'offline' }, + { label: 'OneBot 在线', value: 'online' }, + { label: 'OneBot 离线', value: 'offline' }, ], }, fieldName: 'connectStatus', - label: '连接状态', + label: '账号在线', }, ], }, @@ -323,7 +333,11 @@ export default defineComponent({ scanState.sessionId = result.sessionId; scanState.status = result.status; scanState.webuiPort = result.webuiPort; - scanQrcodeText.value = result.qrcode || ''; + const nextQrcode = result.qrcode || ''; + if (nextQrcode !== scanQrcodeText.value) { + scanQrcodeImageFailed.value = false; + } + scanQrcodeText.value = nextQrcode; if (result.status === 'pending') { startScanPolling(); @@ -389,6 +403,7 @@ export default defineComponent({ status: 'idle', webuiPort: undefined, }); + scanQrcodeImageFailed.value = false; scanQrcodeText.value = ''; } @@ -431,6 +446,157 @@ export default defineComponent({ return '扫码登录请求失败'; } + function isQrcodeImageCandidate(value: string) { + return ( + /^data:image\//i.test(value) || + /^https?:\/\//i.test(value) || + isRawBase64Image(value) + ); + } + + function normalizeQrcodeImageSrc(value: string) { + if (isRawBase64Image(value)) { + return `data:image/png;base64,${value}`; + } + return value; + } + + function isRawBase64Image(value: string) { + const normalized = value.trim(); + return ( + normalized.startsWith('iVBORw0KGgo') || + normalized.startsWith('/9j/') || + normalized.startsWith('R0lGOD') + ); + } + + const renderAccountOnlineStatus = (row: QqbotApi.Account) => { + if (!row.enabled) { + return 已停用; + } + return ( + + {row.connectStatus === 'online' ? 'OneBot 在线' : 'OneBot 离线'} + + ); + }; + + const renderNapcatRuntime = (row: QqbotApi.Account) => { + const napcat = row.napcat; + const meta = getNapcatStatusMeta(napcat); + return ( + + {meta.label} + {napcat?.containerName ? ( + + {napcat.containerName} + {napcat.webuiPort ? `:${napcat.webuiPort}` : ''} + + ) : null} + + ); + }; + + const renderRecentActivity = (row: QqbotApi.Account) => { + const active = getRecentActivity(row); + return ( + + {active.label} + + {active.time || '暂无记录'} + + + ); + }; + + const renderRuntimeSummary = (row: QqbotApi.Account) => { + const summary = getRuntimeSummary(row); + return ( + + {summary.text} + + ); + }; + + function getNapcatStatusMeta( + napcat?: null | QqbotApi.AccountNapcatRuntime, + ) { + if (!napcat) { + return { color: 'default', label: '未绑定专属容器' }; + } + if (!napcat.containerStatus) { + return { color: 'warning', label: '容器记录缺失' }; + } + const statusMap: Record< + NonNullable, + { color: string; label: string } + > = { + creating: { color: 'processing', label: '容器创建中' }, + error: { color: 'error', label: '容器异常' }, + running: { color: 'success', label: '容器运行中' }, + stopped: { color: 'default', label: '容器已停止' }, + }; + return statusMap[napcat.containerStatus]; + } + + function getRecentActivity(row: QqbotApi.Account) { + const candidates = [ + { label: '最近心跳', value: row.lastHeartbeatAt }, + { label: '最近连接', value: row.lastConnectedAt }, + { label: '最近扫码登录', value: row.napcat?.lastLoginAt }, + { label: '容器启动', value: row.napcat?.lastStartedAt }, + ].filter((item) => item.value); + const latest = candidates.toSorted( + (left, right) => + new Date(right.value || '').getTime() - + new Date(left.value || '').getTime(), + )[0]; + return { + label: latest?.label || '暂无活动', + time: latest?.value ? formatDisplayTime(latest.value) : '', + }; + } + + function getRuntimeSummary(row: QqbotApi.Account) { + if (!row.enabled) { + return { level: 'warning', text: '账号已停用' }; + } + if (row.lastError) { + return { level: 'warning', text: `账号异常:${row.lastError}` }; + } + if (row.napcat?.lastError) { + return { level: 'warning', text: `容器异常:${row.napcat.lastError}` }; + } + if (row.connectStatus === 'online') { + return { level: 'normal', text: '消息链路可用' }; + } + if (row.napcat?.containerStatus === 'running') { + return { level: 'warning', text: '等待反向 WS' }; + } + if (row.napcat?.containerStatus === 'creating') { + return { level: 'warning', text: '容器创建中' }; + } + if (row.napcat?.containerStatus === 'stopped') { + return { level: 'warning', text: '容器已停止' }; + } + if (!row.napcat) { + return { + level: 'warning', + text: '可更新登录绑定容器', + }; + } + return { level: 'normal', text: '暂无异常记录' }; + } + + function formatDisplayTime(value: string) { + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleString('zh-CN', { hour12: false }); + } + function getAccountFormDefaults(): QqbotApi.AccountBody { return { accessToken: '', @@ -516,16 +682,17 @@ export default defineComponent({ v-slots={{ bodyCell: ({ column, record }: any) => { const row = record as QqbotApi.Account; - if (column.key === 'connectStatus') { - return ( - - {row.connectStatus === 'online' ? '在线' : '离线'} - - ); + if (column.key === 'accountOnlineStatus') { + return renderAccountOnlineStatus(row); + } + if (column.key === 'napcatRuntime') { + return renderNapcatRuntime(row); + } + if (column.key === 'lastHeartbeatAt') { + return renderRecentActivity(row); + } + if (column.key === 'runtimeSummary') { + return renderRuntimeSummary(row); } return undefined; }, @@ -579,7 +746,12 @@ export default defineComponent({ {scanQrcodeText.value ? ( qqbot-login-qrcode { + if (isQrcodeImageCandidate(scanQrcodeText.value)) { + scanQrcodeImageFailed.value = true; + } + }} + src={scanQrcodeImageSrc.value} style={{ background: '#fff', borderRadius: '8px', diff --git a/apps/web-antdv-next/src/views/system/dict/data.ts b/apps/web-antdv-next/src/views/system/dict/data.ts index 7247f37..5080f24 100644 --- a/apps/web-antdv-next/src/views/system/dict/data.ts +++ b/apps/web-antdv-next/src/views/system/dict/data.ts @@ -125,3 +125,17 @@ export function useGridFormSchema(): VbenFormSchema[] { }, ]; } + +export function useGroupFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '如 COMPONENT_TYPE', + }, + fieldName: 'keyword', + label: $t('system.dict.dictCode'), + }, + ]; +} diff --git a/apps/web-antdv-next/src/views/system/dict/list.vue b/apps/web-antdv-next/src/views/system/dict/list.vue index c6feec6..5ae66c0 100644 --- a/apps/web-antdv-next/src/views/system/dict/list.vue +++ b/apps/web-antdv-next/src/views/system/dict/list.vue @@ -6,22 +6,33 @@ import type { KtTableApi, KtTableButton, KtTableContext, + KtTablePageResult, + KtTableRegisterApi, KtTableRowAction, } from '#/components/ktTable'; -import { h } from 'vue'; +import { h, nextTick, ref, watch } from 'vue'; import { Page, useVbenModal } from '@vben/common-ui'; import { Plus } from '@vben/icons'; import { message, Tag } from 'antdv-next'; -import { deleteDict, getDictTree, toggleDictStatus } from '#/api/system/dict'; +import { + deleteDict, + getDictGroups, + 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 { + getStatusOptions, + useGridFormSchema, + useGroupFormSchema, +} from './data'; import Form from './modules/form.vue'; const [FormModal, formModalApi] = useVbenModal({ @@ -30,8 +41,26 @@ const [FormModal, formModalApi] = useVbenModal({ }); const statusOptions = getStatusOptions(); +const selectedDictCode = ref(''); +const itemTableRegistered = ref(false); -const columns: Array> = [ +const groupColumns: Array> = [ + { + dataIndex: 'dictCode', + key: 'dictCode', + title: $t('system.dict.dictCode'), + width: 220, + }, + { + align: 'right', + dataIndex: 'itemCount', + key: 'itemCount', + title: '项数', + width: 88, + }, +]; + +const columns: Array> = [ { dataIndex: 'dictCode', fixed: 'left', @@ -79,11 +108,27 @@ const columns: Array> = [ }, ]; -const api: KtTableApi = { - list: async (params) => await getDictTree(params), +const groupApi: KtTableApi = { + list: async (params) => await getDictGroups(params), }; -const buttons: Array> = [ +const api: KtTableApi = { + list: async (params) => { + if (!selectedDictCode.value) { + return { + items: [], + total: 0, + }; + } + + return await getDictList({ + ...params, + dictCode: selectedDictCode.value, + }); + }, +}; + +const buttons: Array> = [ { icon: () => h(Plus, { class: 'kt-table__button-icon' }), key: 'create', @@ -94,7 +139,7 @@ const buttons: Array> = [ }, ]; -const rowActions: Array> = [ +const rowActions: Array> = [ { key: 'toggle', label: $t('system.dict.toggle'), @@ -118,34 +163,130 @@ const rowActions: Array> = [ }, ]; -const [registerTable, tableApi] = useKtTable({ +const [registerGroupTable, groupTableApi] = useKtTable( + { + activeRowKey: selectedDictCode.value, + afterFetch: onGroupAfterFetch, + api: groupApi, + columns: groupColumns, + formOptions: { + formGrid: { + actionMinWidth: 180, + actionSpan: 8, + contentSpan: 16, + fieldSpan: 16, + }, + schema: useGroupFormSchema(), + }, + onRowClick: onGroupRowClick, + pageSize: 20, + rowKey: 'dictCode', + showIndex: false, + showSelection: false, + showTableSetting: false, + tableTitle: '字典编码', + }, +); + +const [registerItemTable, tableApi] = useKtTable({ api, buttons, columns, formOptions: { - schema: useGridFormSchema(), + schema: useGridFormSchema().filter((item) => item.fieldName !== 'dictCode'), }, + immediate: false, rowActions, - rowKey: 'treeKey', - showPagination: false, - tableTitle: $t('system.dict.list'), + rowKey: 'id', + showPagination: true, + tableTitle: getItemTableTitle(), }); -function getStatusOption(status: SystemDictApi.DictTreeItem['status']) { +watch(selectedDictCode, (dictCode) => { + groupTableApi.setProps({ + activeRowKey: dictCode, + }); + tableApi.setProps({ + tableTitle: getItemTableTitle(), + }); +}); + +function getItemTableTitle() { + return selectedDictCode.value + ? `字典项:${selectedDictCode.value}` + : '字典项'; +} + +function getStatusOption(status: SystemDictApi.DictItem['status']) { return statusOptions.find((item) => item.value === status); } -function onCreate() { - formModalApi.setData(undefined).open(); +function normalizeGroupRows( + result: + | KtTablePageResult + | SystemDictApi.DictGroup[], +) { + if (Array.isArray(result)) return result; + + return result.items || result.list || result.records || []; } -function onEdit(row: SystemDictApi.DictTreeItem) { +async function onGroupAfterFetch( + result: + | KtTablePageResult + | SystemDictApi.DictGroup[], +) { + const rows = normalizeGroupRows(result); + const selectedExists = rows.some( + (item) => item.dictCode === selectedDictCode.value, + ); + if (!selectedExists) { + selectedDictCode.value = rows[0]?.dictCode || ''; + } + + await reloadItemTable(); + return result; +} + +async function onGroupRowClick(row: SystemDictApi.DictGroup) { + if (selectedDictCode.value === row.dictCode) return; + + selectedDictCode.value = row.dictCode; + await reloadItemTable(); +} + +function onItemTableRegister(api: KtTableRegisterApi) { + registerItemTable(api); + itemTableRegistered.value = true; + reloadItemTable(); +} + +async function reloadItemTable() { + if (!itemTableRegistered.value) return; + + await nextTick(); + await tableApi.search(); +} + +function onCreate() { + formModalApi + .setData( + selectedDictCode.value + ? { + dictCode: selectedDictCode.value, + } + : undefined, + ) + .open(); +} + +function onEdit(row: SystemDictApi.DictItem) { formModalApi.setData(row).open(); } async function onToggle( - row: SystemDictApi.DictTreeItem, - context: KtTableContext, + row: SystemDictApi.DictItem, + context: KtTableContext, ) { const nextStatus = row.status === 1 ? 0 : 1; await toggleDictStatus(row.id, nextStatus); @@ -159,8 +300,8 @@ async function onToggle( } async function onDelete( - row: SystemDictApi.DictTreeItem, - context?: KtTableContext, + row: SystemDictApi.DictItem, + context?: KtTableContext, ) { const hideLoading = message.loading({ content: $t('ui.actionMessage.deleting', [row.label]), @@ -176,30 +317,61 @@ async function onDelete( key: 'action_process_msg', }); await (context || tableApi).reload(); + await groupTableApi.reload(); } catch { hideLoading(); } } -function onRefresh() { - tableApi.reload(); +async function onRefresh() { + await groupTableApi.reload(); + await tableApi.reload(); } + + diff --git a/apps/web-antdv-next/src/views/system/dict/modules/form.vue b/apps/web-antdv-next/src/views/system/dict/modules/form.vue index 4419091..3ff78c2 100644 --- a/apps/web-antdv-next/src/views/system/dict/modules/form.vue +++ b/apps/web-antdv-next/src/views/system/dict/modules/form.vue @@ -18,7 +18,7 @@ const emit = defineEmits<{ success: []; }>(); -const formData = ref(); +const formData = ref>(); const getTitle = computed(() => { return formData.value?.id ? $t('ui.actionTitle.edit', [$t('system.dict.name')]) @@ -51,7 +51,7 @@ const [Modal, modalApi] = useVbenModal({ }, onOpenChange(isOpen) { if (!isOpen) return; - const data = modalApi.getData(); + const data = modalApi.getData>(); formData.value = data || undefined; resetForm(); },