fix(waybill-manage): 修复运单详情弹窗残留问题
- 页面形态状态(列表/独立表单页/独立详情页)在实例创建时锁定,避免随路由变化切换导致弹窗残留 - 模板中形态相关所有判定改为使用锁定状态变量,保证渲染一致性 - 修改 detailContainer、crudContainer、pageFormOption 等方法使用锁定状态,避免缓存实例 option 变形 - 新增 closeInnerDialogs 方法,在组件 deactivated 和 beforeUnmount 生命周期关闭所有 append-to-body 弹窗 - 解决标签页 keep-alive 机制下弹窗 Teleport 持续遗留导致界面异常的问题 - 对运单管理页面多处弹窗和视图控制做统一修复,提升页面稳定性和体验
This commit is contained in:
@@ -21,4 +21,20 @@
|
|||||||
|
|
||||||
**遗留同类风险(未改,已告知用户)**:`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 翻转。
|
**遗留同类风险(未改,已告知用户)**:`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。
|
**未提交**:上述改动 + 上一轮 loading-manage 独立页改造均未 commit。
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
- **根因**:`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**,于是弹窗永久残留。
|
- **根因**:`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;列表页分支另写或只保留弹窗一种形态。
|
- **正确写法**:独立页固定 `<div v-if="isXxxPage">` 普通容器,绝不按路由回退成 el-dialog;列表页分支另写或只保留弹窗一种形态。
|
||||||
- **兜底**:页面内其它 `append-to-body` 的二级弹窗,加 `deactivated(){closeInnerDialogs()}` + `beforeUnmount(){closeInnerDialogs()}` 把 v-model 置 false。
|
- **兜底**:页面内其它 `append-to-body` 的二级弹窗,加 `deactivated(){closeInnerDialogs()}` + `beforeUnmount(){closeInnerDialogs()}` 把 v-model 置 false。
|
||||||
- 同类风险未修:`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 翻转。
|
- 已按此修法落地:loading-manage、project-apply(2026-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`)。
|
||||||
|
|
||||||
## 样式硬规则
|
## 样式硬规则
|
||||||
- 底栏:`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()` 要包住整个选择器。
|
||||||
|
|||||||
@@ -3,21 +3,21 @@
|
|||||||
:class="[
|
:class="[
|
||||||
'waybill-manage-page',
|
'waybill-manage-page',
|
||||||
{
|
{
|
||||||
'waybill-manage-page--form-page': isStandaloneWaybillFormPage,
|
'waybill-manage-page--form-page': formPageLocked,
|
||||||
'waybill-manage-page--detail-page': isStandaloneWaybillDetailPage,
|
'waybill-manage-page--detail-page': detailPageLocked,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
v-if="!isStandaloneWaybillDetailPage"
|
v-if="!detailPageLocked"
|
||||||
:is="crudContainer"
|
:is="crudContainer"
|
||||||
:option="pageFormOption"
|
:option="pageFormOption"
|
||||||
:form-page-title="isStandaloneWaybillFormPage ? formPageTitle : undefined"
|
:form-page-title="formPageLocked ? formPageTitle : undefined"
|
||||||
:table-loading="loading"
|
:table-loading="loading"
|
||||||
:data="data"
|
:data="data"
|
||||||
v-model:page="page"
|
v-model:page="page"
|
||||||
v-model="form"
|
v-model="form"
|
||||||
:status="isStandaloneWaybillFormPage ? dialogReadonly : undefined"
|
:status="formPageLocked ? dialogReadonly : undefined"
|
||||||
ref="crud"
|
ref="crud"
|
||||||
:permission="permissionList"
|
:permission="permissionList"
|
||||||
:before-open="beforeOpen"
|
:before-open="beforeOpen"
|
||||||
@@ -1546,7 +1546,7 @@
|
|||||||
</el-link>
|
</el-link>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isStandaloneWaybillFormPage"
|
v-if="formPageLocked"
|
||||||
class="waybill-manage-page__standalone-menu-actions"
|
class="waybill-manage-page__standalone-menu-actions"
|
||||||
>
|
>
|
||||||
<el-button class="waybill-manage-page__form-close" @click="closeCrudDialog">
|
<el-button class="waybill-manage-page__form-close" @click="closeCrudDialog">
|
||||||
@@ -1747,7 +1747,7 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<empty-pagination
|
<empty-pagination
|
||||||
v-show="!isStandaloneWaybillFormPage && !isStandaloneWaybillDetailPage"
|
v-show="!formPageLocked && !detailPageLocked"
|
||||||
:page="page"
|
:page="page"
|
||||||
@size-change="sizeChange"
|
@size-change="sizeChange"
|
||||||
@current-change="currentChange"
|
@current-change="currentChange"
|
||||||
@@ -1758,18 +1758,18 @@
|
|||||||
:is="detailContainer"
|
:is="detailContainer"
|
||||||
v-model="detailBox"
|
v-model="detailBox"
|
||||||
:title="`${config.title}详情`"
|
:title="`${config.title}详情`"
|
||||||
:append-to-body="!isStandaloneWaybillDetailPage"
|
:append-to-body="!detailPageLocked"
|
||||||
top="10px"
|
top="10px"
|
||||||
width="96%"
|
width="96%"
|
||||||
show-close
|
show-close
|
||||||
:class="[
|
:class="[
|
||||||
'waybill-manage-page__detail-dialog',
|
'waybill-manage-page__detail-dialog',
|
||||||
{ 'waybill-manage-page__detail-page': isStandaloneWaybillDetailPage },
|
{ 'waybill-manage-page__detail-page': detailPageLocked },
|
||||||
$attrs.option?.dialogCustomClass,
|
$attrs.option?.dialogCustomClass,
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<div v-loading="detailLoading" class="waybill-manage-page__detail-content">
|
<div v-loading="detailLoading" class="waybill-manage-page__detail-content">
|
||||||
<template v-if="detailBox || isStandaloneWaybillDetailPage">
|
<template v-if="detailBox || detailPageLocked">
|
||||||
<section-card class="waybill-manage-page__waybill-detail-summary">
|
<section-card class="waybill-manage-page__waybill-detail-summary">
|
||||||
<div class="waybill-manage-page__waybill-heading">
|
<div class="waybill-manage-page__waybill-heading">
|
||||||
<strong>运单详情</strong>
|
<strong>运单详情</strong>
|
||||||
@@ -2223,10 +2223,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isStandaloneWaybillDetailPage" class="waybill-manage-page__detail-footer">
|
<div v-if="detailPageLocked" class="waybill-manage-page__detail-footer">
|
||||||
<el-button type="primary" @click="closeDetail">关闭</el-button>
|
<el-button type="primary" @click="closeDetail">关闭</el-button>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="!isStandaloneWaybillDetailPage" #footer>
|
<template v-if="!detailPageLocked" #footer>
|
||||||
<el-button type="primary" @click="closeDetail">关闭</el-button>
|
<el-button type="primary" @click="closeDetail">关闭</el-button>
|
||||||
</template>
|
</template>
|
||||||
</component>
|
</component>
|
||||||
@@ -3518,9 +3518,21 @@ export default {
|
|||||||
selectionList: [],
|
selectionList: [],
|
||||||
attachmentUploadMode: false,
|
attachmentUploadMode: false,
|
||||||
standaloneFormKey: '',
|
standaloneFormKey: '',
|
||||||
|
// 页面形态(列表 / 独立表单页 / 独立详情页)在实例创建时锁定一次,之后不再随 $route 变化。
|
||||||
|
// 原因:标签页 keep-alive 只会让实例 deactivate,实例仍会随 $route(全局响应式)重新渲染;
|
||||||
|
// 若形态跟着路由翻回列表/弹窗态,缓存实例会渲染出 append-to-body 的 el-dialog —— 弹窗被
|
||||||
|
// Teleport 到 body 后,KeepAlive.deactivate 的 move 搬不动它,DOM 会永久残留在页面上,
|
||||||
|
// 表现为“从运单详情/独立表单离开后点其它菜单,详情弹窗又冒出来”。
|
||||||
|
formPageLocked: false,
|
||||||
|
detailPageLocked: false,
|
||||||
|
// 同上:表单模式(add / edit)也一次性锁定,避免缓存实例随 $route 变 query 导致 option 变形
|
||||||
|
formModeLocked: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.formPageLocked = this.isStandaloneWaybillFormPage;
|
||||||
|
this.detailPageLocked = this.isStandaloneWaybillDetailPage;
|
||||||
|
this.formModeLocked = this.$route.query.mode || '';
|
||||||
if (this.config.enableAllDept && this.isAdmin) {
|
if (this.config.enableAllDept && this.isAdmin) {
|
||||||
this.allDept = 1;
|
this.allDept = 1;
|
||||||
}
|
}
|
||||||
@@ -3544,6 +3556,14 @@ export default {
|
|||||||
this.consumeTemplateCreatePayload();
|
this.consumeTemplateCreatePayload();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// 标签切走(实例被 keep-alive 缓存)或销毁时,收起本页所有 append-to-body 的弹窗。
|
||||||
|
// 否则 Teleport 出去的弹窗 DOM 会一直留在 body 上,盖住后续打开的页面。
|
||||||
|
deactivated() {
|
||||||
|
this.closeInnerDialogs();
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.closeInnerDialogs();
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['permission', 'userInfo']),
|
...mapGetters(['permission', 'userInfo']),
|
||||||
permissionList() {
|
permissionList() {
|
||||||
@@ -3579,8 +3599,9 @@ export default {
|
|||||||
isStandaloneWaybillDetailPage() {
|
isStandaloneWaybillDetailPage() {
|
||||||
return this.standaloneDetailPage && this.$route.path === standaloneWaybillDetailRoute;
|
return this.standaloneDetailPage && this.$route.path === standaloneWaybillDetailRoute;
|
||||||
},
|
},
|
||||||
|
// 容器形态读实例创建时锁定的标志,不随 $route 翻转(详见 data 中 formPageLocked 的说明)
|
||||||
detailContainer() {
|
detailContainer() {
|
||||||
return this.isStandaloneWaybillDetailPage ? 'PageDetail' : 'el-dialog';
|
return this.detailPageLocked ? 'PageDetail' : 'el-dialog';
|
||||||
},
|
},
|
||||||
formPageTitle() {
|
formPageTitle() {
|
||||||
if (this.isStandaloneWaybillFormPage) {
|
if (this.isStandaloneWaybillFormPage) {
|
||||||
@@ -3592,13 +3613,13 @@ export default {
|
|||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
crudContainer() {
|
crudContainer() {
|
||||||
return this.isStandaloneWaybillFormPage ? 'PageAvueForm' : 'avue-crud';
|
return this.formPageLocked ? 'PageAvueForm' : 'avue-crud';
|
||||||
},
|
},
|
||||||
pageFormOption() {
|
pageFormOption() {
|
||||||
if (!this.isStandaloneWaybillFormPage) return this.option;
|
if (!this.formPageLocked) return this.option;
|
||||||
const base = {
|
const base = {
|
||||||
...this.option,
|
...this.option,
|
||||||
boxType: this.$route.query.mode === 'edit' ? 'edit' : 'add',
|
boxType: this.formModeLocked === 'edit' ? 'edit' : 'add',
|
||||||
menuBtn: true,
|
menuBtn: true,
|
||||||
submitBtn: true,
|
submitBtn: true,
|
||||||
emptyBtn: false,
|
emptyBtn: false,
|
||||||
@@ -3862,6 +3883,25 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 页面被 keep-alive 缓存/卸载时调用,关闭本页所有 append-to-body 的弹窗,
|
||||||
|
// 避免 Teleport 出去的 DOM 残留在 body 上盖住后续页面(详见 data 里的说明)。
|
||||||
|
closeInnerDialogs() {
|
||||||
|
this.detailBox = false;
|
||||||
|
this.mileageDialog.visible = false;
|
||||||
|
this.attachmentDocumentPreviewVisible = false;
|
||||||
|
this.attachmentImagePreviewVisible = false;
|
||||||
|
this.waybillRouteChangeBox = false;
|
||||||
|
this.waybillCommonAddressBox = false;
|
||||||
|
this.flowBox = false;
|
||||||
|
this.transportRouteBox = false;
|
||||||
|
this.transportAddressBox = false;
|
||||||
|
this.transportStationBox = false;
|
||||||
|
this.transportMapBox = false;
|
||||||
|
this.commonCargoBox = false;
|
||||||
|
this.cargoImportBox = false;
|
||||||
|
this.waybillProcessConfigBox = false;
|
||||||
|
this.excelBox = false;
|
||||||
|
},
|
||||||
initStandaloneFormPage() {
|
initStandaloneFormPage() {
|
||||||
if (!this.isStandaloneWaybillFormPage) return;
|
if (!this.isStandaloneWaybillFormPage) return;
|
||||||
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
||||||
|
|||||||
Reference in New Issue
Block a user