import editForm from "../form.vue"; import { handleTree } from "@/utils/tree"; import { message } from "@/utils/message"; import { getMenuList, createMenu, updateMenu, deleteMenuApi, updateMenuSort } from "@/api/system"; import { getAsyncRoutes } from "@/api/routes"; import { usePermissionStoreHook } from "@/store/modules/permission"; import { transformI18n } from "@/plugins/i18n"; import { addDialog } from "@/components/ReDialog"; import { reactive, ref, onMounted, h, nextTick } from "vue"; import type { FormItemProps } from "../utils/types"; import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { cloneDeep, isAllEmpty, deviceDetection } from "@pureadmin/utils"; import { initRouter } from "@/router/utils"; import { emitter } from "@/utils/mitt"; import Sortable from "sortablejs"; export function useMenu() { const form = reactive({ title: "" }); const formRef = ref(); const dataList = ref([]); const loading = ref(true); const flatDataMap = ref(new Map()); const flatOrderedList = ref([]); let sortableInstance: Sortable | null = null; let draggedDataCache: any = null; /** 深度优先遍历树,返回与 DOM 行顺序一致的扁平列表 */ function buildFlatOrdered(tree: any[]): any[] { const result: any[] = []; function walk(nodes: any[]) { for (const node of nodes) { result.push(node); if (node.children?.length > 0) { walk(node.children); } } } walk(tree); return result; } /** 给表格每行打上 data-id 属性,供拖拽时识别菜单 */ function tagRowIds(tableEl: HTMLElement) { const rows = tableEl.querySelectorAll(".el-table__body-wrapper .el-table__row"); const list = flatOrderedList.value; for (let i = 0; i < rows.length; i++) { if (i < list.length) { rows[i].setAttribute("data-id", String(list[i].id)); } } } const getMenuType = (type, text = false) => { switch (type) { case 0: return text ? "菜单" : "primary"; case 1: return text ? "iframe" : "warning"; case 2: return text ? "外链" : "danger"; case 3: return text ? "按钮" : "info"; } }; const columns: TableColumnList = [ { label: "菜单名称", prop: "title", align: "left", cellRenderer: ({ row }) => ( <> {h(useRenderIcon(row.icon), { style: { paddingTop: "1px" } })} {transformI18n(row.title)} ) }, { label: "菜单类型", prop: "menuType", width: 100, cellRenderer: ({ row, props }) => ( {getMenuType(row.menuType, true)} ) }, { label: "路由路径", prop: "path" }, { label: "组件路径", prop: "component", formatter: ({ path, component }) => isAllEmpty(component) ? path : component }, { label: "权限标识", prop: "auths" }, { label: "排序", prop: "rank", width: 100 }, { label: "隐藏", prop: "showLink", formatter: ({ showLink }) => (showLink ? "否" : "是"), width: 100 }, { label: "操作", fixed: "right", width: 210, slot: "operation" } ]; function handleSelectionChange(val) { console.log("handleSelectionChange", val); } function resetForm(formEl) { if (!formEl) return; formEl.resetFields(); onSearch(); } async function onSearch() { loading.value = true; const { data } = await getMenuList(); // 这里是返回一维数组结构,前端自行处理成树结构,返回格式要求:唯一id加父节点parentId,parentId取父节点id // 构建 id -> 菜单项 的扁平映射表,供拖拽排序时查询 const map = new Map(); for (const item of data) { map.set(item.id, item); } flatDataMap.value = map; // 构建树并缓存深度优先扁平列表(与 DOM 行顺序一致) const fullTree = handleTree(data); flatOrderedList.value = buildFlatOrdered(fullTree); if (!isAllEmpty(form.title)) { // 前端搜索菜单名称:过滤扁平数据后重新构建树 const filtered = data.filter(item => transformI18n(item.title).includes(form.title) ); dataList.value = handleTree(filtered); } else { dataList.value = fullTree; } setTimeout(() => { loading.value = false; }, 500); } function formatHigherMenuOptions(treeList) { if (!treeList || !treeList.length) return; const newTreeList = []; for (let i = 0; i < treeList.length; i++) { treeList[i].title = transformI18n(treeList[i].title); formatHigherMenuOptions(treeList[i].children); newTreeList.push(treeList[i]); } return newTreeList; } function openDialog(title = "新增", row?: FormItemProps) { addDialog({ title: `${title}菜单`, props: { formInline: { id: row?.id, menuType: row?.menuType ?? 0, higherMenuOptions: formatHigherMenuOptions(cloneDeep(dataList.value)), parentId: row?.parentId ?? 0, title: row?.title ?? "", name: row?.name ?? "", path: row?.path ?? "", component: row?.component ?? "", rank: row?.rank ?? 99, redirect: row?.redirect ?? "", icon: row?.icon ?? "", extraIcon: row?.extraIcon ?? "", enterTransition: row?.enterTransition ?? "", leaveTransition: row?.leaveTransition ?? "", activePath: row?.activePath ?? "", auths: row?.auths ?? "", frameSrc: row?.frameSrc ?? "", frameLoading: row?.frameLoading ?? true, keepAlive: row?.keepAlive ?? false, hiddenTag: row?.hiddenTag ?? false, fixedTag: row?.fixedTag ?? false, showLink: row?.showLink ?? true, showParent: row?.showParent ?? false } }, width: "45%", draggable: true, fullscreen: deviceDetection(), fullscreenIcon: true, closeOnClickModal: false, contentRenderer: () => h(editForm, { ref: formRef, formInline: null }), beforeSure: (done, { options }) => { const FormRef = formRef.value.getRef(); const curData = options.props.formInline as FormItemProps; FormRef.validate(async valid => { if (valid) { try { if (title === "新增") { await createMenu(curData); message(`新增菜单「${transformI18n(curData.title)}」成功`, { type: "success" }); } else { await updateMenu((curData as any).id, curData); message(`修改菜单「${transformI18n(curData.title)}」成功`, { type: "success" }); } done(); onSearch(); } catch (error) { message("操作失败,请重试", { type: "error" }); } } }); } }); } async function handleDelete(row) { try { await deleteMenuApi(row.id); message(`删除菜单「${transformI18n(row.title)}」成功`, { type: "success" }); onSearch(); } catch (error) { message("删除失败,请重试", { type: "error" }); } } function initDragSort(tableEl: HTMLElement) { destroyDragSort(); const tbody = tableEl.querySelector(".el-table__body-wrapper tbody") as HTMLElement; if (!tbody) return; // 给行打上 data-id 属性 tagRowIds(tableEl); sortableInstance = Sortable.create(tbody, { handle: ".drag-handle", draggable: ".el-table__row", animation: 200, ghostClass: "sortable-ghost", onStart() { draggedDataCache = null; }, onMove(evt) { const draggedId = evt.dragged.getAttribute("data-id"); const relatedId = evt.related.getAttribute("data-id"); const draggedData = flatDataMap.value.get(draggedId); const relatedData = flatDataMap.value.get(relatedId); if (!draggedData || !relatedData) return false; if (!draggedDataCache) draggedDataCache = draggedData; // 只允许同级拖拽 return draggedData.parentId === relatedData.parentId; }, async onEnd() { const draggedData = draggedDataCache; if (!draggedData) return; const parentId = draggedData.parentId; // 从 DOM 新顺序中收集同级兄弟的 id(data-id 在 DOM 移动后跟着走了) const allRows = tbody.querySelectorAll(".el-table__row"); const siblingIds: any[] = []; for (const row of allRows) { const id = row.getAttribute("data-id"); const data = flatDataMap.value.get(id); if (data && data.parentId === parentId && !siblingIds.includes(id)) { siblingIds.push(id); } } console.log("[menu-sort] draggedData.parentId:", parentId, "type:", typeof parentId); console.log("[menu-sort] siblingIds:", siblingIds, "count:", siblingIds.length); if (siblingIds.length <= 1) { console.log("[menu-sort] 同级兄弟不足2个,跳过排序"); return; } const sortData = siblingIds.map((id, index) => ({ id, rank: index + 1 })); console.log("[menu-sort] sortData:", sortData); try { await updateMenuSort(sortData); message("排序更新成功", { type: "success" }); // 刷新侧边栏路由顺序 try { // 先直接查 API 返回的原始数据(含 children 中第一条的 title) const routesRes = await getAsyncRoutes(); const apiRanks = (routesRes as any).data?.map((m: any) => { const child = m.children?.[0]; return `${m.meta?.title}(${m.meta?.rank},${m.path})` + (child ? ` → ${child.meta?.title}(${child.meta?.rank},${child.path})` : ''); }); console.log("[menu-sort] API 原始:", apiRanks); await initRouter(); await nextTick(); const topMenus = usePermissionStoreHook().wholeMenus .map((m: any) => `${m.meta?.title || "?"}(${m.meta?.rank},${m.path})`); console.log("[menu-sort] store:", topMenus); emitter.emit("menuRefresh"); } catch (e) { console.error("侧边栏路由刷新失败:", e); } } catch { message("排序更新失败", { type: "error" }); } draggedDataCache = null; onSearch(); } }); } function destroyDragSort() { if (sortableInstance) { sortableInstance.destroy(); sortableInstance = null; } } onMounted(() => { onSearch(); }); return { form, loading, columns, dataList, /** 搜索 */ onSearch, /** 重置 */ resetForm, /** 新增、修改菜单 */ openDialog, /** 删除菜单 */ handleDelete, handleSelectionChange, /** 初始化拖拽排序 */ initDragSort, /** 销毁拖拽排序 */ destroyDragSort }; }