Files
tms-erp-web/.workbuddy/memory/2026-09-18.md
T
gxwebsoft ff9fb9168b fix(router): 修复运单管理详情路由及合同详情路由重复定义问题
- 为 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,标签页行为正常,解决「运单管理不存在」和合同详情空白问题
2026-09-18 12:22:25 +08:00

123 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 2026-09-18
## 修复:改成独立整页后点左侧菜单仍弹出旧弹窗(loading-manage / project-apply
**现象**:配载单管理、项目管理已改为「新增/编辑/详情走独立整页」,但从独立页返回或直接点左侧菜单时,页面上仍会盖出一个老的 `el-dialog`(新增项目管理 / 配载单)。
**根因**(已读 Vue runtime-core 源码确认):
- `src/router/tab.js``tabKey`(默认 fullPath)为每个标签建一个具名 wrapper 组件放进 `wrapperMap``src/page/index/layout.vue``<keep-alive :include="$store.getters.tagsKeep">` 缓存 —— 每个标签页一个实例,全部缓存不销毁。
- 组件被 deactivate 后**仍会随 `$route`(全局响应式)重新渲染**;此时 `isStandalonePage` 变 false`<component :is="isStandalonePage ? 'div' : 'el-dialog'">` 由 div 翻回 `el-dialog`,而 `v-model``projectBox`/`dialogVisible`)仍是 true`append-to-body` 使弹窗 Teleport 到 body。
- `KeepAlive.deactivate``move(vnode, storageContainer, ..., moveType=OUT)`,但 `TeleportImpl.move``moveTeleport`)忽略传入的 moveType、一律按 REORDER 处理 → **teleport 出去的 DOM 不会被搬回 storage container**,永久残留在 `document.body` 上并显示出来。
**修法**(不回退 tab.js 的 `return wrapper` 提交 c1c4fa1,改修根因):
- 独立页容器固定为 `<div v-if="isXxxPage">`,不再按路由回退成 `el-dialog`
- 页内其它 `append-to-body` 二级弹窗加 `deactivated()` / `beforeUnmount()``closeInnerDialogs()` 把 v-model 置 false。
**改动文件**
- `src/views/business/loading-manage.vue`:容器改 `<div v-if="isStandalonePage">`;删 `dialogVisible``dialogTitle``resetLoadingDialog``clearLoadingDialog`、页脚弹窗分支(含「清空」);`watch.$route` 简化;新增 `closeInnerDialogs()`routeChange / commonAddress / candidateSearch+ `deactivated`/`beforeUnmount`
- `src/views/business/project-apply.vue`:容器改 `<div v-if="isProjectFormPage">`;删 `projectFormContainer`/`projectFormContainerProps``projectBox``resetProjectDialog``handleCancelProject``closeProjectForm` 简化为直接 push 列表;新增 `closeInnerDialogs()`changeRecordDetail / 附件文档预览 / 图片预览 / 选人)+ `deactivated`/`beforeUnmount`
**校验**dev server(2889) curl 两个 .vue 及其 scoped style 模块,均 HTTP 200500 才是编译失败)。`vite build` 因沙箱敏感内容保护会在 facelogin.vue 中断,未用。
**遗留同类风险(未改,已告知用户)**`business/components/waybill-manage-page.vue``detailContainer` 在 PageDetail / el-dialog 间切)、`settlement/components/{pre,formal}-settlement-editor.vue``transport-reconciliation-editor.vue``pageMode ? 'div':'el-dialog'`)、`business/components/waybill-import-dialog.vue``standalone`/`createPage`)。统一修法:容器类型在 `created()` 锁定,不随路由/prop 翻转。
---
## 追加修复:运单详情弹窗残留(waybill-manage-page.vue
用户复现路径:打开 `/business/waybill-manage/detail?id=xxx` → 点「配载管理」菜单 → 页面上又冒出运单详情弹窗。与上面同一根因:`detailContainer()``isStandaloneWaybillDetailPage` 在 PageDetail / el-dialog 间翻转,`:append-to-body="!isStandaloneWaybillDetailPage"` 随之为 true`v-model="detailBox"` 仍 true → Teleport 进 body 搬不回来。
**改动**`src/views/business/components/waybill-manage-page.vue`):
- data 新增三个锁定标志 `formPageLocked` / `detailPageLocked` / `formModeLocked`,在 `created()` 一次性从 `isStandaloneWaybillFormPage` / `isStandaloneWaybillDetailPage` / `$route.query.mode` 取值。
- 模板里所有形态判定(根 class、`v-if="!detailPageLocked"` 的列表容器、`form-page-title``status`、独立表单页按钮块、分页 `v-show``:append-to-body`、detail-page class、内容 `v-if="detailBox || detailPageLocked"`、独立页 footer / 弹窗 footer slot)全部改读锁定标志。
- `detailContainer()` / `crudContainer()` / `pageFormOption()`(含 `boxType`)改读锁定标志,避免缓存实例的 option 变形。
- 新增 `closeInnerDialogs()`(关 detailBox、里程补录、附件文档/图片预览、变更路线、常用地址、流程图、运输路线/地址/站点/地图、常用货物、货物导入、过程配置、Excel 导入)+ `deactivated()` / `beforeUnmount()`
**关键验证**`onDeactivated` 在子组件里也会触发 —— apiLifecycle 的 `injectHook` 会向上遍历父链,发现 KeepAlive 父级就把钩子注入到根实例(runtime-core 的 `injectToKeepAliveRoot`),所以写在子组件 waybill-manage-page 上的 `deactivated` 有效。
**校验**dev server curl 该 .vue 与 scoped style 均 200。仍未 commit。
**未提交**:上述改动 + 上一轮 loading-manage 独立页改造均未 commit。
---
## 排查(未改码):配载单详情页反复弹「运单管理不存在」
用户截图:打开 `/business/loading-manage/detail?id=2097607162718441474&name=配载单详情`,连续弹出 6-7 条红色「运单管理不存在」,标签栏堆了一串「运单管理详情」。
**文案来源**:前端 src 里没有这句话,是后端 `BusinessException` 的 msg,被 `src/axios.js` 的响应拦截器(`status !== 200``ElMessage.error(message)`)统一弹出。
**主因(通道 A**`views/business/waybill-manage-detail.vue` 只有 23 行,写了 `:detail-id="$route.query.id"` —— 直接绑全局 `$route`**没有路径守卫**。`waybill-manage-page.vue``detailId` watcher 是 `immediate` 且对任何非空 id 都 `openDetail({id})`。于是:
1. 之前打开过的每个运单详情标签(`/business/waybill-manage/detail?id=A``?id=B`…)各是一个被 keep-alive 缓存的独立实例;
2. 跳到配载单详情后,这些实例被 deactivate 但仍随 `$route` 重渲染 → `$route.query.id` 变成配载单 id
3. prop 变化 → `openDetail({id: 配载单id})``getDetail(配载单id)` 去查运单 → 后端「运单管理不存在」→ 拦截器弹一次。**几个旧标签就几条提示**;
4. 且被污染实例的 `isStandaloneWaybillDetailPage` 仍是 live computed(false) → `openDetail``$router.push` 分支 → 又生成新的「运单管理详情」标签(标签栏那串的来源)。
同理风险:`waybill-manage.vue:6``transport-plan.vue:6``:detail-id="$route.query.detailId"``business-crud-page.vue:7356` 会 push `/business/loading-manage?detailId=id`,正好会污染运单列表实例)。
**次因(通道 B**`loading-manage.vue``restoreWaybillRows` 会用 `waybillIdsJson` 里的每个运单 id 并发 `getWaybillDetail(id)`,若历史运单已被删/失效,也会报同一句文案且并发多条。单条 `.catch(() => null)` 挡不住拦截器已弹出的提示。
**区分方法**Network 里看报错请求的 `id` —— 等于 `2097607162718441474`(配载单 id)→ 通道 A;是别的数字(运单 id)→ 通道 B。或先关掉所有「运单管理详情」标签再复现,通道 A 会消失。
**待确认的修法**(用户要求先报不改):
1. `waybill-manage-page.vue``created()``routePathLocked = this.$route.path``detailId` watcher 首行加 `if (this.$route.path !== this.routePathLocked) return;`
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` 上的 gettervue-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`(只有 Layoutname 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` 的二级 `<router-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 轮改动一起待确认)。