feat(contract-manage): 将合同详情由弹窗改为独立页面

- 新增路由 /business/contract-manage/detail,详情页共享合同管理组件
- 列表、表单、详情三分支用 isFormPage 和 isDetailPage 控制
- openDetail 方法改为跳转详情路由,initDetailPage 获取详情数据
- 新增 resetDetailState 和 closeDetailPage 方法,复用关闭逻辑
- 详情页去除弹窗高度限制,样式改为页面自滚动
- 详情页底栏使用固定位置关闭按钮,保持页面布局一致
- watch 和 created 中添加对 isDetailPage 的初始化调用和监听
This commit is contained in:
2026-09-11 12:30:05 +08:00
parent 6e5b15f203
commit 2131e99ec4
10 changed files with 189 additions and 90 deletions
+28
View File
@@ -0,0 +1,28 @@
# 2026-09-11
## 上传证件区域固定宽度导致跨机器错位(transportCapacity
- 现象:driver.vue 新增弹窗的身份证/大头照上传框在本机对齐,他人电脑不对齐。
- 根因:`--large` 上传框写死 `width:240px`ship 写 248px),而弹窗 `width:90%` + `el-col :span=6` 列宽是弹性的、上方输入框 `width:100%`;列宽随分辨率 / Windows 系统缩放 / 浏览器缩放变化,240px 只在特定宽度下恰好等于列宽。叠加 `labelWidth:auto` 靠 JS 实测字体(苹方 vs 微软雅黑)宽度,label 列宽也不同 → 起始位置漂移。
- 修复:三个文件改为 `width:100%; max-width:240px; flex:none`ship 保留 248px 上限),窄屏跟随列宽、宽屏保持比例上限,左边缘恒与输入框对齐。
- `src/views/transportCapacity/driver.vue`.driver-uploader--large
- `src/views/transportCapacity/vehicle.vue`.vehicle-uploader--large
- `src/views/transportCapacity/ship.vue`.ship-uploader--large
-@vue/compiler-sfc(脚本放项目根目录)验证三个 SFC 编译通过。
- 教训:本项目所有「固定 px 宽度的上传/控件」都要配 `max-width` + `width:100%`,否则高分屏/低分屏下必然错位。
## 合同详情由弹窗改为独立页面(contract-manage
- 路由:新增 `/business/contract-manage/detail``src/router/views/index.js`name『合同详情』,meta `keepAlive:false` + `activeMenu:'/business/contract-manage'`),组件仍复用 `contract-manage.vue`(与 form 页同一套路)。
- 页面内三分支:`v-if="!isFormPage && !isDetailPage"`(列表)/ `v-else-if="isFormPage"`(表单)/ `v-else`(详情)。**v-else 必须紧邻 form 分支的 `</template>`**,中间不能插其它组件,否则 Vue 编译报「v-else 无相邻 v-if」——弹窗类组件(billing-plan-editor / 附件预览 / 变更记录详情 / 流程)放在三个分支之后。
- 关键改动:`openDetail``router.push({ path:'/business/contract-manage/detail', query:{ id, name } })`;新增 `initDetailPage()`(按 query.id 拉详情,`resetDetailState()` 先清空)、`closeDetailPage()`closeTag + 回列表)、`isDetailPage` / `detailPageTitle` computed`created``$route.fullPath` watch 都要加 `else if (this.isDetailPage)`
- 样式:`.contract-manage-detail--page { max-height:none; overflow:visible }`,去掉弹窗的 74vh 限制;底栏复用 `.contract-manage-page__footer`(fixed 浮动),只读详情页按钮保留「关闭」。
- 校验方式:dev server 已运行时 `curl "http://localhost:2889/src/views/business/contract-manage.vue"` 返回 200 即 Vite 编译通过(比整包 build 快)。
## 只给「查看弹窗」加宽 labeltemporary-credit-limit
- 场景:详情页长 label(「剩余项目资金使用额度(万元)」)换行超 2 行撑坏详情表格,但又不能影响新增/编辑页。
- **关键发现:Avue 的 `avue-form` 在 detail 模式下根节点会挂 `.avue--detail` 类**(源码 `node_modules/@smallwei/avue/lib/packages/element-plus/form/index3.js``class: normalizeClass([b(), { 'avue--detail': isDetail }])`)。所以「只针对详情态」的选择器就是:
```scss
.temporary-credit-limit-dialog .avue--detail .el-form-item__label { width:210px !important; flex:0 0 210px !important; }
```
新增/编辑弹窗无 `.avue--detail`,天然不受影响,不需要在 option 里做 mode 判断。
- 校验样式块编译:dev server 请求 `?vue&type=style&index=1&lang.scss`**lang 必须写 scss,写 css 会 500**),grep 规则确认产物。
- 结论备忘:长 label 优先「加宽 + 换行 ≤2 行」,不要用单行省略 + tooltip —— 详情是纯阅读场景,hover 才能看全字段名的可发现性差。
+7 -2
View File
@@ -30,8 +30,13 @@
- Avue 内置弹窗 footer 真实 class 是 `.avue-dialog__footer`,须 `position:sticky; bottom:0; flex:none; background:#fff` 贴底。
### 上传证件区域尺寸(transportCapacity
- 身份证/行驶证/道路运输证等固定 **240×151px**1.586:1)。
- scoped 覆盖 `.xxx-uploader--large { width:240px }``--large .el-upload { width:100%; height:151px }`
- 身份证/行驶证/道路运输证等目标比例 **240×151px**1.586:1ship 为 248px 上限
- ⚠️ **不能写死 `width:240px`**:弹窗 `width:90%` + 列 `el-col :span=6` 是弹性的,固定 px 只在特定分辨率/系统缩放下恰好等于列宽(本机对齐、他人错位)。统一写法:
```scss
.xxx-uploader--large { display:block; width:100%; max-width:240px; flex:none; margin:0; }
.xxx-uploader--large .el-upload { width:100%; height:151px; }
```
2026-09-11 已修正 driver / vehicle / ship 三处。
### 上传图片点击放大预览
- `src/components/image-upload-field/main.vue` 用 `<el-image preview-src-list fit="contain" preview-teleported :z-index="3000" @click.stop>`,阻止冒泡到 el-upload。
+12
View File
@@ -287,6 +287,18 @@ export default [
},
],
},
{
path: '/business/contract-manage/detail',
component: Layout,
children: [
{
path: '',
name: '合同详情',
meta: { keepAlive: false, activeMenu: '/business/contract-manage' },
component: () => import('@/views/business/contract-manage.vue'),
},
],
},
{
path: '/business/contract-manage/change',
component: Layout,
+103 -67
View File
@@ -1,6 +1,8 @@
<template>
<basic-container :class="['contract-manage-page', { 'contract-manage-page--form': isFormPage }]">
<template v-if="!isFormPage">
<basic-container
:class="['contract-manage-page', { 'contract-manage-page--form': isFormPage || isDetailPage }]"
>
<template v-if="!isFormPage && !isDetailPage">
<avue-crud
ref="crud"
v-model:page="page"
@@ -96,7 +98,7 @@
/>
</template>
<template v-else>
<template v-else-if="isFormPage">
<div class="archive-page-form__title">{{ formPageTitle }}</div>
<el-form
@@ -558,65 +560,13 @@
</div>
</template>
<billing-plan-editor
ref="billingPlanEditor"
v-model="billingPlanBox"
:value="billingPlanForm"
:index="billingPlanIndex"
@save="saveBillingPlan"
/>
<template v-else>
<div class="archive-page-form__title">{{ detailPageTitle }}</div>
<el-dialog
v-model="attachmentDocumentPreviewVisible"
:title="attachmentPreviewFile.name || '附件预览'"
append-to-body
destroy-on-close
width="90%"
top="4vh"
<div
v-loading="detailLoading"
class="contract-manage-form contract-manage-detail contract-manage-detail--page"
>
<pdf-preview
v-if="
attachmentDocumentPreviewVisible &&
attachmentPreviewFile.url &&
attachmentPreviewFile.isPdf
"
:source="attachmentPreviewFile.url"
@error="handleAttachmentPreviewError"
/>
<open-file-viewer
v-else-if="attachmentDocumentPreviewVisible && attachmentPreviewFile.url"
:file="attachmentPreviewFile.url"
:file-name="attachmentPreviewFile.name"
:mime-type="attachmentPreviewFile.mimeType"
width="100%"
height="72vh"
fit="contain"
theme="auto"
locale="zh-CN"
:toolbar="attachmentViewerToolbar"
:plugins="attachmentViewerPlugins"
@unsupported="handleAttachmentPreviewUnsupported"
@error="handleAttachmentPreviewError"
/>
</el-dialog>
<el-image-viewer
v-if="attachmentImagePreviewVisible"
:url-list="attachmentImagePreviewUrls"
:initial-index="attachmentImagePreviewIndex"
@close="attachmentImagePreviewVisible = false"
/>
<el-dialog
v-model="detailBox"
title="合同详情"
append-to-body
destroy-on-close
class="contract-manage-detail-dialog"
width="92%"
top="4vh"
>
<div v-loading="detailLoading" class="contract-manage-form contract-manage-detail">
<section class="contract-manage-form__section contract-manage-form__section--panel">
<div class="dialog-section-title">基本信息</div>
<el-descriptions :column="3" class="contract-manage-detail__descriptions">
@@ -819,11 +769,61 @@
</el-table>
</section>
</div>
<template #footer
><el-button type="primary" @click="detailBox = false">关闭</el-button></template
<div class="contract-manage-page__footer">
<el-button type="primary" @click="closeDetailPage">关闭</el-button>
</div>
</template>
<billing-plan-editor
ref="billingPlanEditor"
v-model="billingPlanBox"
:value="billingPlanForm"
:index="billingPlanIndex"
@save="saveBillingPlan"
/>
<el-dialog
v-model="attachmentDocumentPreviewVisible"
:title="attachmentPreviewFile.name || '附件预览'"
append-to-body
destroy-on-close
width="90%"
top="4vh"
>
<pdf-preview
v-if="
attachmentDocumentPreviewVisible &&
attachmentPreviewFile.url &&
attachmentPreviewFile.isPdf
"
:source="attachmentPreviewFile.url"
@error="handleAttachmentPreviewError"
/>
<open-file-viewer
v-else-if="attachmentDocumentPreviewVisible && attachmentPreviewFile.url"
:file="attachmentPreviewFile.url"
:file-name="attachmentPreviewFile.name"
:mime-type="attachmentPreviewFile.mimeType"
width="100%"
height="72vh"
fit="contain"
theme="auto"
locale="zh-CN"
:toolbar="attachmentViewerToolbar"
:plugins="attachmentViewerPlugins"
@unsupported="handleAttachmentPreviewUnsupported"
@error="handleAttachmentPreviewError"
/>
</el-dialog>
<el-image-viewer
v-if="attachmentImagePreviewVisible"
:url-list="attachmentImagePreviewUrls"
:initial-index="attachmentImagePreviewIndex"
@close="attachmentImagePreviewVisible = false"
/>
<el-dialog
v-model="detailChangeRecordVisible"
title="变更记录详情"
@@ -1313,7 +1313,6 @@ export default {
changeRecordRows: [],
settlementTypeOptions: ['月结', '日结', '周结', '半月结', '固定天数周期结算'],
billCycleTypeOptions: ['固定截单日', '自然月'],
detailBox: false,
detailLoading: false,
detailRow: {},
detailContractFileRows: [],
@@ -1352,6 +1351,9 @@ export default {
isFormPage() {
return this.$route.path === '/business/contract-manage/form';
},
isDetailPage() {
return this.$route.path === '/business/contract-manage/detail';
},
formMode() {
return this.$route.query.mode === 'edit' ? 'edit' : 'add';
},
@@ -1362,6 +1364,9 @@ export default {
formPageTitle() {
return this.$route.query.name || `${this.formMode === 'edit' ? '编辑' : '新增'}合同管理`;
},
detailPageTitle() {
return this.$route.query.name || '合同详情';
},
ids() {
return this.selectionList.map(item => item.id).join(',');
},
@@ -1461,6 +1466,7 @@ export default {
watch: {
'$route.fullPath'() {
if (this.isFormPage) this.initFormPage();
else if (this.isDetailPage) this.initDetailPage();
},
settlementConfigTab(tab, oldTab) {
if (tab === oldTab) return;
@@ -1476,6 +1482,7 @@ export default {
this.loadSettlementDictionaries();
this.loadOrganizationOptions();
if (this.isFormPage) this.initFormPage();
else if (this.isDetailPage) this.initDetailPage();
},
methods: {
buildTableOption() {
@@ -2204,19 +2211,42 @@ export default {
if (Number(nextValue) > 100) nextValue = '100';
row.ratioLimit = nextValue;
},
// 合同详情:与新增/编辑一致,跳转独立页面(/business/contract-manage/detail
openDetail(row) {
this.detailBox = true;
this.$router.push({
path: '/business/contract-manage/detail',
query: { id: row.id, name: row.contractName ? `合同详情-${row.contractName}` : '合同详情' },
});
},
initDetailPage() {
this.resetDetailState();
const id = this.$route.query.id;
if (!id) return;
this.detailLoading = true;
this.applyDetailState(row);
this.api
.getDetail(row.id)
.getDetail(id)
.then(res => {
this.applyDetailState(res.data?.data || row);
this.applyDetailState(res.data?.data || {});
})
.finally(() => {
this.detailLoading = false;
});
},
resetDetailState() {
this.detailRow = {};
this.detailContractFileRows = [];
this.detailAttachmentRows = [];
this.detailBillingPlanRows = [];
this.detailPaymentRatioRows = [];
this.detailChangeRecordRows = [];
this.detailPreSettlementRuleForm = defaultSettlementRule();
this.detailFormalSettlementRuleForm = defaultSettlementRule();
this.detailSettlementConfigTab = 'pre';
},
closeDetailPage() {
this.$router.$avueRouter?.closeTag?.();
this.$router.push('/business/contract-manage');
},
applyDetailState(detail = {}) {
this.detailRow = {
...detail,
@@ -2895,6 +2925,12 @@ export default {
max-height: 74vh;
overflow-y: auto;
// 独立详情页面:不再受弹窗高度限制,交给页面自身滚动
&--page {
max-height: none;
overflow: visible;
}
&__descriptions {
:deep(.el-descriptions__label) {
display: inline-block;
+1 -1
View File
@@ -288,7 +288,7 @@
<el-form-item prop="fundLimit" class="project-apply-form__tip-label">
<template #label>
<span>项目资金使用额度</span>
<el-tooltip content="实际业务回款周期内付款" placement="top">
<el-tooltip content="实际业务回款周期内付款额度" placement="top">
<el-icon class="project-apply-form__label-tip"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
@@ -763,6 +763,7 @@ export default {
align-items: center;
justify-content: flex-end;
margin-bottom: 12px;
margin-top: 12px;
}
&__attachment-upload {
@@ -840,6 +841,13 @@ export default {
.temporary-credit-limit-dialog .business-crud-page__detail-content .el-descriptions__label {
width: 180px !important;
}
// 仅查看(detail)态加宽 label 列:长字段名(如「剩余项目资金使用额度(万元)」)收敛为最多两行,
// 避免撑坏详情表格。新增 / 编辑弹窗的 avue-form 无 .avue--detail 类,不受影响
.temporary-credit-limit-dialog .avue--detail .el-form-item__label {
width: 148px !important;
flex: 0 0 148px !important;
}
.el-form-item--default .el-form-item__label {
height: auto !important;
}
+5 -2
View File
@@ -1829,10 +1829,13 @@ export default {
overflow: hidden;
}
// 大尺寸模式:缩成 1.586:1 比例,左对齐(不再 100% 占栏)
// 大尺寸模式:跟随列宽自适应(最大 240px,约 1.586:1 证件比例),
// 保证不同分辨率 / 系统缩放下都与上方输入框左右对齐
:deep(.driver-uploader--large) {
display: block;
width: 240px;
width: 100%;
max-width: 240px;
flex: none;
margin: 0;
}
+5 -1
View File
@@ -1349,9 +1349,13 @@ export default {
height: 151px;
}
// 大尺寸模式:跟随列宽自适应(最大 248px),
// 保证不同分辨率 / 系统缩放下都与上方输入框左右对齐
:deep(.ship-uploader--large) {
display: block;
width: 248px;
width: 100%;
max-width: 248px;
flex: none;
margin: 0;
}
+5 -2
View File
@@ -1741,10 +1741,13 @@ export default {
overflow: hidden;
}
// 1.586:1 100%
// 240px 1.586:1
// /
:deep(.vehicle-uploader--large) {
display: block;
width: 240px;
width: 100%;
max-width: 240px;
flex: none;
margin: 0;
}
+13 -13
View File
@@ -47,26 +47,26 @@ export default ({ mode, command }) => {
__VUE_I18N_LEGACY_API__: true,
__INTLIFY_PROD_DEVTOOLS__: false,
},
server: {
port: 2888,
proxy: {
'/api': {
target: 'http://localhost',
//target: 'https://saber3.bladex.cn/api',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
},
},
},
// server: {
// port: 2889,
// port: 2888,
// proxy: {
// '/api': {
// target: 'http://172.16.203.228:8000',
// target: 'http://localhost',
// //target: 'https://saber3.bladex.cn/api',
// changeOrigin: true,
// rewrite: path => path.replace(/^\/api/, ''),
// },
// },
// },
server: {
port: 2889,
proxy: {
'/api': {
target: 'http://172.16.203.228:8000',
changeOrigin: true,
},
},
},
resolve: {
alias: {
'~': resolve(__dirname, './'),