diff --git a/src/components/advanced/table-header-operation.vue b/src/components/advanced/table-header-operation.vue index 44358a46..c1f5cad1 100644 --- a/src/components/advanced/table-header-operation.vue +++ b/src/components/advanced/table-header-operation.vue @@ -19,7 +19,7 @@ withDefaults(defineProps(), { itemAlign: undefined, showAdd: true, showDelete: true, - showExport: true + showExport: false }); interface Emits { diff --git a/src/hooks/common/tree-table.ts b/src/hooks/common/tree-table.ts index 2a1ebbb3..99d40b71 100644 --- a/src/hooks/common/tree-table.ts +++ b/src/hooks/common/tree-table.ts @@ -4,21 +4,33 @@ import { jsonClone } from '@sa/utils'; import { useBoolean, useHookTable } from '@sa/hooks'; import { useAppStore } from '@/store/modules/app'; import { $t } from '@/locales'; +import { handleTree } from '@/utils/common'; type TableData = NaiveUI.TableData; type GetTableData = NaiveUI.GetTreeTableData; type TableColumn = NaiveUI.TableColumn; -export function useTreeTable(config: NaiveUI.NaiveTreeTableConfig) { +export function useTreeTable( + config: NaiveUI.NaiveTreeTableConfig & CommonType.TreeConfig & { defaultExpandAll?: boolean } +) { const scope = effectScope(); const appStore = useAppStore(); - const { apiFn, apiParams, immediate } = config; + const { + apiFn, + apiParams, + immediate, + idField, + parentIdField = 'parentId', + childrenField = 'children', + defaultExpandAll = true + } = config; const SELECTION_KEY = '__selection__'; - const EXPAND_KEY = '__expand__'; + const expandedRowKeys = ref([]); + const { loading, empty, @@ -36,9 +48,20 @@ export function useTreeTable(config: NaiveUI.N columns: config.columns, transformer: res => { const records = res.data || []; - return { - data: records - }; + if (!records.length) return { data: [] }; + + const treeData = handleTree(records, { + idField, + parentIdField, + childrenField + }); + + // 如果设置了默认展开所有,则收集所有节点的key + if (defaultExpandAll) { + expandedRowKeys.value = records.map(item => item[idField]); + } + + return { data: treeData }; }, getColumnChecks: cols => { const checks: NaiveUI.TableColumnCheck[] = []; @@ -89,6 +112,33 @@ export function useTreeTable(config: NaiveUI.N immediate }); + /** 收集所有节点的key */ + function collectAllNodeKeys(treeNodes: any[]): CommonType.IdType[] { + const keys: CommonType.IdType[] = []; + + const collect = (nodes: any[]) => { + nodes.forEach(node => { + keys.push(node[idField]); + if (node[childrenField]?.length) { + collect(node[childrenField]); + } + }); + }; + + collect(treeNodes); + return keys; + } + + /** 展开所有节点 */ + function expandAll() { + expandedRowKeys.value = collectAllNodeKeys(data.value); + } + + /** 收起所有节点 */ + function collapseAll() { + expandedRowKeys.value = []; + } + scope.run(() => { watch( () => appStore.locale, @@ -112,7 +162,10 @@ export function useTreeTable(config: NaiveUI.N getData, searchParams, updateSearchParams, - resetSearchParams + resetSearchParams, + expandedRowKeys, + expandAll, + collapseAll }; } diff --git a/src/router/elegant/imports.ts b/src/router/elegant/imports.ts index 748638c5..00df680d 100644 --- a/src/router/elegant/imports.ts +++ b/src/router/elegant/imports.ts @@ -22,6 +22,7 @@ export const views: Record Promise import("@/views/_builtin/login/index.vue"), home: () => import("@/views/home/index.vue"), system_config: () => import("@/views/system/config/index.vue"), + system_dept: () => import("@/views/system/dept/index.vue"), system_dict_data: () => import("@/views/system/dict/data/index.vue"), system_dict: () => import("@/views/system/dict/index.vue"), system_dict_type: () => import("@/views/system/dict/type/index.vue"), diff --git a/src/router/elegant/routes.ts b/src/router/elegant/routes.ts index 0639fce7..0078f7d4 100644 --- a/src/router/elegant/routes.ts +++ b/src/router/elegant/routes.ts @@ -95,6 +95,15 @@ export const generatedRoutes: GeneratedRoute[] = [ i18nKey: 'route.system_config' } }, + { + name: 'system_dept', + path: '/system/dept', + component: 'view.system_dept', + meta: { + title: 'system_dept', + i18nKey: 'route.system_dept' + } + }, { name: 'system_dict', path: '/system/dict', diff --git a/src/router/elegant/transform.ts b/src/router/elegant/transform.ts index 072fb6c7..883dc97f 100644 --- a/src/router/elegant/transform.ts +++ b/src/router/elegant/transform.ts @@ -171,6 +171,7 @@ const routeMap: RouteMap = { "login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?", "system": "/system", "system_config": "/system/config", + "system_dept": "/system/dept", "system_dict": "/system/dict", "system_dict_data": "/system/dict/data", "system_dict_type": "/system/dict/type", diff --git a/src/service/api/system/dept.ts b/src/service/api/system/dept.ts index 1c8b9baa..adb404c6 100644 --- a/src/service/api/system/dept.ts +++ b/src/service/api/system/dept.ts @@ -2,7 +2,7 @@ import { request } from '@/service/request'; /** 获取部门列表 */ export function fetchGetDeptList(params?: Api.System.DeptSearchParams) { - return request({ + return request({ url: '/system/dept/list', method: 'get', params @@ -28,7 +28,7 @@ export function fetchUpdateDept(data: Api.System.DeptOperateParams) { } /** 批量删除部门 */ -export function fetchDeleteDept(deptIds: CommonType.IdType[]) { +export function fetchBatchDeleteDept(deptIds: CommonType.IdType[]) { return request({ url: `/system/dept/${deptIds.join(',')}`, method: 'delete' diff --git a/src/typings/api/system.api.d.ts b/src/typings/api/system.api.d.ts index 9f296ab4..f9d64952 100644 --- a/src/typings/api/system.api.d.ts +++ b/src/typings/api/system.api.d.ts @@ -316,11 +316,13 @@ declare namespace Api { email: string; /** 部门状态(0正常 1停用) */ status: string; + /** 子部门 */ + children: Dept[]; }>; /** dept search params */ type DeptSearchParams = CommonType.RecordNullable< - Pick & Api.Common.CommonSearchParams + Pick & Api.Common.CommonSearchParams >; /** dept operate params */ @@ -358,8 +360,9 @@ declare namespace Api { /** post search params */ type PostSearchParams = CommonType.RecordNullable< - Pick - & { belongDeptId: CommonType.IdType } & Api.Common.CommonSearchParams + Pick & { + belongDeptId: CommonType.IdType; + } & Api.Common.CommonSearchParams >; /** post operate params */ @@ -441,7 +444,7 @@ declare namespace Api { /** tenant search params */ type TenantSearchParams = CommonType.RecordNullable< Pick & - Api.Common.CommonSearchParams + Api.Common.CommonSearchParams >; /** tenant operate params */ @@ -492,7 +495,7 @@ declare namespace Api { /** tenant package search params */ type TenantPackageSearchParams = CommonType.RecordNullable< Pick & - Api.Common.CommonSearchParams + Api.Common.CommonSearchParams >; /** tenant package operate params */ diff --git a/src/typings/common.d.ts b/src/typings/common.d.ts index 9ccc95ba..d0cc97c0 100644 --- a/src/typings/common.d.ts +++ b/src/typings/common.d.ts @@ -31,4 +31,16 @@ declare namespace CommonType { /** The res error code */ type ErrorCode = '401' | '403' | '404' | 'default'; + + /** 构造树型结构数据的配置选项 */ + type TreeConfig = { + /** id字段名 */ + idField: string; + /** 父节点字段名 */ + parentIdField?: string; + /** 子节点字段名 */ + childrenField?: string; + /** 过滤函数 */ + filterFn?: (node: any) => boolean; + }; } diff --git a/src/typings/elegant-router.d.ts b/src/typings/elegant-router.d.ts index ca03cef7..147323f1 100644 --- a/src/typings/elegant-router.d.ts +++ b/src/typings/elegant-router.d.ts @@ -25,6 +25,7 @@ declare module "@elegant-router/types" { "login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?"; "system": "/system"; "system_config": "/system/config"; + "system_dept": "/system/dept"; "system_dict": "/system/dict"; "system_dict_data": "/system/dict/data"; "system_dict_type": "/system/dict/type"; @@ -96,6 +97,7 @@ declare module "@elegant-router/types" { | "login" | "home" | "system_config" + | "system_dept" | "system_dict_data" | "system_dict" | "system_dict_type" diff --git a/src/utils/common.ts b/src/utils/common.ts index 593ad752..be60e4af 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,5 +1,4 @@ import { $t } from '@/locales'; - /** * Transform record to option * @@ -80,3 +79,76 @@ export function humpToLine(str: string, line: string = '-') { export function isNotNull(value: any) { return value !== undefined && value !== null && value !== '' && value !== 'undefined' && value !== 'null'; } + +/** + * 构造树型结构数据 + * + * @param {T[]} data 数据源 + * @param {TreeConfig} config 配置选项 + * @returns {T[]} 树形结构数据 + */ +export const handleTree = >(data: T[], config: CommonType.TreeConfig): T[] => { + if (!data?.length) { + return []; + } + + const { + idField, + parentIdField = 'parentId', + childrenField = 'children', + filterFn = () => true // 添加过滤函数,默认为不过滤 + } = config; + + // 使用 Map 替代普通对象,提高性能 + const childrenMap = new Map(); + const nodeMap = new Map(); + const tree: T[] = []; + + // 第一遍遍历:构建节点映射 + for (const item of data) { + const id = item[idField]; + const parentId = item[parentIdField]; + + 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]; + if (!nodeMap.has(parentId) && filterFn(item)) { + tree.push(item); + } + } + + // 递归构建树形结构 + const buildTree = (node: T) => { + const id = node[idField]; + 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; +}; diff --git a/src/utils/ruoyi.ts b/src/utils/ruoyi.ts deleted file mode 100644 index 85cead7c..00000000 --- a/src/utils/ruoyi.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * 构造树型结构数据 - * - * @param {any} data 数据源 - * @param {any} id id字段 默认 'id' - * @param {any} parentId 父节点字段 默认 'parentId' - * @param {any} children 孩子节点字段 默认 'children' - */ -export const handleMenuTree = ( - data: Api.System.MenuList, - id: keyof Api.System.Menu, - parentId?: keyof Api.System.Menu, - children?: keyof Api.System.Menu - // eslint-disable-next-line max-params -): Api.System.MenuList => { - const config: { - id: keyof Api.System.Menu; - parentId: keyof Api.System.Menu; - childrenList: keyof Api.System.Menu; - } = { - id: id || 'id', - parentId: parentId || 'parentId', - childrenList: children || 'children' - }; - - const childrenListMap: any = {}; - const nodeIds: any = {}; - const tree: Api.System.MenuList = []; - - for (const d of data) { - const pid = d[config.parentId]; - if (!childrenListMap[pid]) { - childrenListMap[pid] = []; - } - nodeIds[d[config.id]] = d; - if (d.menuType !== 'F') { - childrenListMap[pid].push(d); - } - - if (childrenListMap[pid].length === 0) { - childrenListMap[pid] = undefined; - } - } - - for (const d of data) { - const pid = d[config.parentId]; - if (!nodeIds[pid]) { - tree.push(d); - } - } - const adaptToChildrenList = (o: any) => { - if (childrenListMap[o[config.id]] !== null) { - o[config.childrenList] = childrenListMap[o[config.id]]; - } - if (o[config.childrenList]) { - for (const c of o[config.childrenList]) { - adaptToChildrenList(c); - } - } - }; - - for (const t of tree) { - adaptToChildrenList(t); - } - - return tree; -}; diff --git a/src/views/system/config/index.vue b/src/views/system/config/index.vue index 14edc6f5..2812a922 100644 --- a/src/views/system/config/index.vue +++ b/src/views/system/config/index.vue @@ -198,7 +198,7 @@ async function handleExport() { :scroll-x="962" :loading="loading" remote - :row-key="row => row.id" + :row-key="row => row.configId" :pagination="mobilePagination" class="sm:h-full" /> diff --git a/src/views/system/dept/index.vue b/src/views/system/dept/index.vue new file mode 100644 index 00000000..4b9bacf2 --- /dev/null +++ b/src/views/system/dept/index.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/src/views/system/dept/modules/dept-operate-drawer.vue b/src/views/system/dept/modules/dept-operate-drawer.vue new file mode 100644 index 00000000..f752ff83 --- /dev/null +++ b/src/views/system/dept/modules/dept-operate-drawer.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/src/views/system/dept/modules/dept-search.vue b/src/views/system/dept/modules/dept-search.vue new file mode 100644 index 00000000..d59e5c96 --- /dev/null +++ b/src/views/system/dept/modules/dept-search.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue index 195e94e0..dccae20b 100644 --- a/src/views/system/menu/index.vue +++ b/src/views/system/menu/index.vue @@ -7,13 +7,12 @@ import { fetchDeleteMenu, fetchGetMenuList } from '@/service/api/system'; import { useAppStore } from '@/store/modules/app'; import { menuIsFrameRecord, menuTypeRecord } from '@/constants/business'; import { $t } from '@/locales'; -import { handleMenuTree } from '@/utils/ruoyi'; import { useDict } from '@/hooks/business/dict'; import SvgIcon from '@/components/custom/svg-icon.vue'; import DictTag from '@/components/custom/dict-tag.vue'; import ButtonIcon from '@/components/custom/button-icon.vue'; +import { handleTree } from '@/utils/common'; import MenuOperateDrawer from './modules/menu-operate-drawer.vue'; - useDict('sys_show_hide'); useDict('sys_normal_disable'); @@ -44,7 +43,7 @@ const getMeunTree = async () => { menuId: 0, menuName: '根目录', icon: 'material-symbols:home-outline-rounded', - children: handleMenuTree(data, 'menuId') + children: handleTree(data, { idField: 'menuId', filterFn: item => item.menuType !== 'F' }) } ] as Api.System.Menu[]; endLoading(); diff --git a/src/views/system/post/index.vue b/src/views/system/post/index.vue index 369ebcd5..240f2d63 100644 --- a/src/views/system/post/index.vue +++ b/src/views/system/post/index.vue @@ -1,7 +1,8 @@