import { reactive, computed } from 'vue' import { fetchCurrentMenu } from '@/api/menu' import router from '@/router' import { useAuth } from '@/store/auth' // 上边栏最多支持的层级数;超出部分只把最后一层放到侧边栏 const MAX_TOPBAR_LEVELS = 3 const state = reactive({ loaded: false, loading: false, error: null, topMenus: [], activeId: null, }) function bySort(a, b) { return (a?.sort ?? 0) - (b?.sort ?? 0) } function sortTree(list) { if (!Array.isArray(list)) return [] return list .slice() .sort(bySort) .map((m) => ({ ...m, children: Array.isArray(m.children) ? sortTree(m.children) : [] })) } function pathMatches(item, path) { if (!item?.route) return false return path === item.route || path.startsWith(`${item.route}/`) } // 判断 item 是否“拥有”该路径,即自身匹配,或任意后代匹配 function ownsRoute(item, path) { if (pathMatches(item, path)) return true if (!Array.isArray(item.children)) return false return item.children.some((c) => ownsRoute(c, path)) } function findNode(list, path) { if (!Array.isArray(list)) return null for (const node of list) { if (pathMatches(node, path)) return node if (Array.isArray(node.children)) { const hit = findNode(node.children, path) if (hit) return node } } return null } // 计算整棵菜单树的最大深度 function treeDepth(list, current = 1) { if (!Array.isArray(list) || list.length === 0) return 0 let max = current for (const node of list) { if (Array.isArray(node.children) && node.children.length) { const d = treeDepth(node.children, current + 1) if (d > max) max = d } } return max } // 从顶层往下逐级选中(未命中的层级用第一个子项补齐) function buildCascade(items, path) { const result = [] let current = items while (Array.isArray(current) && current.length) { // 用 ownsRoute 判断:路由可能在当前项上,也可能在后代上 let hit = current.find((m) => ownsRoute(m, path)) if (!hit) hit = current[0] result.push(hit) current = hit.children } return result } function findFirstDescendantRoute(item) { let cur = item while (cur) { if (cur.route) return cur.route cur = Array.isArray(cur.children) && cur.children.length ? cur.children[0] : null } return null } export function useMenu() { async function load(force = false) { if (state.loading) return if (state.loaded && !force) return state.loading = true state.error = null try { const data = await fetchCurrentMenu() const list = Array.isArray(data) ? data : [] state.topMenus = sortTree(list) state.loaded = true if (!state.topMenus.some((m) => m.id === state.activeId)) { state.activeId = state.topMenus[0]?.id ?? null } } catch (err) { state.error = err useAuth().logout() const redirect = router.currentRoute.value.fullPath if (router.currentRoute.value.name !== 'login') { router.replace({ name: 'login', query: { redirect } }) } } finally { state.loading = false } } function setActive(id) { if (state.activeId === id) return state.activeId = id } // 根据当前路径同步选中的一级菜单(及一级菜单下的二级高亮) function syncActiveFromPath(path) { if (!Array.isArray(state.topMenus) || !state.topMenus.length) return const hit = findNode(state.topMenus, path) if (hit && state.activeId !== hit.id) { state.activeId = hit.id } } function reset() { state.loaded = false state.loading = false state.error = null state.topMenus = [] state.activeId = null } const activeTopMenu = computed( () => state.topMenus.find((m) => m.id === state.activeId) ?? null, ) const subMenus = computed(() => { const cur = activeTopMenu.value return cur && Array.isArray(cur.children) ? cur.children : [] }) // ===== 多级菜单:上边栏最多展示 N-1 层(≤3),侧边栏只展示最后一层 ===== // 深度按当前激活的一级菜单所在分支计算,而非整棵树 const activeBranchRoot = computed(() => cascadeSelection.value[0] ?? null) // 当前激活一级菜单分支的深度 const activeBranchDepth = computed(() => { const root = activeBranchRoot.value if (!root) return 0 return treeDepth([root]) }) // 上边栏要渲染的层级数: // 深度=1 → 1;深度=2 → 1;深度=3 → 2;深度=4 → 3;深度 >4 同样最多 3 const topBarLevelCount = computed(() => { const depth = activeBranchDepth.value if (depth <= 0) return 0 return Math.max(1, Math.min(depth - 1, MAX_TOPBAR_LEVELS)) }) // 侧边栏要展示的层级(当前分支最后一层);深度=1 时为 0(不展示侧边栏) const sidebarDepth = computed(() => Math.max(0, activeBranchDepth.value - topBarLevelCount.value)) // 当前路由对应的级联选中项,顶层到深层依次排列;缺失层级用第一个子项补齐 const cascadeSelection = computed(() => { const path = router.currentRoute.value.path return buildCascade(state.topMenus, path) }) // 上边栏各层级要渲染的菜单项(数组的每一项对应一行) const topBarLevelItems = computed(() => { const sel = cascadeSelection.value const rows = [state.topMenus] for (let i = 0; i < topBarLevelCount.value - 1; i++) { const parent = sel[i] if (!parent) { rows.push([]) continue } rows.push(Array.isArray(parent.children) ? parent.children : []) } return rows }) // 侧边栏要展示的项 = 上边栏最后一行选中项的子项(即整棵树的最后一层) const sidebarItems = computed(() => { if (sidebarDepth.value <= 0) return [] const sel = cascadeSelection.value const parent = sel[topBarLevelCount.value - 1] if (!parent) return [] return Array.isArray(parent.children) ? parent.children : [] }) // 当前激活的层级项(高亮用):按层级取 cascadeSelection 中对应下标 const activeByLevel = computed(() => cascadeSelection.value.slice()) // 点击某层某项:定位该层为选中项,并跳转到该项目(或首个后代)的路由 function selectLevel(level, item) { if (!item) return // L1 仅为分组目录(无 route 且有子项)时,不更新 activeId 也不导航, // 真正的导航要交给二级 / 三级菜单触发 if (level === 1 && !item.route && Array.isArray(item.children) && item.children.length > 0) { return } if (level === 1) { state.activeId = item.id } const target = findFirstDescendantRoute(item) if (target && router.currentRoute.value.path !== target) { router.push(target) } } return { state, load, setActive, syncActiveFromPath, reset, activeTopMenu, subMenus, activeBranchRoot, activeBranchDepth, topBarLevelCount, sidebarDepth, cascadeSelection, topBarLevelItems, sidebarItems, activeByLevel, selectLevel, } }