导航切换缓存刷新机制功能概述切换导航页面时自动从两个 NE网元接口获取真实数据内部页面使用缓存数据展示。同时提供请求去重、竞态条件防护、异常兜底等机制确保页面在数据加载失败时不会白屏。涉及文件文件角色src/app/(main)/layout.tsx入口监听路由变化主动获取两个接口数据并写入 storesrc/store/store.ts数据中心定义缓存结构类型提供全局状态src/app/(main)/ne/neConfig/feature/ConfigDetail.tsx消费者从 store 读取缓存展示并提供手动刷新能力数据流架构路由变化 (pathname) │ ▼ layout.tsx useEffect │ ├─ ① 取消上一次请求 (fetchAbortRef.abort) │ ├─ ② 创建新 AbortController 递增 fetchId │ ├─ ③ await neService.getNeData() │ └─ 重组排序connected 在前disconnected 在后 │ └─ 写入 store.setNeData() │ └─ 触发 page.tsx → ConfigDetail 挂载 │ └─ ④ fetchId 核对 → getAllTypeData(ne.uid) └─ try-catch 保护 └─ 写入 store.setAllTypeData({ uid, data }) └─ 触发 ConfigDetail 重新读取缓存缓存读取流程ConfigDetail挂载 / typeActiveTabKey 变化 │ ▼ initialTableData(typeActiveTabKey) │ ├─ 检查 allTypeData 是否为 null → 空表 ✅ ├─ 检查 allTypeData.uid neUid → 不匹配时空表 ✅ ├─ 检查 allTypeData.data[type] 是否有数据 → 空表 ✅ └─ 有数据 → getTableData → 展示 ✅手动刷新流程ConfigDetail用户点击 reload │ ▼ handleTypeChange(key, uid) │ ├─ 取消上一次请求 (abortControllerRef.abort) ├─ 创建新 AbortController ├─ 发起 API 请求支持 AbortSignal ├─ 检查 controller.signal.aborted → 取消则不处理 ├─ 成功 → getTableData → 更新表格 └─ 失败 → catch → setAllDataSource([]) → finally → setLoading(false)三个文件的改动详情1. layout.tsx新增变量fetchAbortRef— useRefAbortController用于取消上一次未完成的请求fetchIdRef— useRefnumber递增计数器确保只应用最新请求的响应新增/修改函数getAllTypeData— 添加 try-catch写入 store 时携带{ uid, data }结构核心 useEffect依赖 pathname每次路由变化 1. 跳过公开路由/login, /register, /forgot-password, /auth 2. 取消上一次请求 3. 创建新 AbortController 递增 fetchId 4. 调用 getNeData()成功后 a. 检查 controller.signal.aborted取消则不处理 b. 排序重组 NE 列表 → setNeData() c. 核对 fetchId 一致性 → getAllTypeData(ne.uid) 5. 清理函数 → 取消请求 6. AbortError 静默忽略2. store.ts新增接口定义interfaceAllTypeDataCache{uid:string;// 该缓存所属的网元 IDdata:Recordstring,any;// 按类型分组的缓存数据}类型变更字段之前之后allTypeDataanyAllTypeDataCache | nullsetAllTypeData参数(data: any)(data: AllTypeDataCache | null)初始值变更// 之前空对象语义上容易误认为 缓存已就绪但为空allTypeData:{},// 之后null明确表示 尚未获取任何缓存allTypeData:null,3. ConfigDetail.tsx新增变量abortControllerRef— useRefAbortController每个 ConfigDetail 实例独立隔离卸载 cleanup useEffect — 组件销毁时取消正在进行的请求函数改动函数改动getCardPreConf添加 try-catch失败时初始化为 8 个空槽位getStateData添加 try-catchgetChassisData添加 try-catchhandleTypeChange重构AbortController per-instance try-catch-finally loading 在 finally 中清除initialTableData添加!allTypeData与allTypeData.uid ! neUid双重防御删除的死代码isLoadingRef— 只写不读已删除if (!options)分支 — options 初始值始终为 truthy无效守卫关键设计决策为什么要按 uid 隔离缓存allTypeData是全局 store 变量。如果仅存裸数据{ chassis: [...], allcard: [...] }则打开多 Tab不同 NE时Tab-B 会读到 Tab-A 的缓存显示错误数据。改为{ uid: string, data: {...} }后Tab-B 读取时检查allTypeData.uid ! neUid发现不匹配直接返回空数据保证不会显示错误数据。为什么用 useRef 管理 AbortController每个 ConfigDetail 实例需要独立的 AbortController否则一个 Tab 的请求会取消另一个 Tab 的请求。useRef保证每个组件实例有自己独立的引用。为什么 try-catch-finally 是唯一正确模式在handleTypeChange中setLoading(false)放在finally块中保证无论成功、失败、还是异常loading 状态都会被清除。这是 React 中处理异步 loading 的标准模式。维护规则供后续页面沿用规则 1Layout 只负责刷新缓存Layout 只做两件事 → getNeData() 写入 neData网元列表 → getAllTypeData() 写入 allTypeData全局缓存池 其他页面自己的数据在各自组件内管理规则 2缓存数据必须按业务 key 隔离// ✅ 正确带 uid 标记setAllTypeData({uid:id,data:res.data});// ❌ 错误全局裸对象多实例会互相覆盖setAllTypeData(res.data);规则 3读缓存时校验归属// ✅ 正确的读缓存模式if(!allTypeData||allTypeData.uid!myUid){setDataSource([]);return;}constdataallTypeData.data[type];规则 4所有 async 函数必须 try-catchloading 必须 finally 清除constfetchDataasync(){setLoading(true);try{constresawaitapi();if(!res?.data){setDataSource([]);return;}setDataSource(res.data);}catch(error){console.warn([Component] fetch failed:,error);setDataSource([]);}finally{setLoading(false);// ❗保证一定被清除}};规则 5请求去重 竞态防护constabortRefuseRefAbortController|null(null);constdoFetchasync(){if(abortRef.current)abortRef.current.abort();constcontrollernewAbortController();abortRef.currentcontroller;constresawaitfetch(url,{signal:controller.signal});if(controller.signal.aborted)return;// 过时响应不处理setData(res);};// 卸载时取消useEffect(()()abortRef.current?.abort(),[]);