| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- 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,
- }
- }
|