admin_new/src/stores/menu.ts
Sisyphus 4899bc1793 fix(F006-W18): admin 前端 BUG A/B/C (客户运维反馈)
BUG A: APIKey 申请搜企业搜不到
  - ApplyApiKeyDialog.vue loadCustomers 加 name 参数调后端, 不再本地 filter
  - 去掉 filteredCustomers computed, 表格直接用 customers
  - searchKey 输入加 debounce 300ms watch 调 loadCustomers

BUG B: 企业列表/行业/标签管理等菜单不显示
  - menu.ts filterByFrontendTruth 加 url 规范化匹配
  - 后端 url 带 / (如 /user/enterprise/enterprise), MENU_LABELS 不带 /
  - normalizeUrl = url.replace(/^\//,"") 后再 has() 匹配

BUG C: baseURL 空串 巨变数字 nginx 不通
  - request.ts baseURL:"" 改 "/api"
  - 巨变数字 nginx 只有 /api/ location (没独立 /sys/ /auth/ 等)
  - devbox nginx /api/ 也去前缀 (rewrite ^/api/(.*)\$ /\$1), 兼容

dist 验证: baseURL:"/api" ✓
devbox 实测: name=郑州巨变 返 1 条命中
2026-07-14 19:15:48 +08:00

111 lines
4.0 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref } from 'vue'
import axios from 'axios'
import { httpGet } from '@/utils/request'
import { transformMenu, MENU_LABELS, type ResolvedMenu, type BackendMenuItem } from '@/config/menu-labels'
/**
* 菜单 Store
* W3.7 改用 httpGet 统一封装
* W2 D3 改造:从后端 /sys/menu/nav 拉真实菜单树
* - 后端返回 name=null,改前端 MENU_LABELS 映射表(详见 config/menu-labels.ts)
* - 用 transformMenu 转后端结构 -> 前端 ResolvedMenu
* - 同 menu store 暴露给 AppLayout 渲染
*/
export type MenuItem = ResolvedMenu
export const useMenuStore = defineStore('menu', () => {
const list = ref<MenuItem[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const collapsed = ref<boolean>(false)
/** 上次拉取时间(毫秒) */
const fetchedAt = ref<number>(0)
function toggleCollapsed() {
collapsed.value = !collapsed.value
}
/**
* F006 W2 D3:调 /sys/menu/nav 拿后端菜单树
* 走 vite proxy 转发到 gateway,header 带 Bearer(token 由 httpGet 拦截器自动加)
* 5 分钟内不重复拉
*/
/**
* 过滤无效菜单项(测试数据、数字名称等)
* super_admin=1 会返回所有菜单,包含很多测试/占位数据
*/
function filterInvalidMenus(items: ResolvedMenu[]): ResolvedMenu[] {
return items.filter(item => {
const title = item.title?.trim() || ''
// 过滤纯数字标题(如 "2", "70", "1111111111")
if (/^\d+$/.test(title)) return false
// 过滤"未知菜单"
if (title === '未知菜单') return false
// 过滤过长的数字串(超过8位)
if (/^\d{8,}$/.test(title)) return false
// 递归过滤子菜单
if (item.children?.length) {
item.children = filterInvalidMenus(item.children)
}
return true
})
}
/**
* V2 R2 (方案 B 实施): 后端拉到的菜单, url 不在 MENU_LABELS 里的全砍
* 父菜单(无 url 但有 children): 递归砍子菜单, 若砍光连父也删
* 极致干净: 前端 MENU_LABELS 是真实意图, 后端 super_admin 全集只是临时数据
*/
function filterByFrontendTruth(items: ResolvedMenu[]): ResolvedMenu[] {
const allowedUrls = new Set(MENU_LABELS.map(m => m.url))
const seenUrls = new Set<string>() // W5.x.x+3.1: 跨父菜单去重 (老 vue 一个 url 可能在多个父菜单下重复)
return items
.map(item => {
// 父菜单: 无 url, 递归处理 children
if (!item.url) {
if (!item.children?.length) return null
const filtered = filterByFrontendTruth(item.children)
if (!filtered.length) return null
return { ...item, children: filtered }
}
// F002-W18 修: 后端 url 可能带 / 前缀 (如 /user/enterprise/enterprise),
// MENU_LABELS 不带 / (如 user/enterprise/enterprise), 规范化后匹配
const normalizedUrl = item.url.replace(/^\//, '')
if (!allowedUrls.has(item.url) && !allowedUrls.has(normalizedUrl)) return null
if (seenUrls.has(normalizedUrl)) return null
seenUrls.add(normalizedUrl)
return item
})
.filter((item): item is ResolvedMenu => item !== null)
}
async function fetchMenu(force = false) {
if (loading.value) return
if (!force && fetchedAt.value && Date.now() - fetchedAt.value < 5 * 60 * 1000) {
return
}
loading.value = true
error.value = null
try {
const data = await httpGet<BackendMenuItem[]>('/sys/menu/nav')
const transformed = transformMenu(Array.isArray(data) ? data : [])
const frontendTruth = filterByFrontendTruth(transformed)
list.value = filterInvalidMenus(frontendTruth)
fetchedAt.value = Date.now()
} catch (e: any) {
error.value = e?.msg || e?.message || '菜单加载失败'
} finally {
loading.value = false
}
}
function clear() {
list.value = []
fetchedAt.value = 0
error.value = null
}
return { list, loading, error, collapsed, fetchedAt, toggleCollapsed, fetchMenu, clear }
})