fix(waybill-manage): 修复运单详情弹窗残留问题

- 页面形态状态(列表/独立表单页/独立详情页)在实例创建时锁定,避免随路由变化切换导致弹窗残留
- 模板中形态相关所有判定改为使用锁定状态变量,保证渲染一致性
- 修改 detailContainer、crudContainer、pageFormOption 等方法使用锁定状态,避免缓存实例 option 变形
- 新增 closeInnerDialogs 方法,在组件 deactivated 和 beforeUnmount 生命周期关闭所有 append-to-body 弹窗
- 解决标签页 keep-alive 机制下弹窗 Teleport 持续遗留导致界面异常的问题
- 对运单管理页面多处弹窗和视图控制做统一修复,提升页面稳定性和体验
This commit is contained in:
2026-09-18 11:29:52 +08:00
parent 3e7110ca8c
commit 986feff7b7
2 changed files with 49 additions and 29 deletions
+26
View File
@@ -38,3 +38,29 @@
**校验**dev server curl 该 .vue 与 scoped style 均 200。仍未 commit。 **校验**dev server curl 该 .vue 与 scoped style 均 200。仍未 commit。
**未提交**:上述改动 + 上一轮 loading-manage 独立页改造均未 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` 用,避免历史脏数据刷屏。
+23 -29
View File
@@ -3,38 +3,32 @@
配套后端:`/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'` - `/xxx``/xxx/form``/xxx/detail` 指向同一 .vueform/detail 为 `component: Layout` + 空 children`meta:{keepAlive:false}`detail 带 `activeMenu:'/xxx'`
- 组件内按 `$route.path` 分流,列表部分统一 `v-if="!isStandalonePage"` 隐藏。 - 组件内按 `$route.path` 分流,列表部分 `v-if="!isStandalonePage"` 隐藏;路由不同但组件相同 → 必须 `watch:{$route}` 重新 init
- 路由记录不同但组件相同 → 必须 `watch:{$route}` 里重新 init`mounted` 只覆盖直接打开/刷新 - 跳转 `push({path:'/xxx/form',query:{mode,id,name}})`,标签标题取 `query.name`;只读详情底栏「关闭」,表单页「关闭/暂存/确认」
- 跳转 `push({path:'/xxx/form',query:{mode,id,name:'新增xxx'}})`,标签标题取 `query.name`
- 底栏:只读详情页「关闭」;表单页「关闭/暂存/确认」。
## ⚠️ 独立页容器禁止用 `<component :is>` 在 div 与 el-dialog 间切换 ## ⚠️ keep-alive 缓存引起的两个同源坑(`tab.js` + `layout.vue`
- **现象**:改成独立整页后,点左侧菜单又弹出旧弹窗,且永久盖在页面上。 `tab.js` 按 fullPath 建 wrapper`layout.vue``<keep-alive :include="tagsKeep">` 缓存,**每标签一实例且永不销毁**;实例 deactivate 后**仍会随全局 `$route` 重渲染**。由此:
- **根因**`src/router/tab.js` 按 fullPath 建 wrapper 组件,`layout.vue``<keep-alive :include="tagsKeep">` 缓存,每个标签一个实例。实例被 deactivate 后**仍会随 `$route` 重新渲染**;此时 `:is` 由 div 变回 `el-dialog``v-model` 仍为 true`append-to-body` 的弹窗经 Teleport 渲染进 body——而 `KeepAlive.deactivate → Teleport.move` 只做 reorder**不会把 teleport 出去的 DOM 搬回 storage container**,于是弹窗永久残留。
- **正确写法**:独立页固定 `<div v-if="isXxxPage">` 普通容器,绝不按路由回退成 el-dialog;列表页分支另写或只保留弹窗一种形态。
- **兜底**:页面内其它 `append-to-body` 的二级弹窗,加 `deactivated(){closeInnerDialogs()}` + `beforeUnmount(){closeInnerDialogs()}` 把 v-model 置 false。
- 已按此修法落地:loading-manage、project-apply2026-09-18)、waybill-manage-page(同日,锁 detailPage/formPage/formMode 三个标志)。
- 未修同类风险:`settlement/components/{pre,formal}-settlement-editor.vue``transport-reconciliation-editor.vue``pageMode ? 'div':'el-dialog'`)、`waybill-import-dialog.vue``standalone`/`createPage`)。
**坑一:独立页容器禁止用 `<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`
**坑二:宿主 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()` 要包住整个选择器。 - 底栏:`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`(禁绿色、禁两个蓝实心)。顺序 `[辅助][取消][保存草稿][提交]` - 按钮层级:次要=不写 type;中间步骤=`primary plain`;主操作=`primary`(禁绿色、禁两个蓝实心)。顺序 `[辅助][取消][保存草稿][提交]`
- 独立页标题 `.archive-page-form__title`18px/600 + 4px 主色竖条);分组用全局 `<section-card>`弹窗灰底 `#f5f6fa` - 独立页标题 `.archive-page-form__title`;分组用全局 `<section-card>`上传证件区 `width:100%; max-width:240px` + `.el-upload{height:151px}`
- 上传证件区:`width:100%; max-width:240px` + `.el-upload{height:151px}`,禁写死 px。
- ⚠️ `<style scoped lang="scss">` **顶层禁 `//` 注释**Vite5+sass 报 `Unexpected '/'` → 样式模块 500),顶层用 `/* */` - ⚠️ `<style scoped lang="scss">` **顶层禁 `//` 注释**Vite5+sass 报 `Unexpected '/'` → 样式模块 500),顶层用 `/* */`
- 其余全站细则见 `src/styles/element-ui.scss` 与 AGENTS.md 4.4。 - 其余细则见 `src/styles/element-ui.scss` 与 AGENTS.md 4.4。
## 表单文案 ## 表单文案 / 自检 / 导出
- placeholder 只写 `请输入`/`请选择`;例外保留`请输入或选择车辆`、日期区间 `起/止`、示例值`如:京A12345`。校验 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`
- `business/components/business-crud-page.vue`option 经 `cloneOption` 克隆;独立表单页走 `PageAvueForm`;详情弹窗 `detailButton + detailSections` - 导出:前端 `exportColumns` 后端未用,以 `XxxExportExcel.java` 为准;BladeX `BeanUtil` 类型不兼容静默跳过(`Date``LocalDateTime` 丢值,须 `DateUtil.fromDate(...)`
- 自检:dev(2889) 时 `curl "localhost:2889/src/xxx.vue"``curl "...?vue&type=style&index=0&scoped=true&lang.scss"`,200 通过、500 返回带堆栈错误页。
- ⚠️ `vite build` 会在 `src/page/login/facelogin.vue` 被沙箱敏感内容保护中断(环境问题),不能作为唯一校验手段。
- dev`VITE_APP_API=/api`,代理 `172.16.203.228:8000`
## 导出(后端 FastExcel
- 前端 `exportColumns` 后端未使用,导出列以 `XxxExportExcel.java` 为准。
- BladeX `BeanUtil` 类型不兼容时静默跳过(`Date createTime``LocalDateTime` 丢值),须 Service 内 `DateUtil.fromDate(...)` 赋值。
- 排查导出文件用 Python `zipfile``xl/worksheets/sheet1.xml`