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,标签页行为正常,解决「运单管理不存在」和合同详情空白问题
This commit is contained in:
2026-09-18 12:22:25 +08:00
parent 986feff7b7
commit ff9fb9168b
9 changed files with 150 additions and 44 deletions
+17 -24
View File
@@ -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` 指向同一 .vueform/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`;独立页容器固定 `<div v-if>`,不用 `el-dialog`
## ⚠️ keep-alive 缓存引起的两个同源坑(`tab.js` + `layout.vue`
`tab.js`fullPath 建 wrapper`layout.vue``<keep-alive :include="tagsKeep">` 缓存,**每标签一实例永不销毁**;实例 deactivate 后**仍随全局 `$route` 重渲染**。由此:
## ⚠️ keep-alive 三坑
背景(`router/tab.js` + `page/index/index.vue`):按 `fullPath` 建 wrapper`<keep-alive :include="tagsKeep">` 缓存,**每标签一实例永不销毁**deactivate 后**仍随全局 `$route` 重渲染**。
**坑一:独立页容器禁用 `<component :is>` 在 div el-dialog 间切换**
- 机制:`:is` 由 div 翻回 `el-dialog` + `v-model` 仍 true + `append-to-body` → 弹窗 Teleport 进 body`TeleportImpl.move` 忽略 `moveType`**不会把 teleport 的 DOM 搬回 storage container** → 点左侧菜单又冒出旧弹窗。
- 修法:独立页固定 `<div v-if="isXxxPage">`;另加 `deactivated(){closeInnerDialogs()}` + `beforeUnmount()` 收起页内 append-to-body 二级弹窗(子组件里写 `deactivated` 也有效)
- 已修:loading-manage、project-apply、waybill-manage-page2026-09-18,后者用 `formPageLocked/detailPageLocked/formModeLocked` 锁形态)。未修:`settlement/components/{pre,formal}-settlement-editor.vue``transport-reconciliation-editor.vue``waybill-import-dialog.vue`
**坑一:独立页容器禁用 `<component :is>` 在 div / el-dialog 间切换** `append-to-body` 走 Teleport`TeleportImpl.move` 忽略 `moveType`,不会把 DOM 搬回缓存容器 → 切菜单冒出旧弹窗。修法:固定 `<div v-if>` + `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`;分组用全局 `<section-card>`;上传证件区 `width:100%; max-width:240px` + `.el-upload{height:151px}`
- ⚠️ `<style scoped lang="scss">` **顶层禁 `//` 注释**Vite5+sass `Unexpected '/'` 样式模块 500),顶层用 `/* */`
- 其余细则见 `src/styles/element-ui.scss` 与 AGENTS.md 4.4。
- ⚠️ `<style scoped lang="scss">` 顶层禁 `//` 注释(Vite5 + sass `Unexpected '/'` → 500),顶层用 `/* */`其余见 `src/styles/element-ui.scss``AGENTS.md` 4.4。
## 表单文案 / 自检 / 导出
- placeholder 只写 `请输入`/`请选择`;例外:`请输入或选择车辆`、日期区间 `起/止`、示例值。校验 message 须带字段名。
## 表单 / 自检 / 导出
- placeholder 只写 `请输入`/`请选择`;例外:`请输入或选择车辆`、日期区间 `起`/`止`、示例值。校验 message 须带字段名。
- `business-crud-page.vue`option 经 `cloneOption` 克隆,独立表单页走 `PageAvueForm`
- 自检:dev(2889) 时 `curl "localhost:2889/src/xxx.vue"``"...?vue&type=style&index=0&scoped=true&lang.scss"`200 通过、500 报错`vite build` `facelogin.vue` 被沙箱中断,不能作唯一校验。dev`VITE_APP_API=/api`代理 `172.16.203.228:8000`
- 导出:前端 `exportColumns` 后端未用,以 `XxxExportExcel.java` 为准;BladeX `BeanUtil` 类型不兼容静默跳过(`Date``LocalDateTime` 丢值, `DateUtil.fromDate(...)`)。
- 自检:`curl "localhost:2889/src/xxx.vue"``"...?vue&type=style&index=0&scoped=true&lang.scss"`200 / 500`vite build``facelogin.vue` 被沙箱中断,不能作唯一校验。dev `VITE_APP_API=/api` 代理 `172.16.203.228:8000`
- 导出:前端 `exportColumns` 后端未用,以 `XxxExportExcel.java` 为准;BladeX `BeanUtil` 类型不兼容静默跳过(`Date``LocalDateTime` 丢值, `DateUtil.fromDate`)。