From ff9fb9168b6aff64b2453efd104cbbd1d5816b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Fri, 18 Sep 2026 12:22:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(router):=20=E4=BF=AE=E5=A4=8D=E8=BF=90?= =?UTF-8?q?=E5=8D=95=E7=AE=A1=E7=90=86=E8=AF=A6=E6=83=85=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E5=8F=8A=E5=90=88=E5=90=8C=E8=AF=A6=E6=83=85=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E5=AE=9A=E4=B9=89=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 waybill-manage-page、transport-plan-page 等组件增加 routePathLocked 变量锁定路由路径 - 添加 isOwnedRouteActive 计算属性,防止缓存实例错误读取其它路由的 detailId 导致误请求 - watcher 和 openDetail 方法中添加路由守卫,避免多余的请求和误开详情标签 - 修改宿主视图绑定 detailId,避免直接绑定全局 $route.query 导致缓存污染 - 删除 router/views/index.js 中合同管理详情路由重复定义,防止内容区空白问题 - 优化 waybill-import-dialog 组件样式和结构,增加 section-card 包裹表单区域 - 调整相关组件 data、computed、watch 使用,确保第一次立即 watcher 正确读取路由数据 - 验证所有改动后相关页面 HTTP 200,标签页行为正常,解决「运单管理不存在」和合同详情空白问题 --- .workbuddy/memory/2026-09-18.md | 56 +++++++++++++++++++ .workbuddy/memory/MEMORY.md | 41 ++++++-------- src/router/views/index.js | 12 ---- .../components/transport-plan-page.vue | 12 ++++ .../components/waybill-import-dialog.vue | 18 ++++-- .../components/waybill-manage-page.vue | 18 ++++++ src/views/business/transport-plan.vue | 11 +++- src/views/business/waybill-manage-detail.vue | 15 ++++- src/views/business/waybill-manage.vue | 11 +++- 9 files changed, 150 insertions(+), 44 deletions(-) diff --git a/.workbuddy/memory/2026-09-18.md b/.workbuddy/memory/2026-09-18.md index b6ad131..d7645e5 100644 --- a/.workbuddy/memory/2026-09-18.md +++ b/.workbuddy/memory/2026-09-18.md @@ -64,3 +64,59 @@ 2. `openDetail()` 的 push 分支加同样守卫(防其它调用路径)。 3. 宿主 view 绑定加路径守卫(`waybill-manage-detail.vue` / `waybill-manage.vue` / `transport-plan.vue`)双保险。 4. 可选:`getWaybillDetail` 支持静默模式,供 `restoreWaybillRows` 用,避免历史脏数据刷屏。 + +## 修复(已执行):「运单管理不存在」误报 + 误开标签 + +用户确认后按上方案落地,5 个文件: + +1. `src/views/business/components/waybill-manage-page.vue` + - `data()` 新增 `routePathLocked: this.$route.path` —— **必须放 data,不能放 created**:Vue Options API 顺序是 data → computed → watch(immediate) → created(已在 runtime-core `applyOptions` 源码中核实)。放 created 会让首个 immediate watcher 读到空值。 + - 新增 computed `isOwnedRouteActive() { return this.$route.path === this.routePathLocked; }` + - `detailId` watcher 首行加 `if (!this.isOwnedRouteActive) return;` + - `openDetail` 的 `$router.push` 分支同样加守卫。 +2. `src/views/business/components/transport-plan-page.vue`:完全同构的写法(`:detail-id` host 一致),加同样的 `routePathLocked` + `isOwnedRouteActive` + watcher 守卫。 +3. `src/views/business/waybill-manage-detail.vue`:`:detail-id="$route.query.id"` → `:detail-id="detailId"`,computed 做 `this.$route.path === '/business/waybill-manage/detail'` 守卫。 +4. `src/views/business/waybill-manage.vue`:同上,守卫 `/business/waybill-manage`(保留旧链接 `?detailId=` 兼容)。 +5. `src/views/business/transport-plan.vue`:同上,守卫 `/business/transport-plan`。 + +**验证**: +- dev server curl 5 个 .vue + 2 个 scoped style 全部 HTTP 200。 +- 用 `@vue/server-renderer` 写了临时 mjs(已删)模拟带 `$route` 的 app,确认 **data() 里访问 `this.$route` 可行**:immediate watcher 触发时 `routePathLocked` 已是正确路径、首次加载不被拦截。 +- `$route` 是 vue-router 挂在 `app.config.globalProperties` 上的 getter(vue-router.mjs:1498),组件实例化时已就绪。 + +**未做(第 4 条可选)**:给 `getWaybillDetail` 加静默模式供 `restoreWaybillRows` 使用(历史失效运单仍会刷多条红字),待用户定夺。 + +**另注**:`business-crud-page.vue:6651` 有同构的 `detailId` immediate watcher,但全项目只有上述 3 个宿主 view 传 `:detail-id`,它暂无调用方,未动。 + +**仍未 commit。** + +## 修复(已执行):合同详情页整页空白 —— 路由表重复定义 + +**现象**:主订单详情点合同编号 → `/business/contract-manage/detail?id=...&name=合同详情` 内容区空白(侧栏/标签正常)。 + +**定位过程(关键:真实浏览器复现,不猜)** +1. 静态排查全部排除:`contract-manage.vue` 及其 26 个依赖 HTTP 200、模块 `import()` 成功、无循环依赖、详情分支引用的 24 个成员全部有定义。 +2. dev 环境登录态无法直接拿(验证码 + `admin/admin` 报 `error_code 2004 用户密码强度过低`),改为**注入式复现**: + - `agent-browser open <目标 URL>`(落 `/login`); + - eval 取 `document.getElementById('app').__vue_app__.config.globalProperties` 拿 `$router` / `$store`; + - 写 cookie `saber3-access-token`(注意 token 走 **js-cookie**,不是 localStorage)+ `$store.state.user.token`; + - 覆写 `XMLHttpRequest.prototype.open/send` 让所有 `/api/**` 返回 `{code:200,data:{...}}`(defineProperty 伪造 readyState/status/responseText 后手动触发 `onloadend`)。 + - 再 `$router.push(...)` 做 SPA 跳转。 +3. 复现出空白,并拿到决定性证据: + - `/business/contract-manage/detail` → `$route.matched.length === 1`(只有 Layout,name undefined),`#avue-view.children.length === 0`,innerHTML 为 ``。 + - 对照 `/business/contract-manage/change`(结构相同但未重复定义)→ `matched.length === 2`(Layout + 合同变更),内容完整渲染。 + - `$router.getRoutes()` 里 detail 有 **3 条**记录:2 个 Layout 父记录 + 1 个子记录(`合同详情`)。 + +**根因**:`src/router/views/index.js` 中 `/business/contract-manage/detail` **被复制粘贴定义了两次**,两次的子路由都叫 `合同详情`。vue-router 的 `addRoute` 遇到重名会 `removeRoute(name)` 把先注册的子记录删掉,但**不会把它从父记录的 `children` 数组里摘除**;于是第一条 Layout 壳记录留在 matcher 列表里且排在前面,`resolve('/business/contract-manage/detail')` 命中这个"无有效子路由"的壳 → matched 只有 1 层 → `page/index/index.vue` 里 `#avue-view` 的二级 `` 无匹配 → 内容区空白。 +(`getRoutes()` 计数是判据:form / change 都是 2 条 = 父 + 子(健康),detail 是 3 条 = 2 父 + 1 子(坏)。) + +**修复**:删除重复块(原 314-325 行),只保留 159-169 行那一份。 + +**验证**: +- 重复扫描脚本(正则匹配「顶层 path + `component: Layout` + 首个子路由 name」):views/index.js 27 条 Layout 子路由中仅 `合同详情` 重复;page/index.js 无重复 → 已清零。 +- 修复后浏览器复验:detail → `matched.length === 2`(Layout + 合同详情)、`#avue-view.children.length === 1`,正文完整渲染「基本信息 / 签约类型 / 合同编号 HT-2026-001 / … / 变更记录」。 +- 全量回归:48 条 business/vehicle/payment/settlement 路由 `resolve()` 后 `matched.length` 全部 ≥ 2,`bad: []`。 +- `curl` 校验:`views/index.js`、`contract-manage.vue` 均 200。 +- 残留报错均为 mock 数据形状不符所致(所有接口都返回同一个合同对象,导致 `tree.reduce` / `feeCategories.find` 之类失败),非真实缺陷。 + +**未 commit**(连同 1、2 轮改动一起待确认)。 diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index 5e989b9..6be373c 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -1,34 +1,27 @@ # 项目长期记忆(tms-erp-web-ws / Saber3) -配套后端:`/Users/gxwebsoft/JAVA/tms-api`(不是 tms-erp-api-ws,后者功能滞后)。 +后端:`/Users/gxwebsoft/JAVA/tms-api`(`tms-erp-api-ws` 滞后,别看错仓库)。 -## 列表 + 独立表单页 + 独立详情页(同一组件分流) -- `/xxx`、`/xxx/form`、`/xxx/detail` 指向同一 .vue;form/detail 为 `component: Layout` + 空 children、`meta:{keepAlive:false}`,detail 带 `activeMenu:'/xxx'`。 -- 组件内按 `$route.path` 分流,列表部分 `v-if="!isStandalonePage"` 隐藏;路由不同但组件相同 → 必须 `watch:{$route}` 重新 init。 -- 跳转 `push({path:'/xxx/form',query:{mode,id,name}})`,标签标题取 `query.name`;只读详情底栏「关闭」,表单页「关闭/暂存/确认」。 +## 列表 / 表单 / 详情三态同组件分流 +`/xxx`、`/xxx/form`、`/xxx/detail` 指向同一 `.vue`;form/detail 在 `src/router/views/index.js` 是 `component: Layout` + 空 `children.path` + `meta:{keepAlive:false}`(detail 另带 `activeMenu`)。组件内按 `$route.path` 分流,必须 `watch:{$route}` 重新 init;标签标题取 `query.name`;独立页容器固定 `
`,不用 `el-dialog`。 -## ⚠️ keep-alive 缓存引起的两个同源坑(`tab.js` + `layout.vue`) -`tab.js` 按 fullPath 建 wrapper,`layout.vue` 用 `` 缓存,**每标签一实例且永不销毁**;实例 deactivate 后**仍会随全局 `$route` 重渲染**。由此: +## ⚠️ keep-alive 三坑 +背景(`router/tab.js` + `page/index/index.vue`):按 `fullPath` 建 wrapper,`` 缓存,**每标签一实例、永不销毁**,deactivate 后**仍随全局 `$route` 重渲染**。 -**坑一:独立页容器禁止用 `` 在 div 与 el-dialog 间切换** -- 机制:`:is` 由 div 翻回 `el-dialog` + `v-model` 仍 true + `append-to-body` → 弹窗 Teleport 进 body;`TeleportImpl.move` 忽略 `moveType`,**不会把 teleport 的 DOM 搬回 storage container** → 点左侧菜单又冒出旧弹窗。 -- 修法:独立页固定 `
`;另加 `deactivated(){closeInnerDialogs()}` + `beforeUnmount()` 收起页内 append-to-body 二级弹窗(子组件里写 `deactivated` 也有效)。 -- 已修:loading-manage、project-apply、waybill-manage-page(2026-09-18,后者用 `formPageLocked/detailPageLocked/formModeLocked` 锁形态)。未修:`settlement/components/{pre,formal}-settlement-editor.vue`、`transport-reconciliation-editor.vue`、`waybill-import-dialog.vue`。 +**坑一:独立页容器禁用 `` 在 div / el-dialog 间切换。** `append-to-body` 走 Teleport,`TeleportImpl.move` 忽略 `moveType`,不会把 DOM 搬回缓存容器 → 切菜单冒出旧弹窗。修法:固定 `
` + `deactivated(){closeInnerDialogs()}` + `beforeUnmount()`(写在子组件也生效)。已修 loading-manage / project-apply / waybill-manage-page;未修 `settlement/components/{pre,formal}-settlement-editor.vue`、`transport-reconciliation-editor.vue`、`waybill-import-dialog.vue`。 + +**坑二:宿主 view 直绑全局 `$route.query.*` 给子组件 prop。** 缓存实例被灌入当前页 id → 拿错 id 查后端(批量弹「运单管理不存在」),live computed 还会误 push 标签。修法:`routePathLocked` 锁路径 + watcher / push 前判 `isOwnedRouteActive` + 宿主绑定加路径守卫。⚠️ `routePathLocked` **必须放 `data()`**(Options API 顺序 data → computed → watch(immediate) → created;放 created 会读到空值、挡掉首次加载)。已修 waybill-manage-page / transport-plan-page + 三个宿主 view。 + +**坑三:路由表重复定义 → 内容区整页空白(2026-09-18)。** `/business/contract-manage/detail` 在 `router/views/index.js` 定义两次、子路由同名 `合同详情`。重名 `addRoute` 会 `removeRoute(name)` 移除旧子记录,但**不从父记录 children 里摘除**,第一条 Layout 壳记录留在 matcher 且靠前 → `resolve()` 命中它 → `$route.matched.length === 1` → 二级 router-view 无匹配 → 侧栏/标签正常但内容空白。修法:删重复定义。排查:扫「顶层 path + 首个子路由 name」重复项。 -**坑二:宿主 view 把全局 `$route.query.*` 直接绑给子组件 prop** -- 现象:打开 A 页详情却弹「运单管理不存在」这类**用错 id** 的后端报错(可重复多条),还莫名多出别的标签。 -- 机制:`waybill-manage-detail.vue` 写 `:detail-id="$route.query.id"` → 旧缓存实例把「当前页的 id」灌进来 → 触发 `openDetail` → 用别人的 id 查自己的接口 → 后端「XX 不存在」→ 全局 axios 拦截器红字弹出(几个旧标签就几条)。其 `isStandaloneXxxPage` 是 live computed(false) → 还会误 `push` 出新标签。 -- 修法:`created()` 锁 `routePathLocked = $route.path`,watcher/跳转方法先判 `if ($route.path !== routePathLocked) return;`;宿主 view 绑定也加路径守卫。 -- 同理:按 id 批量查详情处(如 `restoreWaybillRows`)虽有 `.catch`,但**拦截器已弹过提示**,catch 拦不住。 ## 样式硬规则 -- 底栏:`flex; justify-content:flex-end`、**不写 gap**(靠 EP 默认 `.el-button+.el-button{margin-left:12px}`)。浮动底栏必须 `position:fixed`(祖先 overflow:hidden 使 sticky 失效)+ `:global(.avue--collapse .x){left:60px}`、`:global(.avue-layout--horizontal .x){left:0}`,`:global()` 包整个选择器。 -- 按钮层级:次要=不写 type;中间步骤=`primary plain`;主操作=`primary`(禁绿色、禁两个蓝实心)。顺序 `[辅助][取消][保存草稿][提交]`。 +- 底栏 `flex; justify-content:flex-end`,**不写 gap**(靠 EP 默认 `.el-button+.el-button{margin-left:12px}`);浮动底栏必须 `position:fixed`(祖先 overflow:hidden 使 sticky 失效)+ `:global(.avue--collapse .x){left:60px}`、`:global(.avue-layout--horizontal .x){left:0}`,`:global()` 要包整个选择器。 +- 按钮层级:次要=无 type;中间步骤=`primary plain`;主操作=`primary`(禁绿、禁两个蓝实心)。顺序 `[辅助][取消][保存草稿][提交]`。 - 独立页标题 `.archive-page-form__title`;分组用全局 ``;上传证件区 `width:100%; max-width:240px` + `.el-upload{height:151px}`。 -- ⚠️ ` diff --git a/src/views/business/components/waybill-manage-page.vue b/src/views/business/components/waybill-manage-page.vue index 9d73d46..2f3576a 100644 --- a/src/views/business/components/waybill-manage-page.vue +++ b/src/views/business/components/waybill-manage-page.vue @@ -3518,6 +3518,11 @@ export default { selectionList: [], attachmentUploadMode: false, standaloneFormKey: '', + // 实例所属路由的路径快照:用于判断当前 $route 是否还是「本实例自己的路由」。 + // 必须在 data 里初始化,不能放 created —— Vue 的 Options API 执行顺序是 + // data → computed → watch(immediate) → created,放 created 会被首个 + // immediate watcher 读到空值,把本该执行的首次加载也挡掉。 + routePathLocked: this.$route.path, // 页面形态(列表 / 独立表单页 / 独立详情页)在实例创建时锁定一次,之后不再随 $route 变化。 // 原因:标签页 keep-alive 只会让实例 deactivate,实例仍会随 $route(全局响应式)重新渲染; // 若形态跟着路由翻回列表/弹窗态,缓存实例会渲染出 append-to-body 的 el-dialog —— 弹窗被 @@ -3600,6 +3605,12 @@ export default { return this.standaloneDetailPage && this.$route.path === standaloneWaybillDetailRoute; }, // 容器形态读实例创建时锁定的标志,不随 $route 翻转(详见 data 中 formPageLocked 的说明) + // 当前路由是否仍属于本实例(tab.js 按 fullPath 建实例,一个实例只对应一个 path)。 + // 被 keep-alive 缓存后如果 $route 已经跑到别的页面上,就不该再用「当前页面的 id」 + // 去触发本实例的请求或跳转,否则会拿别的单据 id 查自己的接口。 + isOwnedRouteActive() { + return this.$route.path === this.routePathLocked; + }, detailContainer() { return this.detailPageLocked ? 'PageDetail' : 'el-dialog'; }, @@ -3867,6 +3878,10 @@ export default { detailId: { immediate: true, handler(id) { + // 宿主 view 传的是全局 $route.query.id / detailId,实例被缓存后仍会被重渲染, + // 必须确认当前路由还是自己的:否则跳到别的详情页时会把「别的单据 id」灌进来, + // 触发 openDetail 用错 id 查运单 → 报「运单管理不存在」。 + if (!this.isOwnedRouteActive) return; if (id !== undefined && id !== null && id !== '') { this.$nextTick(() => this.openDetail({ id })); } @@ -4449,6 +4464,9 @@ export default { }, openDetail(row) { if (!this.isStandaloneWaybillDetailPage) { + // 已不在自己的路由上(被缓存后路由切走):此时 row.id 极可能是别的单据的 id, + // 再 push 会生成一堆错误的「运单管理详情」标签 + if (!this.isOwnedRouteActive) return; this.$router.push({ path: standaloneWaybillDetailRoute, query: { id: row.id } }); return; } diff --git a/src/views/business/transport-plan.vue b/src/views/business/transport-plan.vue index b43cad7..4c5e770 100644 --- a/src/views/business/transport-plan.vue +++ b/src/views/business/transport-plan.vue @@ -1,9 +1,11 @@