fix(frontend): 菜单点击偶发无响应三处根因——①路由懒加载chunk失败被静默吞掉(新增onError自动整页重载+保险丝防循环) ②首页入口被权限接口阻塞(改乐观显示+缓存TTL10分钟+超时30s→8s) ③菜单hover预取目标页chunk(工具卡片/工作流步骤/顶部导航)
This commit is contained in:
@@ -37,6 +37,40 @@ function getAppPermissionCacheKey(uid: number) {
|
||||
return `app_column_permissions:${String(uid)}`
|
||||
}
|
||||
|
||||
/** 权限缓存有效期:10 分钟内直接用本地缓存渲染菜单/入口,避免接口慢时入口消失 */
|
||||
const PERMISSION_CACHE_TTL_MS = 10 * 60 * 1000
|
||||
|
||||
interface CachedAppPermissions {
|
||||
ts: number
|
||||
items: PermissionMenuItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取本地缓存的权限菜单项(兼容旧版纯数组格式:无时间戳视为长期有效)。
|
||||
* 接口失败/超时时由调用方用它兜底,保证菜单不因权限接口抖动而消失。
|
||||
*/
|
||||
export function readCachedAppColumnPermissions(uid: number): PermissionMenuItem[] | null {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(getAppPermissionCacheKey(uid)) || ''
|
||||
if (!raw) return null
|
||||
const parsed: unknown = JSON.parse(raw)
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed as PermissionMenuItem[]
|
||||
}
|
||||
if (
|
||||
parsed &&
|
||||
typeof parsed === 'object' &&
|
||||
Array.isArray((parsed as CachedAppPermissions).items) &&
|
||||
Date.now() - Number((parsed as CachedAppPermissions).ts || 0) < PERMISSION_CACHE_TTL_MS
|
||||
) {
|
||||
return (parsed as CachedAppPermissions).items
|
||||
}
|
||||
return null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getAuthToken() {
|
||||
return typeof window === 'undefined' ? '' : window.localStorage.getItem('aiimage_auth_token') || ''
|
||||
}
|
||||
@@ -70,6 +104,8 @@ async function fetchAppColumnPermissions() {
|
||||
{
|
||||
params: { menuType: 'app' },
|
||||
headers,
|
||||
// 权限接口只决定菜单显隐:8s 未返回即失败走缓存兜底,避免入口区长时间空白
|
||||
timeout: 8000,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -79,7 +115,7 @@ async function fetchAppColumnPermissions() {
|
||||
|
||||
const items = res.data || res.items || []
|
||||
try {
|
||||
window.localStorage.setItem(cacheKey, JSON.stringify(items))
|
||||
window.localStorage.setItem(cacheKey, JSON.stringify({ ts: Date.now(), items }))
|
||||
} catch (_error) {}
|
||||
|
||||
return items
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { Router } from 'vue-router'
|
||||
|
||||
/**
|
||||
* 路由懒加载 chunk 预取
|
||||
*
|
||||
* 页面均为动态 import(每页独立 chunk,见 src/router/index.ts),首次进入某页
|
||||
* 需现场从线上拉取 chunk。菜单 hover 时提前触发 import()(结果被 router 缓存,
|
||||
* 点击时直接命中缓存),弱网下显著减少"点了没反应/跳转慢"的等待时间。
|
||||
*/
|
||||
|
||||
/** 触发目标路由的懒加载 chunk 预取;href 为空或预取失败时静默(点击时会走 onError 兜底) */
|
||||
export function prefetchRouteChunk(router: Router, href?: string): void {
|
||||
if (!href) return
|
||||
let target = href.trim()
|
||||
const hashIndex = target.indexOf('#')
|
||||
if (hashIndex >= 0) {
|
||||
target = target.slice(0, hashIndex)
|
||||
}
|
||||
try {
|
||||
const resolved = router.resolve(target)
|
||||
const component = resolved.matched[0]?.components?.default as unknown
|
||||
// 懒加载路由的 default 是 () => import(...) 加载器:调用即触发 chunk 下载(结果被 vue-router 缓存)
|
||||
if (typeof component === 'function') {
|
||||
const result = (component as () => unknown)()
|
||||
if (result && typeof (result as Promise<unknown>).then === 'function') {
|
||||
void (result as Promise<unknown>).catch(() => {
|
||||
/* 预取失败静默,点击时统一由 router.onError 兜底 */
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* 路径无法解析时忽略 */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user