From 4699654fc1c3b1d2545f266a2fae9693752ecbd8 Mon Sep 17 00:00:00 2001 From: xlsea Date: Thu, 31 Jul 2025 17:08:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(hooks):=20=E5=AE=8C=E6=88=90=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=20Hooks=20=E6=94=B9=E9=80=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/common/table.ts | 215 ++++++++++++++++- src/locales/langs/en-us.ts | 3 + src/locales/langs/zh-cn.ts | 3 + src/router/elegant/imports.ts | 2 + src/router/elegant/routes.ts | 29 +++ src/router/elegant/transform.ts | 3 + src/service/request/index.ts | 55 ++--- src/typings/components.d.ts | 19 ++ src/typings/elegant-router.d.ts | 6 + src/views/demo/demo/index.vue | 204 ++++++++++++++++ .../demo/demo/modules/demo-operate-drawer.vue | 144 +++++++++++ src/views/demo/demo/modules/demo-search.vue | 84 +++++++ src/views/demo/tree/index.vue | 224 ++++++++++++++++++ .../demo/tree/modules/tree-operate-drawer.vue | 156 ++++++++++++ src/views/demo/tree/modules/tree-search.vue | 98 ++++++++ 15 files changed, 1204 insertions(+), 41 deletions(-) create mode 100644 src/views/demo/demo/index.vue create mode 100644 src/views/demo/demo/modules/demo-operate-drawer.vue create mode 100644 src/views/demo/demo/modules/demo-search.vue create mode 100644 src/views/demo/tree/index.vue create mode 100644 src/views/demo/tree/modules/tree-operate-drawer.vue create mode 100644 src/views/demo/tree/modules/tree-search.vue diff --git a/src/hooks/common/table.ts b/src/hooks/common/table.ts index 6e106eb5..31bd3d16 100644 --- a/src/hooks/common/table.ts +++ b/src/hooks/common/table.ts @@ -1,4 +1,4 @@ -import { computed, effectScope, onScopeDispose, reactive, shallowRef, watch } from 'vue'; +import { computed, effectScope, onScopeDispose, reactive, ref, shallowRef, watch } from 'vue'; import type { Ref } from 'vue'; import type { PaginationProps } from 'naive-ui'; import { useBoolean, useTable } from '@sa/hooks'; @@ -224,25 +224,216 @@ export function defaultTransform( ): PaginationData { const { data, error } = response; - if (!error) { - const { rows: records, pageSize: current, pageNum: size, total } = data; - + if (error) { return { - data: records, - pageNum: current, - pageSize: size, - total + data: [], + pageNum: 1, + pageSize: 10, + total: 0 }; } + const { rows: records, pageSize: current, pageNum: size, total } = data; + return { - data: [], - pageNum: 1, - pageSize: 10, - total: 0 + data: records, + pageNum: current, + pageSize: size, + total }; } +type UseNaiveTreeTableOptions = UseNaiveTableOptions & { + keyField: keyof ApiData; + defaultExpandAll?: boolean; +}; + +export function useNaiveTreeTable(options: UseNaiveTreeTableOptions) { + const scope = effectScope(); + const appStore = useAppStore(); + const rows: Ref = ref([]); + + const result = useTable, false>({ + ...options, + pagination: false, + getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible), + getColumns, + onFetched: data => { + rows.value = data; + } + }); + + const { keyField = 'id', defaultExpandAll = false } = options; + + const expandedRowKeys = ref([]); + const { bool: isCollapse, toggle: toggleCollapse } = useBoolean(defaultExpandAll); + + /** expand all nodes */ + function expandAll() { + toggleCollapse(); + expandedRowKeys.value = rows.value.map(item => item[keyField as keyof ApiData]); + } + + /** collapse all nodes */ + function collapseAll() { + toggleCollapse(); + expandedRowKeys.value = []; + } + + scope.run(() => { + watch( + () => appStore.locale, + () => { + result.reloadColumns(); + } + ); + }); + + onScopeDispose(() => { + scope.stop(); + }); + + return { + ...result, + rows, + isCollapse, + expandedRowKeys, + expandAll, + collapseAll + }; +} + +export function useTreeTableOperate(data: Ref, idKey: keyof ApiData, getData: () => Promise) { + const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean(); + + const operateType = shallowRef('add'); + + function handleAdd() { + operateType.value = 'add'; + openDrawer(); + } + + /** the editing row data */ + const editingData = shallowRef(null); + + function handleEdit(id: ApiData[keyof ApiData]) { + operateType.value = 'edit'; + const findItem = data.value.find(item => item[idKey] === id) || null; + editingData.value = jsonClone(findItem); + + openDrawer(); + } + + /** the checked row keys of table */ + const checkedRowKeys = shallowRef([]); + + /** the hook after the batch delete operation is completed */ + async function onBatchDeleted() { + window.$message?.success($t('common.deleteSuccess')); + + checkedRowKeys.value = []; + + await getData(); + } + + /** the hook after the delete operation is completed */ + async function onDeleted() { + window.$message?.success($t('common.deleteSuccess')); + + await getData(); + } + + return { + drawerVisible, + openDrawer, + closeDrawer, + operateType, + handleAdd, + editingData, + handleEdit, + checkedRowKeys, + onBatchDeleted, + onDeleted + }; +} + +type TreeTableOptions = { + /** id field name */ + idField?: keyof ApiData; + /** parent id field name */ + parentIdField?: keyof ApiData; + /** children field name */ + childrenField?: keyof ApiData; + /** filter function */ + filterFn?: (node: ApiData) => boolean; +}; + +export function treeTransform( + response: FlatResponseData, + options: TreeTableOptions +): ApiData[] { + const { data, error } = response; + + if (error || !data.length) { + return []; + } + + const { idField = 'id', parentIdField = 'parentId', childrenField = 'children', filterFn = () => true } = options; + + // 使用 Map 替代普通对象,提高性能 + const childrenMap = new Map(); + const nodeMap = new Map(); + const tree: ApiData[] = []; + + // 第一遍遍历:构建节点映射 + for (const item of data) { + const id = item[idField as keyof ApiData]; + const parentId = item[parentIdField as keyof ApiData]; + + nodeMap.set(id, item); + + if (!childrenMap.has(parentId)) { + childrenMap.set(parentId, []); + } + // 应用过滤函数 + if (filterFn(item)) { + childrenMap.get(parentId)!.push(item); + } + } + + // 第二遍遍历:找出根节点 + for (const item of data) { + const parentId = item[parentIdField as keyof ApiData]; + if (!nodeMap.has(parentId) && filterFn(item)) { + tree.push(item); + } + } + + // 递归构建树形结构 + const buildTree = (node: ApiData) => { + const id = node[idField as keyof ApiData]; + const children = childrenMap.get(id); + + if (children?.length) { + // 使用类型断言确保类型安全 + (node as any)[childrenField] = children; + for (const child of children) { + buildTree(child); + } + } else { + // 如果没有子节点,设置为 undefined + (node as any)[childrenField] = undefined; + } + }; + + // 从根节点开始构建树 + for (const root of tree) { + buildTree(root); + } + + return tree; +} + function getColumnChecks>( cols: Column[], getColumnVisible?: (column: Column) => boolean diff --git a/src/locales/langs/en-us.ts b/src/locales/langs/en-us.ts index e0eed800..686f7dd6 100644 --- a/src/locales/langs/en-us.ts +++ b/src/locales/langs/en-us.ts @@ -243,6 +243,9 @@ const local: App.I18n.Schema = { 'social-callback': 'Social Callback', monitor_cache: 'Cache Monitor', 'user-center': 'User Center', + demo: 'Demo', + demo_demo: 'Demo Table', + demo_tree: 'Demo Tree', exception: 'Exception', exception_403: '403', exception_404: '404', diff --git a/src/locales/langs/zh-cn.ts b/src/locales/langs/zh-cn.ts index 085130aa..84991a1a 100644 --- a/src/locales/langs/zh-cn.ts +++ b/src/locales/langs/zh-cn.ts @@ -240,6 +240,9 @@ const local: App.I18n.Schema = { 'social-callback': '单点登录回调', monitor_cache: '缓存监控', 'user-center': '个人中心', + demo: '测试', + demo_demo: '测试单表', + demo_tree: '测试树表', exception: '异常页', exception_403: '403', exception_404: '404', diff --git a/src/router/elegant/imports.ts b/src/router/elegant/imports.ts index 823a09a0..97950bea 100644 --- a/src/router/elegant/imports.ts +++ b/src/router/elegant/imports.ts @@ -22,6 +22,8 @@ export const views: Record Promise import("@/views/_builtin/login/index.vue"), "social-callback": () => import("@/views/_builtin/social-callback/index.vue"), "user-center": () => import("@/views/_builtin/user-center/index.vue"), + demo_demo: () => import("@/views/demo/demo/index.vue"), + demo_tree: () => import("@/views/demo/tree/index.vue"), home: () => import("@/views/home/index.vue"), monitor_cache: () => import("@/views/monitor/cache/index.vue"), }; diff --git a/src/router/elegant/routes.ts b/src/router/elegant/routes.ts index a6335e75..dd3eb0cc 100644 --- a/src/router/elegant/routes.ts +++ b/src/router/elegant/routes.ts @@ -39,6 +39,35 @@ export const generatedRoutes: GeneratedRoute[] = [ hideInMenu: true } }, + { + name: 'demo', + path: '/demo', + component: 'layout.base', + meta: { + title: 'demo', + i18nKey: 'route.demo' + }, + children: [ + { + name: 'demo_demo', + path: '/demo/demo', + component: 'view.demo_demo', + meta: { + title: 'demo_demo', + i18nKey: 'route.demo_demo' + } + }, + { + name: 'demo_tree', + path: '/demo/tree', + component: 'view.demo_tree', + meta: { + title: 'demo_tree', + i18nKey: 'route.demo_tree' + } + } + ] + }, { name: 'home', path: '/home', diff --git a/src/router/elegant/transform.ts b/src/router/elegant/transform.ts index 5f3bc481..d460f19f 100644 --- a/src/router/elegant/transform.ts +++ b/src/router/elegant/transform.ts @@ -170,6 +170,9 @@ const routeMap: RouteMap = { "403": "/403", "404": "/404", "500": "/500", + "demo": "/demo", + "demo_demo": "/demo/demo", + "demo_tree": "/demo/tree", "home": "/home", "iframe-page": "/iframe-page/:url", "login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?", diff --git a/src/service/request/index.ts b/src/service/request/index.ts index dccb839c..00292d14 100644 --- a/src/service/request/index.ts +++ b/src/service/request/index.ts @@ -25,6 +25,32 @@ export const request = createFlatRequest( refreshTokenPromise: null } as RequestInstanceState, transform(response: AxiosResponse>) { + if (import.meta.env.VITE_APP_ENCRYPT === 'Y') { + // 加密后的 AES 秘钥 + const keyStr = response.headers[encryptHeader]; + // 加密 + if (keyStr && keyStr !== '') { + const data = String(response.data); + // 请求体 AES 解密 + const base64Str = decrypt(keyStr); + // base64 解码 得到请求头的 AES 秘钥 + const aesKey = decryptBase64(base64Str.toString()); + // aesKey 解码 data + const decryptData = decryptWithAes(data, aesKey); + // 将结果 (得到的是 JSON 字符串) 转为 JSON + response.data = JSON.parse(decryptData); + } + } + + // 二进制数据则直接返回 + if (response.request.responseType === 'blob' || response.request.responseType === 'arraybuffer') { + return response.data; + } + + if (response.data.rows) { + return response.data; + } + return response.data.data; }, async onRequest(config) { @@ -131,35 +157,6 @@ export const request = createFlatRequest( return null; }, - transformBackendResponse(response) { - if (import.meta.env.VITE_APP_ENCRYPT === 'Y') { - // 加密后的 AES 秘钥 - const keyStr = response.headers[encryptHeader]; - // 加密 - if (keyStr && keyStr !== '') { - const data = String(response.data); - // 请求体 AES 解密 - const base64Str = decrypt(keyStr); - // base64 解码 得到请求头的 AES 秘钥 - const aesKey = decryptBase64(base64Str.toString()); - // aesKey 解码 data - const decryptData = decryptWithAes(data, aesKey); - // 将结果 (得到的是 JSON 字符串) 转为 JSON - response.data = JSON.parse(decryptData); - } - } - - // 二进制数据则直接返回 - if (response.request.responseType === 'blob' || response.request.responseType === 'arraybuffer') { - return response.data; - } - - if (response.data.rows) { - return response.data; - } - - return response.data.data; - }, onError(error) { // when the request is fail, you can show error message diff --git a/src/typings/components.d.ts b/src/typings/components.d.ts index 0a0d484f..0d8d0df1 100644 --- a/src/typings/components.d.ts +++ b/src/typings/components.d.ts @@ -26,15 +26,25 @@ declare module 'vue' { FullScreen: typeof import('./../components/common/full-screen.vue')['default'] IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default'] IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default'] + IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default'] IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default'] IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default'] + IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default'] + IconIcRoundSearch: typeof import('~icons/ic/round-search')['default'] IconLocalBanner: typeof import('~icons/local/banner')['default'] IconLocalLogo: typeof import('~icons/local/logo')['default'] + 'IconMaterialSymbols:add': typeof import('~icons/material-symbols/add')['default'] + 'IconMaterialSymbols:deleteOutline': typeof import('~icons/material-symbols/delete-outline')['default'] + 'IconMaterialSymbols:downloadRounded': typeof import('~icons/material-symbols/download-rounded')['default'] + 'IconMaterialSymbols:refreshRounded': typeof import('~icons/material-symbols/refresh-rounded')['default'] 'IconMdi:github': typeof import('~icons/mdi/github')['default'] IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default'] IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default'] + IconMdiDrag: typeof import('~icons/mdi/drag')['default'] IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default'] IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default'] + 'IconQuill:collapse': typeof import('~icons/quill/collapse')['default'] + 'IconQuill:expand': typeof import('~icons/quill/expand')['default'] 'IconSimpleIcons:gitee': typeof import('~icons/simple-icons/gitee')['default'] IconUilSearch: typeof import('~icons/uil/search')['default'] JsonPreview: typeof import('./../components/custom/json-preview.vue')['default'] @@ -53,7 +63,10 @@ declare module 'vue' { NButton: typeof import('naive-ui')['NButton'] NCard: typeof import('naive-ui')['NCard'] NCheckbox: typeof import('naive-ui')['NCheckbox'] + NCollapse: typeof import('naive-ui')['NCollapse'] + NCollapseItem: typeof import('naive-ui')['NCollapseItem'] NColorPicker: typeof import('naive-ui')['NColorPicker'] + NDataTable: typeof import('naive-ui')['NDataTable'] NDialogProvider: typeof import('naive-ui')['NDialogProvider'] NDivider: typeof import('naive-ui')['NDivider'] NDrawer: typeof import('naive-ui')['NDrawer'] @@ -63,6 +76,7 @@ declare module 'vue' { NEmpty: typeof import('naive-ui')['NEmpty'] NForm: typeof import('naive-ui')['NForm'] NFormItem: typeof import('naive-ui')['NFormItem'] + NFormItemGi: typeof import('naive-ui')['NFormItemGi'] NGi: typeof import('naive-ui')['NGi'] NGrid: typeof import('naive-ui')['NGrid'] NInput: typeof import('naive-ui')['NInput'] @@ -75,6 +89,7 @@ declare module 'vue' { NMessageProvider: typeof import('naive-ui')['NMessageProvider'] NModal: typeof import('naive-ui')['NModal'] NNotificationProvider: typeof import('naive-ui')['NNotificationProvider'] + NP: typeof import('naive-ui')['NP'] NPopconfirm: typeof import('naive-ui')['NPopconfirm'] NPopover: typeof import('naive-ui')['NPopover'] NScrollbar: typeof import('naive-ui')['NScrollbar'] @@ -86,8 +101,12 @@ declare module 'vue' { NTab: typeof import('naive-ui')['NTab'] NTabs: typeof import('naive-ui')['NTabs'] NTag: typeof import('naive-ui')['NTag'] + NText: typeof import('naive-ui')['NText'] NThing: typeof import('naive-ui')['NThing'] NTooltip: typeof import('naive-ui')['NTooltip'] + NTreeSelect: typeof import('naive-ui')['NTreeSelect'] + NUpload: typeof import('naive-ui')['NUpload'] + NUploadDragger: typeof import('naive-ui')['NUploadDragger'] NWatermark: typeof import('naive-ui')['NWatermark'] OssUpload: typeof import('./../components/custom/oss-upload.vue')['default'] PinToggler: typeof import('./../components/common/pin-toggler.vue')['default'] diff --git a/src/typings/elegant-router.d.ts b/src/typings/elegant-router.d.ts index f1c5b8d3..415290c5 100644 --- a/src/typings/elegant-router.d.ts +++ b/src/typings/elegant-router.d.ts @@ -24,6 +24,9 @@ declare module "@elegant-router/types" { "403": "/403"; "404": "/404"; "500": "/500"; + "demo": "/demo"; + "demo_demo": "/demo/demo"; + "demo_tree": "/demo/tree"; "home": "/home"; "iframe-page": "/iframe-page/:url"; "login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?"; @@ -69,6 +72,7 @@ declare module "@elegant-router/types" { | "403" | "404" | "500" + | "demo" | "home" | "iframe-page" | "login" @@ -99,6 +103,8 @@ declare module "@elegant-router/types" { | "login" | "social-callback" | "user-center" + | "demo_demo" + | "demo_tree" | "home" | "monitor_cache" >; diff --git a/src/views/demo/demo/index.vue b/src/views/demo/demo/index.vue new file mode 100644 index 00000000..fcdfa2be --- /dev/null +++ b/src/views/demo/demo/index.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/src/views/demo/demo/modules/demo-operate-drawer.vue b/src/views/demo/demo/modules/demo-operate-drawer.vue new file mode 100644 index 00000000..18c1e976 --- /dev/null +++ b/src/views/demo/demo/modules/demo-operate-drawer.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/src/views/demo/demo/modules/demo-search.vue b/src/views/demo/demo/modules/demo-search.vue new file mode 100644 index 00000000..665eb430 --- /dev/null +++ b/src/views/demo/demo/modules/demo-search.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/src/views/demo/tree/index.vue b/src/views/demo/tree/index.vue new file mode 100644 index 00000000..e907f0ff --- /dev/null +++ b/src/views/demo/tree/index.vue @@ -0,0 +1,224 @@ + + + + + diff --git a/src/views/demo/tree/modules/tree-operate-drawer.vue b/src/views/demo/tree/modules/tree-operate-drawer.vue new file mode 100644 index 00000000..832e9f5d --- /dev/null +++ b/src/views/demo/tree/modules/tree-operate-drawer.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/src/views/demo/tree/modules/tree-search.vue b/src/views/demo/tree/modules/tree-search.vue new file mode 100644 index 00000000..6b901f9a --- /dev/null +++ b/src/views/demo/tree/modules/tree-search.vue @@ -0,0 +1,98 @@ + + + + +