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

13 KiB
Raw Blame History

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 200(500 才是编译失败)。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 上的 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 的二级 <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 轮改动一起待确认)。