Merge remote-tracking branch 'websoft/master'

This commit is contained in:
2026-08-21 12:08:01 +08:00
22 changed files with 714 additions and 229 deletions
+60
View File
@@ -0,0 +1,60 @@
# 2026-08-21
## process-config 弹窗卡片间隔(对齐 project-apply 风格)
- 需求:process-config 新增/编辑弹窗内三张 `<section-card>`(基本信息 / 现行过程节点 / 节点设置)要像 project-apply 表单页一样独立成卡、卡间露出灰底(12px)。
- 根因:全站规则 `.el-dialog .avue-form { background:#fff; padding:8px 16px 4px }`element-ui.scss ~246 行)把 Avue 弹窗表单整体包成一张白卡,section-card 白卡叠在其上看不出间隔。
- 改动(仅 2 文件):
1. `src/option/business/process-config.js`option 顶层加 `dialogCustomClass: 'process-config-dialog'`
2. `src/styles/element-ui.scss`contract-manage-dialog 规则块后新增):
- `.process-config-dialog .avue-form { background:transparent; box-shadow:none; padding:0 }` —— 白卡透明化,灰底(.el-dialog__body 全局 #f2f2f2)露出,section-card 自带 margin-bottom:12px 形成卡距。
- `.process-config-dialog .avue-form__group > .el-col > .el-form-item { margin-bottom:0 }` —— formslot 外层 form-item margin 与卡片 margin 叠加会拉大卡距,归零统一 12px。
- `.process-config-dialog .avue-dialog__footer` + `.avue-crud__dialog .process-config-dialog .avue-dialog__footer { margin-top:0 !important }` —— 抵消全局 947 行 `.avue-crud__dialog .avue-dialog__footer { margin-top:-30px !important }`(form 透明化后会上移压卡)。双选择器兼顾 dialog 未 teleport / teleport 两种挂载。
- 已验证:Vite 2889 编译通过、HMR 生效。
- 备注:`menu-form-before` 插槽(保存草稿/保存并启用按钮)渲染在 `.avue-dialog__footer` 内(avue 源码 dialog-form3.mjs 确认),不受 form 透明化影响,仍处白底底栏。
## 合同管理表单页:多分组卡片风格(对齐 project-apply 风格)
- 需求:合同管理新增/编辑表单页(`/business/contract-manage/form?mode=add|edit`)的 7 个业务分组(基本信息 / 合同文件 / 计费信息 / 结算单规则 / 付款比例设置 / 其它附件 / 变更记录)要像 project-apply 一样独立成白底卡、卡间 12px 间距、标题 4px 主色蓝条。
- 关键事实(容易踩坑):
- 合同表单走 `PageAvueForm` 组件(`src/views/business/components/business-crud-page.vue:5577`),render 函数 `h('div', { class: option.dialogCustomClass }, [h('avue-form', ...)])`**不包 el-dialog**(所以底部按钮天然不浮动贴底,会随内容滚动——这点与 project-apply 的 el-dialog #footer 浮动有差异)。
- `option` 顶层在 `isStandaloneFormPage` 时已设 `dialogCustomClass: 'contract-manage-form-dialog'``dialogAppendToBody: false``dialogModal: false``dialogTop: '0'``dialogWidth: '100%'`5897-5914 行)。
- 7 个分区标题列在 option.column 里以 `order` 升序分块(`basicInfoTitle=200``contractFileTitle=30``billingInfoTitle=10``settlementRuleTitle=-10``paymentRatioTitle=-30``attachmentTitle=-50``changeRecordTitle=-70`),其余字段 order 在每对标题列之间。
- 实施方案(已提交 63256a9):
1. `buildContractFormGroupOption(option)`business-crud-page.vue 新增方法):按 order 降序遍历 option.column,遇到 7 个 `*Title` 列就切片分组;`groups.length<=1` 时回退原 option(不影响其他业务弹窗)。返回 `{ ...option, column: groups[0], group: groups.slice(1).map(g=>({ label:'', arrow:false, column:g })) }`
2. `pageFormOption`6205 行)`isContractFormPage` 时调用上述方法。
3. CSSbusiness-crud-page.vue scoped 末尾):
- `.contract-manage-form-dialog``padding:20px 24px 96px; background:#f5f6fa; box-sizing:border-box;`(外层灰底)
- `.business-crud-page--contract-form .avue-form { background:transparent; padding:0 }`(表单白卡透明化)
- `.business-crud-page--contract-form .avue-form > .el-form > .avue-form__group``.avue-group` 都设白底 + 6px 圆角 + 0 1px 2px 阴影 + 12px margin-bottom + 14px 16px 4px padding(两张选择器分别覆盖第 1 组 column 与第 2-7 组 group 渲染)
- 验证(agent-browser + 系统 Chrome,已登录后截图核对):
- 合同表单 7 张白底卡清晰分隔,每张卡左侧 4px 主色蓝条 + 标题(基本信息 / 合同文件 / 计费信息 / 结算单规则 / 付款比例设置 / 其它附件 / 变更记录),卡间约 12px 灰底露白。
- 底部 5 个按钮(提交 / 关闭 / 保存 / 提交临时合同 / 提交正式合同)随内容滚动(非浮动贴底)。
- 与 project-apply 滚动后的"客户信息 / 承运商信息"卡风格完全一致。
- 与 project-apply 的已知差异(用户本次未要求改):
- 合同表单底部按钮非浮动贴底(PageAvueForm 不包 el-dialogavue-form 的 form_menu 渲染在内部)
- 合同表单无"返回"按钮(用"关闭"代替)
- 合同表单无独立页面标题(page-header 区域在 breadcrumb 处,不在 main 标题区)
- 行为验证码:登录页拼图/旋转求解基础设施已搭好——`/tmp/captcha_solve.py`CORS 修复后支持 puzzle+rotate 双模式)、`/tmp/ab-install/node_modules/.bin/agent-browser --browser-channel chrome`。登录流程:访问 → 切"账号密码登录" → 点登录 → 检测 `.puzzle-stage`/`.rotate-image` → POST 至 http://127.0.0.1:8765 → 取 xDisp/angle → JS PointerEvent 模拟拖动 .track-handle 即可。
- 提交:63256a9 feat(business-crud-page): 合同管理表单页改为多分组卡片风格(1 file, +86/-2)。未提交改动(process-config.js、element-ui.scss、master-order-editor.vue、loading-manage.vue、project-apply.vue、formal-settlement.vue、customer-archive.vue)与本任务无关,未纳入。
## shipping-template 独立表单页加 archive-page-form__title(参考 customer-archive
- 需求:独立表单页 `/business/shipping-template/form?mode=add&name=新运费发货模板` 顶部缺页面标题,参考 customer-archive 的 `.archive-page-form__title` 加标题。
- 改动(单文件 `src/views/business/components/business-crud-page.vue`):
1. `PageAvueForm`5578 行)声明 `props.formPageTitle`render 时若非空则先 push `<div class="archive-page-form__title">{{ title }}</div>` 再渲染 avue-formtitle 在表单容器 `shipping-template-dialog` 内、avue-form 之前)。
2. 模板 `<component :is="crudContainer">` 新增 `:form-page-title="isStandaloneFormPage ? formPageTitle : undefined"`
3. 新增 computed `formPageTitle()``isStandaloneFormPage && isShippingTemplatePage` 时返回 `this.$route.query.name || '新增'/'编辑' + config.title`,否则 ''。
4. 样式(scoped 末尾,用 `:global` 因为 title div 由 PageAvueForm 函数式组件渲染、无 scoped 属性):`:global(.business-crud-page--form-page .archive-page-form__title)` 复制 customer-archive 的 18px/600/#303133 + margin-bottom:20px::before 为空,与参考一致,仅纯文字标题)。
- 仅作用于 shipping-template 独立表单页;其他独立表单页(waybill/transport-plan/contract`formPageTitle` 返回 '',不受影响。
## transport-plan 独立表单页也加 archive-page-form__title
- 需求:`/business/transport-plan/form?mode=add&name=新运费运输计划` 也要同样的页面标题。
- 改动:`formPageTitle()` 条件由 `isStandaloneFormPage && isShippingTemplatePage` 改为 `isStandaloneFormPage && (isShippingTemplatePage || isTransportPlanPage)`(复用已有 `isTransportPlanPage` computed`config.permission==='transport_plan'`)。标题文案取 `this.$route.query.name`(「新运费运输计划」)或回退「新增/编辑运输计划」。
- 验证:Vite 2889 编译通过(HTTP 200,无错误)。
- 验证:Vite 2889 编译通过(HTTP 200,无 transform/syntax 错误),HMR 生效。如需扩展到其他独立表单页,去掉 `isShippingTemplatePage` 限定即可。
## 将 .archive-page-form__title 全局化,并再加 waybill / loading-manage / master-order 三页
- 需求:把页面标题扩展到 `/business/waybill-manage/form``/business/loading-manage/form``/business/master-order?mode=editor` 三页,并与 customer-archive / shipping-template / transport-plan 视觉统一。
- **全局化样式**`.archive-page-form__title` 原本分散在 `customer-archive.vue`scoped)与 `business-crud-page.vue``:global`,::before 为空纯文字)。现统一提取到 `src/styles/element-ui.scss`5b 段):18px/600/#303133 + margin-bottom:20px + `::before` 4px 主色竖条 + 8px 间距(补全竖条,与 section-card 卡头风格一致)。并删除上述两处本地重复定义。
- **waybill-manage/form**:在 `business-crud-page.vue``formPageTitle()` 条件再并入 `isWaybillDetailLayout``config.permission==='waybill_manage'`),复用既有 computed。标题取 `query.name || '新增/编辑运单管理'`
- **loading-manage/form**:该路由直接映射到 `loading-manage.vue`(非 business-crud-page)。新增 computed `formPageTitle()``query.name || '新增/编辑配载管理'`),并在独立表单页的 `<component>` 内、body 之前加 `<div v-if="isStandaloneFormPage" class="archive-page-form__title">{{ formPageTitle }}</div>`
- **master-order?mode=editor**`master-order.vue` 渲染 `master-order-editor.vue`。将 `<master-order-editor>` 包进 `<template v-else-if="mode==='editor'">`,前置 `<div class="archive-page-form__title">{{ masterEditorTitle }}</div>`;新增 computed `masterEditorTitle``routeId ? '编辑总单' : '新增总单'`)。
- 验证:4 个 SFC + element-ui.scss 经 Vite 2889 编译均 HTTP 200、无 transform/syntax 错误。
+35 -70
View File
@@ -2,83 +2,48 @@
## 设计约定(经用户确认的决策) ## 设计约定(经用户确认的决策)
### 弹窗内多字段表单分组:使用全局 `<section-card>` 组件(项目统一标准) ### 弹窗分组:统一用全局 `<section-card>` 组件
- 整弹窗 body 由 `src/styles/element-ui.scss` 全局 `.el-dialog__body { background:#f5f6fa; padding:16px }` 统一灰底;每组 = 一张白底卡片(4px 主色竖条 + 8px 间距卡头 + 白底 body,圆角 6px + 极淡阴影)。
- 组件已全局注册 `<section-card title="...">`,支持 `#title` / `#extra` slot。严禁再手写 `.xxx-form__block` + `.dialog-section-title` 拼凑卡片。
- 仅 3 项的小分组用 `el-col :span="8"` 均分一行。
**整体结构** ### Avue 内置弹窗内用 section-card:需 dialogCustomClass 透明化 avue-form
- 整个弹窗 body 由 `src/styles/element-ui.scss` 全局 `.el-dialog__body { background: #f5f6fa; padding: 16px }` 统一灰底(无需页面再覆盖) - 全站 `.el-dialog .avue-form { background:#fff; padding:8px 16px 4px }` 会把表单整体包白卡,内置弹窗放 section-card 会白卡叠白卡
- 每个业务分组 = 一张白底卡片。用全局组件 `<section-card title="分组名">...</section-card>`,自动渲染 4px 主色竖条 + 8px 间距的卡头与白底 body(圆角 6px + 极淡阴影),与 `loading-manage.vue` / `customer-archive.vue` 等已落地页面一致。 - 解法:option 顶层加 `dialogCustomClass:'xxx-dialog'`,再在 element-ui.scss 写三条:
- 严禁再手写 `.xxx-form__block` + `.dialog-section-title` override 拼凑卡片(项目已有 `<section-card>` 标准组件)。 `.xxx-dialog .avue-form { background:transparent; box-shadow:none; padding:0 }`
`.xxx-dialog .avue-form__group > .el-col > .el-form-item { margin-bottom:0 }`(防 form-item margin 与卡距叠加)
`.xxx-dialog .avue-dialog__footer, .avue-crud__dialog .xxx-dialog .avue-dialog__footer { margin-top:0 !important }`(抵消全局 947 行 `-30px !important` 上移)。
**HTML 结构**(每组三件套): ### 弹窗底部操作栏浮动(全站统一,全局 CSS 实现)
```vue - `.el-dialog__body { max-height: calc(100vh - 200px); overflow-y:auto; overflow-x:hidden }``.el-dialog__footer { margin:0; padding:12px 20px; background:#fff; border-top:1px solid #ebeef5; box-shadow:0 -2px 8px rgba(0,0,0,.04) }`
<section-card title="分组标题"> - **易踩坑**Avue 内置弹窗 footer 真实 class 是 `.avue-dialog__footer`(非 `.el-dialog__footer`),且渲染在 `.el-dialog__body` 内、随 body 滚动滚出视口。要让它贴底须 `position:sticky; bottom:0; flex:none; background:#fff`(配合 body flex 列布局 + overflow-y:auto)。手写 el-dialog 的 `.el-dialog__footer` 在 body 外天然可见。
<el-row :gutter="18"> - **弹窗高度链**:① `.avue-dialog`(库内置)`max-height:calc(100% - 200px)` 控制整体上限;② `.avue-crud__dialog .el-dialog__body { max-height:calc(100vh - 200px); overflow-y:auto }`(451-457 行);③ 专项弹窗(shipping-template-dialog / transport-plan-dialog`.el-dialog` 自身 `margin-top:4vh; height/max-height:calc(100vh-4vh-24px); display:flex; flex-direction:column` + header flex:none / body flex:1 1 auto; min-height:0; overflow-y:auto; max-height:none!important / footer sticky。**改这类弹窗尺寸只改 section-card 周边规则,不要动 body 内 max-height。**
<el-col :span="8">...</el-col>
</el-row>
<!-- 同一组的所有 row 都放同一张白卡内 -->
</section-card>
```
**适用**:新增/编辑弹窗中字段超过 ~10 个、需按业务语义分组时。 ### 上传证件区域尺寸规范(transportCapacity 强制)
- 二代身份证 85.6×53.98mm → 宽:高 ≈ **1.586:1**;取 ~75% → 固定 **240×151px**el-col 内 `margin:0` 左对齐。
-`<image-upload-field large>` 适用(身份证/行驶证/道路运输证/登记本/船舶各证)。每页 scoped 覆盖 `.xxx-uploader--large``width:240px`)与 `--large .el-upload``width:100%; height:151px`);注意 `.xxx-uploader` 须写在 `.xxx-uploader--large` 之前(同特定性,源码顺序决胜)。
### 简单分组(仅 3 项 ### 上传图片点击放大预览(全局组件 `image-upload-field`
- 联系信息等仅 3 项的分组,可用 `el-col :span="8"` 让三项均分一行,避免 4 列网格留白 - `src/components/image-upload-field/main.vue`:裸 `<img>` `<el-image :preview-src-list="[value]" fit="contain" preview-teleported :z-index="3000" @click.stop>`。图片 `@click.stop` 阻止冒泡到 el-upload(否则重传);"更换"经隐藏 `input.el-upload__input``.click()`el-upload 无 openFileDialog)。readonly 仅预览。driver/vehicle/ship 三页自动获得。z-index 必须 3000 + teleported 否则被 dialog 遮罩盖住
### 弹窗底部操作栏浮动效果(全统一规范 ### 独立表单页标题 `archive-page-form__title`(全统一,参考 customer-archive
- **需求来源**:用户要求参考 `business/project-apply`(手写 el-dialog + `#footer` slotbody `max-height:76vh;overflow-y:auto`footer 固定底部)实现"取消/保存按钮浮动在底部"。因绝大多数页面是 Avue CRUD 内置弹窗(无法用 `#footer` slot),统一用**全局 CSS** 实现,全站自动生效 - 样式已**全局化**到 `src/styles/element-ui.scss`5b 段):18px/600/#303133 + margin-bottom:20px + `::before` 4px 主色竖条 + 8px 间距。原 `customer-archive.vue`scoped)与 `business-crud-page.vue``:global`)两处本地重复定义已删除,改由全局规则统一驱动(全站任意页面用此 class 即得统一标题)
- **实现位置**`src/styles/element-ui.scss`(在 `.el-dialog .el-form-item` 规则之后新增)。 - **business-crud-page 系列**shipping-template / transport-plan / waybill-manage 独立表单页):`PageAvueForm`render `h('div',{class:dialogCustomClass},[titleDiv, avue-form])`)声明 `props.formPageTitle`,模板 `:form-page-title="isStandaloneFormPage ? formPageTitle : undefined"`computed `formPageTitle()``isStandaloneFormPage && (isShippingTemplatePage || isTransportPlanPage || isWaybillDetailLayout)` 时返回 `this.$route.query.name || '新增'/'编辑'+config.title``isWaybillDetailLayout` = `config.permission==='waybill_manage'`)。
- **规则** - **loading-manage/form**:路由直接映射 `loading-manage.vue`(非 business-crud-page)。新增 computed `formPageTitle()``query.name || '新增/编辑配载管理'`),并在 `<component :is="isStandaloneFormPage?'div':'el-dialog'">` 内、body 前加 `<div v-if="isStandaloneFormPage" class="archive-page-form__title">`
- `.el-dialog__body { max-height: calc(100vh - 200px); overflow-y: auto; overflow-x: hidden; }` —— 内容超高时独立滚动,footer(位于 body 之外)始终可见 - **master-order?mode=editor**`master-order.vue``<master-order-editor>` 包进 `<template v-else-if="mode==='editor'">`,前置 `<div class="archive-page-form__title">{{ masterEditorTitle }}</div>`computed `masterEditorTitle` = `routeId ? '编辑总单' : '新增总单'`
- `.el-dialog__footer { margin:0; padding:12px 20px; background:#fff; border-top:1px solid var(--el-border-color-lighter,#ebeef5); box-shadow:0 -2px 8px rgba(0,0,0,.04); border-radius:0 0 var(--el-dialog-border-radius,4px); .el-button+.el-button{margin-left:8px} }` —— 视觉底栏(白底+上边框+轻阴影),与主内容区分 - 扩展原则:新增独立表单页要带此标题,要么并入 business-crud-page 的 `formPageTitle` 条件(business-crud-page 系列),要么在对应页面模板手写 `<div class="archive-page-form__title">`loading-manage / master-order 模式)
- **注意**`.el-dialog__body``max-height` 会作用于所有弹窗(含纯展示/表格弹窗),无 footer 的弹窗不受影响;project-apply 自身 `.project-apply-dialog .el-dialog__body{76vh}` 特异性更高仍优先。如需让某弹窗豁免,加更具体 class 覆盖即可。
- **Avue 内置 CRUD 弹窗的 footer 真实 class(重要,易踩坑)**:Avue 内置弹窗的"提交/取消"按钮 class 是 `.avue-dialog__footer`**不是** `.el-dialog__footer`),且**渲染在 `.el-dialog__body` 内部**、随 body 滚动会滚出视口。要让它像手写 `#footer` 那样始终贴底,必须针对 `.avue-dialog__footer``position: sticky; bottom: 0; flex: none; background: #fff`(配合 body `flex` 列布局 + `overflow-y:auto`)。手写 `el-dialog` 的 footer 才是 `.el-dialog__footer` 且在 body 外、天然可见。典型落地见 `shipping-template-dialog` / `transport-plan-dialog` 两条全局规则。
- **弹窗高度链分层(重要,易混淆)**:
- `.avue-dialog { max-height: calc(100% - 200px); min-height: 176px; display: flex; flex-direction: column; ... }` —— 这是 **Avue 库自身内置**src/ 与 node_modules 源码都搜不到出处),作用在弹窗容器外层,控制整体高度上限(视口减去 200px 安全区);项目改不动根,只能用 `!important`/更高特异性覆盖。
- `.avue-crud__dialog .el-dialog__body { max-height: calc(100vh - 200px); overflow-y: auto; }` —— 见 `src/styles/element-ui.scss` 451-457 行,作用于 avue 内置 CRUD 弹窗 body,让内容超出时 body 内滚动(注意是 **100vh**,且仅作用于 `.avue-crud__dialog` 限定范围内)。
- 特定页面专项弹窗(如 `shipping-template-dialog` / `transport-plan-dialog`)的 `.el-dialog` 自身用 `margin-top:4vh !important; height/max-height: calc(100vh - 4vh - 24px) 或 72vh; display: flex; flex-direction: column` 重新分配高度,并配合 `.el-dialog__header { flex:none }` / `.el-dialog__body { flex:1 1 auto; min-height:0; overflow-y:auto; max-height:none !important }` / `.avue-dialog__footer { position:sticky; bottom:0; flex:none; background:#fff }` 三段 flex 分摊。**改 shipping-template 这类弹窗尺寸,只改这些 section-card 周边规则即可,不要动 body 内的 `max-height`**。
### 上传证件区域尺寸规范(transportCapacity 模块强制)
- **比例**:中国二代身份证标准尺寸 85.6 × 53.98 mm → 宽:高 = **1.586 : 1**(横放)
- **尺寸**:取真实身份证的 ~75% → 固定 **240 × 151 px**1.586:1 精确:151 × 1.586 ≈ 240
- **位置**:在 el-col 列内 `margin: 0` 左对齐显示(不再 `width: 100%` 占满整栏,也不再居中)
- **适用范围**:所有 `<image-upload-field large>` 上传区,包括身份证正反面、大头照、机动车行驶证主页正/反面/副页正/反面、道路运输证、机动车登记本、船舶所有权/安全环保/国籍/最低安全配员/光船租赁/营业运输 等所有 cert 图片
- **实现方式**:在每个页面 scoped 样式里覆盖 class-prefix 对应的 `--large` 选择器(`.driver-uploader--large` / `.vehicle-uploader--large` / `.ship-uploader--large`):
```scss
:deep(.xxx-uploader) { display: block; } // 不再 width:100%
:deep(.xxx-uploader--large) {
display: block;
width: 240px;
margin: 0;
}
:deep(.xxx-uploader--large .el-upload),
:deep(.xxx-uploader--large.el-upload) {
width: 100%;
height: 151px;
}
```
- **CSS 顺序要点**`.xxx-uploader`(特定性 0,0,1,0)必须写在 `.xxx-uploader--large`(同为 0,0,1,0)之前,源码位置决定覆盖生效。
### 上传图片点击放大预览(全局组件 `image-upload-field` 已实现)
- **组件**`src/components/image-upload-field/main.vue`。原裸 `<img>` 改为 `<el-image :preview-src-list="[value]" fit="contain" preview-teleported :z-index="3000" @click.stop>`。点击图片即全屏预览(滚轮缩放 / 旋转 / 下载),零额外依赖。
- **预览与上传分离**:图片 `@click.stop` 阻止冒泡到 `el-upload`(否则会触发重传);"更换"入口(form 模式图片下方 `el-link`、card 模式 head 的 `actionText`)通过 `el-upload` 内部隐藏 `input.el-upload__input` 的 `.click()` 触发文件框。
- **为什么用 DOM click**Element Plus `el-upload` 仅 expose `abort/submit/clearFiles/handleStart/handleRemove`**没有** `openFileDialog`,故用 `this.$refs.uploadRef.$el.querySelector('.el-upload__input').click()`。
- **readonly 模式**:仅渲染 `el-image` 预览,不显示上传入口(查看档案时也能放大看证件)。
- **z-index 注意**:务必 `preview-teleported`(预览层挂 body+ `:z-index="3000"`,否则会被 `el-dialog` 遮罩(2000 区间)盖住。
- **适用范围**:全仓通用组件,driver / vehicle / ship 三个页面自动获得该能力(改动一次组件即可)。
## 关键文件 ## 关键文件
- 通用分组白卡组件:`src/components/section-card/main.vue`(已全局注册为 `<section-card>`,支持 `title` / `#title` / `#extra` slot - 分组白卡:`src/components/section-card/main.vue`
- 弹窗灰底 body + section-card 全局样式:`src/styles/element-ui.scss``.el-dialog__body`、`.section-card { &__header / &__bar / &__title / &__extra / &__body }` - 全局弹窗/section-card 样式:`src/styles/element-ui.scss`
- 司机档案弹窗:`src/views/transportCapacity/driver.vue`(手写 `el-form` + `<section-card>` 4 大分组:基础身份信息 / 驾驶证信息 / 从业资格证信息 / 司机联系信息)。 - 客商档案弹窗:`src/views/vehicle/customer-archive.vue`(用 `archive-page-form__title` 作页面标题,`:deep(.archive-dialog .el-dialog__body){max-height:72vh}`)。
- 车辆档案弹窗:`src/views/transportCapacity/vehicle.vue`(手写 `el-form` + `<section-card>` 2 大分组:基础车辆信息 / 车辆资质图片;6 张大图按身份证比例 240×151 左对齐)。 - 司机/车辆/船舶档案:`src/views/transportCapacity/{driver,vehicle,ship}.vue`(手写 el-form + section-card + 240×151 证件图)。
- 船舶档案弹窗:`src/views/transportCapacity/ship.vue`(手写 `el-form` + `<section-card>` 2 大分组:基础船舶信息 / 船舶证书信息;内部 6 张 cert 图片按身份证比例 240×151 左对齐;子证书分段用 `.cert-block` + border-top)。 - 项目补录弹窗:`src/views/business/project-apply.vue`(手写 el-dialog + #footer 浮动,视觉参照)。
- 客商档案弹窗:`src/views/vehicle/customer-archive.vue`(工商信息 / 联系信息 / 财务/信用信息 / 其他信息 等分组也用 `<section-card>`)。
- 项目补录弹窗:`src/views/business/project-apply.vue`(手写 `__grid` / `__table` / `__textarea-list` 同款白卡结构,先于 `<section-card>` 组件化之前落地,可视为视觉参照)。
## 环境注意 ## 环境注意
- Vite 代理跨域:Saber3 axios 开 `withCredentials=true`,直连绝对地址后端须 CORS `Allow-Origin` 为具体域名(不能用 `*`),否则浏览器拒绝。改 `.env`/`vite.config.mjs` 必须重启 dev。 - Vite 代理跨域:axios `withCredentials=true`,直连绝对地址后端须 CORS `Allow-Origin` 为具体域名(不能用 `*`。改 `.env`/`vite.config.mjs` 必须重启 dev。
### business-crud-page 封装页面的弹窗高度改法(重要,复用模式) ## business-crud-page 复用模式(被 waybill/transport-plan/shipping-template/contract 共用
- `src/views/business/components/business-crud-page.vue` 被 waybill-manage / transport-plan 等多个业务页共用,内部 `<avue-crud :option="option">`option 由传入 prop `crudOption` 经 `cloneOption`(组件内 6261 行,浅展开+重映射 column,**保留所有顶层属性**)克隆而来 - option 由 prop `crudOption``cloneOption` 克隆(保留所有顶层属性)。给各页 option 顶层加 `dialogCustomClass:'xxx-dialog'` + element-ui.scss `.xxx-dialog.el-dialog{height/max-height:calc(100vh - 40px)!important}` 可只影响该页
- 因此给各业务页 `option` 顶层加 `dialogCustomClass: 'xxx-dialog'`,再在 `element-ui.scss` 写 `.xxx-dialog.el-dialog { height/max-height: calc(100vh - 40px) !important; ... }` 等规则,即可**只影响该页**、不波及其他共用页面(已验证 waybill-manage / transport-plan 走同一机制生效) - avue-crud 弹窗带 `avue-crud__dialog` class,受全局 `.avue-crud__dialog .avue-dialog__footer{margin-top:-30px!important}` 影响,自定义 footer 须加 `margin-top:0!important` 抵消
- 注意 avue-crud 弹窗带 `avue-crud__dialog` class,会被全局 `.avue-crud__dialog .avue-dialog__footer { margin-top: -30px !important }`element-ui.scss 863 行)影响,自定义 footer 规则需加 `margin-top: 0 !important` 抵消;纯 dialog-form 弹窗(如 shipping-template)无此 class 不受影响 - 独立表单页(contract-manage / shipping-template / waybill / transport-plan)走 `PageAvueForm`,不包 el-dialog,底部按钮随内容滚动不浮动。`pageFormOption`6205 行)负责装配。contract 用 `buildContractFormGroupOption`6861 行)按 order 分 7 组白卡
+1
View File
@@ -183,5 +183,6 @@ export const option = {
delBtn: false, delBtn: false,
dialogTop: '4vh', dialogTop: '4vh',
dialogWidth: '96%', dialogWidth: '96%',
dialogCustomClass: 'process-config-dialog',
menuWidth: 220, menuWidth: 220,
}; };
+1 -1
View File
@@ -445,7 +445,7 @@ export const option = {
searchLabel: '状态', searchLabel: '状态',
searchOrder: 2, searchOrder: 2,
dicData: planStatusOptions, dicData: planStatusOptions,
minWidth: 120, minWidth: 82,
fixed: 'right', fixed: 'right',
addDisplay: false, addDisplay: false,
editDisplay: false, editDisplay: false,
@@ -8,8 +8,8 @@ export const transportReconciliationTableColumns = [
{ prop: 'contractName', label: '合同名称', minWidth: 150 }, { prop: 'contractName', label: '合同名称', minWidth: 150 },
{ prop: 'settlementAmount', label: '结算金额', minWidth: 120, money: true }, { prop: 'settlementAmount', label: '结算金额', minWidth: 120, money: true },
{ prop: 'reconciliationModeName', label: '对账模式', minWidth: 110 }, { prop: 'reconciliationModeName', label: '对账模式', minWidth: 110 },
{ prop: 'externalBillCount', label: '账单总数(条)', minWidth: 110 }, { prop: 'externalBillCount', label: '账单总数(条)', minWidth: 130 },
{ prop: 'matchedCount', label: '匹配数(条)', minWidth: 100 }, { prop: 'matchedCount', label: '匹配数(条)', minWidth: 120 },
{ prop: 'reconciliationStatusName', label: '对账状态', minWidth: 100 }, { prop: 'reconciliationStatusName', label: '对账状态', minWidth: 100 },
{ prop: 'createUserName', label: '创建人', minWidth: 100 }, { prop: 'createUserName', label: '创建人', minWidth: 100 },
{ prop: 'createTime', label: '创建时间', minWidth: 160 }, { prop: 'createTime', label: '创建时间', minWidth: 160 },
+1 -1
View File
@@ -46,7 +46,7 @@ export const option = {
viewBtn: true, viewBtn: true,
selection: true, selection: true,
dialogClickModal: false, dialogClickModal: false,
menuWidth: 120, menuWidth: 140,
column: [ column: [
{ {
label: '车船类型', label: '车船类型',
+43
View File
@@ -393,6 +393,27 @@
} }
} }
// 5b) 独立表单页顶部页面标题(与 vehicle/customer-archive 编辑弹窗一致)
// 全站统一:18px / 600 / #303133,底部 20px 间距,4px 主色竖条 + 8px 间距
.archive-page-form__title {
display: flex;
align-items: center;
margin-bottom: 20px;
color: #303133;
font-size: 18px;
font-weight: 600;
&::before {
flex: none;
//width: 4px;
height: 16px;
//margin-right: 8px;
border-radius: 2px;
//background-color: var(--el-color-primary);
content: '';
}
}
// 6) 弹窗内两列表单栅格工具类(与 project-apply 一致) // 6) 弹窗内两列表单栅格工具类(与 project-apply 一致)
.form-grid-2 { .form-grid-2 {
display: grid; display: grid;
@@ -586,6 +607,28 @@
background: #fff; background: #fff;
} }
// 过程配置新增/编辑弹窗:avue-form 整体白卡透明化,
// 让 <section-card> 分组白卡(基本信息 / 现行过程节点 / 节点设置)独立成卡、卡间露出灰底,
// 与 business/project-apply 表单页的分组卡片风格统一
.process-config-dialog .avue-form {
background: transparent;
box-shadow: none;
padding: 0;
}
// formslot 外层 el-form-item 默认 margin-bottom 与 section-card 自身 margin 叠加会拉大卡距,
// 归零后卡间距统一为 section-card 自身的 12px(与 project-apply 一致)
.process-config-dialog .avue-form__group > .el-col > .el-form-item {
margin-bottom: 0;
}
// 抵消全局 .avue-crud__dialog .avue-dialog__footer 的 -30px 上移
// avue-form 已透明化,footer 若上移会压住最后一张分组卡片)
.process-config-dialog .avue-dialog__footer,
.avue-crud__dialog .process-config-dialog .avue-dialog__footer {
margin-top: 0 !important;
}
// 统一表格线条颜色 // 统一表格线条颜色
.el-table { .el-table {
--el-table-border-color: #eff1f7; --el-table-border-color: #eff1f7;
+8 -6
View File
@@ -21,9 +21,6 @@
@on-load="onLoad" @on-load="onLoad"
> >
<template #menu-left> <template #menu-left>
<el-button type="primary" v-if="permission.cargo_type_add" @click="$refs.crud.rowAdd()">
<el-icon class="el-icon--left"><Plus /></el-icon>新建货物类型
</el-button>
<el-button type="primary" plain v-if="permission.cargo_type_import" @click="handleImport" <el-button type="primary" plain v-if="permission.cargo_type_import" @click="handleImport"
><el-icon class="el-icon--left"><Upload /></el-icon>批量导入</el-button ><el-icon class="el-icon--left"><Upload /></el-icon>批量导入</el-button
> >
@@ -130,7 +127,7 @@ import { openImportDialog } from '@/utils/import-excel';
import { getUploadHeaders } from '@/utils/upload'; import { getUploadHeaders } from '@/utils/upload';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
import { Plus, Upload, Download, Delete } from '@element-plus/icons-vue'; import { Upload, Download, Delete } from '@element-plus/icons-vue';
// 导入失败明细中,每个错误文本对应的「字段列」与「规则说明」 // 导入失败明细中,每个错误文本对应的「字段列」与「规则说明」
const CARGO_TYPE_FAIL_RULES = [ const CARGO_TYPE_FAIL_RULES = [
@@ -229,7 +226,7 @@ export const decorateCargoTypeFailDetail = async workbook => {
}; };
export default { export default {
components: { Plus, Upload, Download, Delete }, components: { Upload, Download, Delete },
data() { data() {
return { return {
form: {}, form: {},
@@ -281,7 +278,7 @@ export default {
...mapGetters(['permission']), ...mapGetters(['permission']),
permissionList() { permissionList() {
return { return {
addBtn: false, addBtn: true,
}; };
}, },
ids() { ids() {
@@ -592,6 +589,11 @@ export default {
:deep(.avue-crud__header) { :deep(.avue-crud__header) {
margin-top: 12px; margin-top: 12px;
background: transparent; background: transparent;
// 工具栏按钮组紧贴左侧
.avue-crud__left {
justify-content: flex-start;
}
} }
// 除 basic-container 卡片内层 el-card 的 padding(规范 4.4 // 除 basic-container 卡片内层 el-card 的 padding(规范 4.4
@@ -11,6 +11,7 @@
<component <component
:is="crudContainer" :is="crudContainer"
:option="pageFormOption" :option="pageFormOption"
:form-page-title="isStandaloneFormPage ? formPageTitle : undefined"
:table-loading="loading" :table-loading="loading"
:data="data" :data="data"
v-model:page="page" v-model:page="page"
@@ -2641,7 +2642,7 @@
<span v-else>-</span> <span v-else>-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="vehicleNo" label="车牌号/航班号/船号/班列号" width="192" /> <el-table-column prop="vehicleNo" label="车牌号/航班号/船号/班列号" width="200" />
<el-table-column prop="driverName" label="司机" width="128" /> <el-table-column prop="driverName" label="司机" width="128" />
<el-table-column prop="carrierName" label="承运商" width="192" /> <el-table-column prop="carrierName" label="承运商" width="192" />
<el-table-column label="运输方式" width="192"> <el-table-column label="运输方式" width="192">
@@ -2656,7 +2657,7 @@
{{ formatTransportPlanProvinceCityDistrict(row.departureAddress) }} {{ formatTransportPlanProvinceCityDistrict(row.departureAddress) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="状态" width="120" fixed="right"> <el-table-column label="状态" width="80" align="center" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<span <span
:class="['business-crud-page__transport-plan-waybill-status', row.statusClass]" :class="['business-crud-page__transport-plan-waybill-status', row.statusClass]"
@@ -2665,7 +2666,7 @@
</span> </span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="128" fixed="right"> <el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-link type="primary" @click="openTransportPlanWaybillDetail(row)"> <el-link type="primary" @click="openTransportPlanWaybillDetail(row)">
详情 详情
@@ -3225,7 +3226,7 @@
width="78%" width="78%"
class="business-crud-page__route-change-dialog" class="business-crud-page__route-change-dialog"
> >
<el-tabs v-model="waybillRouteChangeTab"> <el-tabs type="border-card" v-model="waybillRouteChangeTab">
<el-tab-pane label="变更路线" name="change"> <el-tab-pane label="变更路线" name="change">
<section-card title="运单地址"> <section-card title="运单地址">
<el-table :data="waybillRouteChangeRows" border> <el-table :data="waybillRouteChangeRows" border>
@@ -3310,7 +3311,7 @@
<el-empty v-else description="暂无路线" :image-size="60" /> <el-empty v-else description="暂无路线" :image-size="60" />
</section-card> </section-card>
<el-form label-width="auto"> <el-form label-width="auto">
<el-form-item label="变更备注"> <el-form-item label="变更备注" style="margin-top: 8px">
<el-input <el-input
v-model="waybillRouteChangeRemark" v-model="waybillRouteChangeRemark"
type="textarea" type="textarea"
@@ -3501,7 +3502,7 @@
批量删除 批量删除
</el-button> </el-button>
</div> </div>
<el-tabs v-model="transportPlanImportTab" class="business-crud-page__import-preview-tabs"> <el-tabs type="border-card" v-model="transportPlanImportTab" class="business-crud-page__import-preview-tabs">
<el-tab-pane :label="`计划单数(${transportPlanImportRows.length}`" name="all" /> <el-tab-pane :label="`计划单数(${transportPlanImportRows.length}`" name="all" />
<el-tab-pane :label="`疑似重复(${transportPlanImportDuplicateCount})`" name="duplicate" /> <el-tab-pane :label="`疑似重复(${transportPlanImportDuplicateCount})`" name="duplicate" />
</el-tabs> </el-tabs>
@@ -3917,7 +3918,7 @@
show-overflow-tooltip show-overflow-tooltip
sortable sortable
/> />
<el-table-column label="操作" width="220" align="center" fixed="right"> <el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="{ row, $index }"> <template #default="{ row, $index }">
<div class="business-crud-page__dispatch-actions"> <div class="business-crud-page__dispatch-actions">
<el-link type="primary" @click="handleDispatchEditRow(row, $index)">编辑</el-link> <el-link type="primary" @click="handleDispatchEditRow(row, $index)">编辑</el-link>
@@ -5623,6 +5624,9 @@ const attachmentViewerPlugins = [
const PageAvueForm = { const PageAvueForm = {
inheritAttrs: false, inheritAttrs: false,
props: {
formPageTitle: { type: String, default: '' },
},
render() { render() {
const attrs = { ...this.$attrs }; const attrs = { ...this.$attrs };
const rowSave = attrs.onRowSave; const rowSave = attrs.onRowSave;
@@ -5643,9 +5647,12 @@ const PageAvueForm = {
slot, slot,
]) ])
); );
return h('div', { class: attrs.option?.dialogCustomClass || 'business-crud-form-page-dialog' }, [ const children = [];
h(resolveComponent('avue-form'), attrs, slots), if (this.formPageTitle) {
]); children.push(h('div', { class: 'archive-page-form__title' }, this.formPageTitle));
}
children.push(h(resolveComponent('avue-form'), attrs, slots));
return h('div', { class: attrs.option?.dialogCustomClass || 'business-crud-form-page-dialog' }, children);
}, },
}; };
@@ -5956,6 +5963,9 @@ export default {
: 'business-crud-form-page-dialog'; : 'business-crud-form-page-dialog';
opt.dialogTop = '0'; opt.dialogTop = '0';
opt.dialogWidth = '100%'; opt.dialogWidth = '100%';
} else if (!opt.dialogCustomClass) {
// body +
opt.dialogCustomClass = 'business-crud-form-dialog';
} }
return opt; return opt;
})(), })(),
@@ -6254,6 +6264,18 @@ export default {
(this.isStandaloneBusinessPage && ['add', 'edit'].includes(this.$route.query.mode)) (this.isStandaloneBusinessPage && ['add', 'edit'].includes(this.$route.query.mode))
); );
}, },
formPageTitle() {
if (
this.isStandaloneFormPage &&
(this.isShippingTemplatePage || this.isTransportPlanPage || this.isWaybillDetailLayout)
) {
return (
this.$route.query.name ||
`${this.$route.query.mode === 'edit' ? '编辑' : '新增'}${this.config.title || ''}`
);
}
return '';
},
crudContainer() { crudContainer() {
return this.isStandaloneFormPage ? 'PageAvueForm' : 'avue-crud'; return this.isStandaloneFormPage ? 'PageAvueForm' : 'avue-crud';
}, },
@@ -6262,13 +6284,14 @@ export default {
const isTransportPlanCreate = const isTransportPlanCreate =
this.config.enableTransportPlanCreateActions === true && this.config.enableTransportPlanCreateActions === true &&
this.$route.query.mode === 'add'; this.$route.query.mode === 'add';
return { const base = {
...this.option, ...this.option,
boxType: this.$route.query.mode === 'edit' ? 'edit' : 'add', boxType: this.$route.query.mode === 'edit' ? 'edit' : 'add',
menuBtn: true, menuBtn: true,
submitBtn: !isTransportPlanCreate, submitBtn: !isTransportPlanCreate,
emptyBtn: false, emptyBtn: false,
}; };
return this.buildFormGroupOption(base);
}, },
isWaybillDetailLayout() { isWaybillDetailLayout() {
return this.config.permission === 'waybill_manage'; return this.config.permission === 'waybill_manage';
@@ -6921,6 +6944,45 @@ export default {
if (mode === 'edit') this.form = { id }; if (mode === 'edit') this.form = { id };
this.beforeOpen(() => {}, mode); this.beforeOpen(() => {}, mode);
}, },
buildFormGroupOption(option) {
const sectionTitleProps = [
'basicInfoTitle',
'templateInfoTitle',
'shippingInfoTitle',
'goodsInfoTitle',
'taskInfoTitle',
'contractFileTitle',
'billingInfoTitle',
'settlementRuleTitle',
'paymentRatioTitle',
'attachmentTitle',
'changeRecordTitle',
];
const columns = [...(option.column || [])].sort(
(a, b) => (b.order || 0) - (a.order || 0)
);
const groups = [];
let current = [];
columns.forEach(col => {
if (sectionTitleProps.includes(col.prop)) {
if (current.length) groups.push(current);
current = [col];
} else {
current.push(col);
}
});
if (current.length) groups.push(current);
if (groups.length <= 1) return option;
return {
...option,
column: groups[0],
group: groups.slice(1).map(group => ({
label: '',
arrow: false,
column: group,
})),
};
},
openContractFormPage(mode, row = {}) { openContractFormPage(mode, row = {}) {
if (this.isStandaloneBusinessPage) { if (this.isStandaloneBusinessPage) {
this.$router.push({ this.$router.push({
@@ -15428,8 +15490,9 @@ export default {
&__transport-plan-detail-head, &__transport-plan-detail-head,
&__transport-plan-waybill-card { &__transport-plan-waybill-card {
border: 1px solid #dfe3e8;
background: #fff; background: #fff;
border-radius: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
} }
&__transport-plan-detail-head { &__transport-plan-detail-head {
@@ -15586,7 +15649,7 @@ export default {
} }
&__transport-plan-waybill-card { &__transport-plan-waybill-card {
margin-top: 8px; //margin-top: 8px;
padding: 16px 14px 12px; padding: 16px 14px 12px;
} }
@@ -16109,7 +16172,7 @@ export default {
border-radius: 6px; border-radius: 6px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
padding: 16px; padding: 16px;
margin-bottom: 16px; margin-bottom: 12px;
.avue-form__group--title { .avue-form__group--title {
padding: 0 !important; padding: 0 !important;
@@ -16125,7 +16188,7 @@ export default {
width: 100% !important; width: 100% !important;
min-height: 100%; min-height: 100%;
box-sizing: border-box; box-sizing: border-box;
padding: 20px 24px 96px; //padding: 20px 24px 96px;
background: #f5f6fa; background: #f5f6fa;
margin: 0 !important; margin: 0 !important;
border-radius: 0; border-radius: 0;
@@ -16286,6 +16349,66 @@ export default {
width: auto; width: auto;
} }
// 使 12px section-card
:global(.contract-manage-form-dialog) {
//padding: 20px 24px 96px;
background: #f5f6fa;
box-sizing: border-box;
}
// ///使 12px section-card
:global(.business-crud-form-page-dialog:not(.el-dialog)) {
//padding: 20px 24px 96px;
box-sizing: border-box;
}
:global(.business-crud-page--form-page .avue-form) {
background: transparent;
padding: 0;
}
:global(.business-crud-page--form-page .avue-form > .el-form > .avue-form__group),
:global(.business-crud-page--form-page .avue-group) {
background: #fff;
border-radius: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
margin-bottom: 12px;
padding: 14px 16px 4px;
overflow: hidden;
}
:global(.business-crud-page--form-page .avue-form > .el-form > .avue-form__group:last-child),
:global(.business-crud-page--form-page .avue-group:last-child) {
margin-bottom: 0;
}
// .avue-group .avue-form__group
:global(.business-crud-page--form-page .avue-form .avue-form__group:has(.dialog-section-title)) {
background: transparent;
border-radius: 0;
box-shadow: none;
padding: 0;
margin-bottom: 0;
}
:global(.business-crud-page--form-page .avue-group .el-collapse) {
border-top: none;
border-bottom: none;
}
:global(.business-crud-page--form-page .avue-group .el-collapse-item__header) {
display: none;
}
:global(.business-crud-page--form-page .avue-group .el-collapse-item__wrap) {
border-bottom: none;
will-change: auto;
}
:global(.business-crud-page--form-page .avue-group .el-collapse-item__content) {
padding-bottom: 0;
}
:global(.avue--collapse .el-overlay:has(.contract-manage-form-dialog)) { :global(.avue--collapse .el-overlay:has(.contract-manage-form-dialog)) {
left: 60px !important; left: 60px !important;
} }
@@ -1735,10 +1735,12 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.master-editor section { .master-editor section {
margin-bottom: 16px; margin-bottom: 12px;
padding: 16px; padding: 16px;
border: 1px solid #eff1f7; border: 1px solid #eff1f7;
background: #fff; background: #fff;
border-radius: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
h3 { h3 {
margin: 0 0 16px; margin: 0 0 16px;
padding-left: 12px; padding-left: 12px;
+13 -5
View File
@@ -388,6 +388,7 @@
@update:model-value="value => !isStandaloneFormPage && (dialogVisible = value)" @update:model-value="value => !isStandaloneFormPage && (dialogVisible = value)"
@closed="resetLoadingDialog" @closed="resetLoadingDialog"
> >
<div v-if="isStandaloneFormPage" class="archive-page-form__title">{{ formPageTitle }}</div>
<div v-loading="dialogLoading" class="loading-manage-dialog__body"> <div v-loading="dialogLoading" class="loading-manage-dialog__body">
<div v-if="dialogReadonly" class="loading-detail"> <div v-if="dialogReadonly" class="loading-detail">
<section-card title="配载单详情"> <section-card title="配载单详情">
@@ -619,7 +620,7 @@
</div> </div>
</section-card> </section-card>
<section-card title="配载信息" class="loading-manage-dialog__load-info-card"> <div title="配载信息" class="loading-manage-dialog__load-info-card">
<section-card title="待配载运单"> <section-card title="待配载运单">
<el-table <el-table
ref="candidateTableRef" ref="candidateTableRef"
@@ -724,7 +725,7 @@
</section-card> </section-card>
<div class="loading-manage-dialog__waybill-route-layout"> <div class="loading-manage-dialog__waybill-route-layout">
<section-card title="待配载列表"> <section-card title="待配载列表" style="min-height: 257px">
<el-table <el-table
:data="selectedWaybillRows" :data="selectedWaybillRows"
border border
@@ -905,7 +906,7 @@
</el-form-item> </el-form-item>
</div> </div>
</section-card> </section-card>
</section-card> </div>
</el-form> </el-form>
</div> </div>
@@ -1228,6 +1229,12 @@ export default {
['add', 'edit'].includes(this.$route.query.mode) ['add', 'edit'].includes(this.$route.query.mode)
); );
}, },
formPageTitle() {
return (
this.$route.query.name ||
`${this.$route.query.mode === 'edit' ? '编辑' : '新增'}配载管理`
);
},
}, },
mounted() { mounted() {
this.loadTransportTypeOptions(); this.loadTransportTypeOptions();
@@ -2352,7 +2359,7 @@ export default {
.loading-manage-page { .loading-manage-page {
.loading-manage-page__search { .loading-manage-page__search {
padding: 12px 12px 4px; padding: 12px 12px 4px;
margin-bottom: 8px; margin-bottom: 20px;
background: #fff; background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
} }
@@ -2472,6 +2479,7 @@ export default {
} }
.loading-manage-dialog { .loading-manage-dialog {
min-height: 12vh;
.loading-manage-dialog__body { .loading-manage-dialog__body {
max-height: 78vh; max-height: 78vh;
overflow-y: auto; overflow-y: auto;
@@ -2504,7 +2512,7 @@ export default {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px; gap: 16px;
align-items: start; align-items: start;
margin-bottom: 16px; //margin-bottom: 12px;
} }
.loading-manage-dialog__candidate-actions, .loading-manage-dialog__candidate-actions,
+14 -9
View File
@@ -116,12 +116,14 @@
</div> </div>
</section> </section>
</template> </template>
<master-order-editor <template v-else-if="mode === 'editor'">
v-else-if="mode === 'editor'" <div class="archive-page-form__title">{{ masterEditorTitle }}</div>
:id="routeId" <master-order-editor
@back="goList" :id="routeId"
@dispatch="goDispatch" @back="goList"
/> @dispatch="goDispatch"
/>
</template>
<master-order-dispatch v-else-if="mode === 'dispatch'" :id="routeId" @back="goList" /> <master-order-dispatch v-else-if="mode === 'dispatch'" :id="routeId" @back="goList" />
<master-order-detail v-else :id="routeId" @back="goList" /> <master-order-detail v-else :id="routeId" @back="goList" />
<el-dialog v-model="confirm.visible" title="提示" width="400px" <el-dialog v-model="confirm.visible" title="提示" width="400px"
@@ -180,6 +182,9 @@ export default {
routeId() { routeId() {
return this.$route.query.id; return this.$route.query.id;
}, },
masterEditorTitle() {
return this.routeId ? '编辑总单' : '新增总单';
},
}, },
watch: { watch: {
'$route.query': { '$route.query': {
@@ -395,7 +400,7 @@ export default {
<style scoped lang="scss"> <style scoped lang="scss">
.master-search { .master-search {
margin-bottom: 8px; margin-bottom: 12px;
padding: 12px 12px 4px; padding: 12px 12px 4px;
background: #fff; background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
@@ -419,9 +424,8 @@ export default {
} }
} }
.master-list-panel { .master-list-panel {
background: #fff;
.toolbar { .toolbar {
padding: 8px; padding: 8px 0;
} }
} }
.master-card { .master-card {
@@ -513,6 +517,7 @@ export default {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
line-height: 1.4; line-height: 1.4;
max-width: 180px;
} }
} }
.route-connector { .route-connector {
+4 -4
View File
@@ -2103,8 +2103,8 @@ export default {
.project-apply-page-form { .project-apply-page-form {
min-height: 100%; min-height: 100%;
padding: 20px 24px 92px; //padding: 20px 24px 92px;
background: #f5f6fa; //background: #f5f6fa;
} }
.project-apply-page__title { .project-apply-page__title {
@@ -2116,10 +2116,10 @@ export default {
font-weight: 600; font-weight: 600;
&::before { &::before {
width: 4px; //width: 4px;
height: 18px; height: 18px;
margin-right: 8px; margin-right: 8px;
background: #409eff; //background: #409eff;
border-radius: 2px; border-radius: 2px;
content: ''; content: '';
} }
+1 -1
View File
@@ -99,7 +99,7 @@
width="120" width="120"
fixed="right" fixed="right"
align="center" align="center"
/><el-table-column label="操作" width="320" fixed="right" align="left" /><el-table-column label="操作" width="120" fixed="right" align="left"
><template #default="{ row }" ><template #default="{ row }"
><div class="voucher-manage-page__actions"> ><div class="voucher-manage-page__actions">
<el-link <el-link
+1 -1
View File
@@ -41,7 +41,7 @@
</section> </section>
<section class="formal-page__table-panel"> <section class="formal-page__table-panel">
<el-tabs v-model="activeSettlementType" @tab-change="handleSettlementTypeChange"> <el-tabs type="border-card" v-model="activeSettlementType" @tab-change="handleSettlementTypeChange">
<el-tab-pane label="应付" name="payable" /> <el-tab-pane label="应付" name="payable" />
<el-tab-pane label="应收" name="receivable" /> <el-tab-pane label="应收" name="receivable" />
</el-tabs> </el-tabs>
+1 -1
View File
@@ -743,7 +743,7 @@ export default {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
min-height: 56px; min-height: 56px;
padding: 12px; //padding: 12px;
} }
&__toolbar-left, &__toolbar-left,
@@ -312,7 +312,7 @@ export default {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
min-height: 56px; min-height: 56px;
padding: 12px; //padding: 12px;
} }
.reconciliation-page__toolbar-right { .reconciliation-page__toolbar-right {
display: flex; display: flex;
+1 -1
View File
@@ -471,7 +471,7 @@ export default {
} }
.exception-disposal-page__table-panel { .exception-disposal-page__table-panel {
background: #fff; //background: #fff;
} }
.exception-disposal-page__toolbar { .exception-disposal-page__toolbar {
+1 -1
View File
@@ -410,7 +410,7 @@ export default {
} }
.risk-disposal-page__table-panel { .risk-disposal-page__table-panel {
background: #fff; //background: #fff;
} }
.risk-disposal-page__toolbar { .risk-disposal-page__toolbar {
@@ -1,38 +1,43 @@
<template> <template>
<el-dialog :model-value="modelValue" title="船舶综合台账" width="96%" top="3vh" append-to-body destroy-on-close @update:model-value="$emit('update:modelValue', $event)"> <el-dialog :model-value="modelValue" title="船舶综合台账" width="96%" top="3vh" append-to-body destroy-on-close @update:model-value="$emit('update:modelValue', $event)">
<div v-loading="detailLoading" class="ship-ledger"> <div v-loading="detailLoading" class="ship-ledger">
<section v-for="section in sections" :key="section.title" class="ship-ledger__section"> <section-card
<div class="dialog-section-title">{{ section.title }}</div> v-for="section in sections"
:key="section.title"
:title="section.title"
class="ship-ledger__section"
>
<el-descriptions :column="3" border> <el-descriptions :column="3" border>
<el-descriptions-item v-for="item in section.items" :key="item.label" :label="item.label"> <el-descriptions-item v-for="item in section.items" :key="item.label" :label="item.label">
{{ display(item) }} {{ display(item) }}
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</section> </section-card>
<section class="ship-ledger__section"> <section-card title="证书附件" class="ship-ledger__section">
<div class="dialog-section-title">证书附件</div>
<div class="ship-ledger__images"> <div class="ship-ledger__images">
<div v-for="item in imageFields" :key="item.prop" class="ship-ledger__image-item"> <div v-for="item in imageFields" :key="item.prop" class="ship-ledger__image-item">
<span>{{ item.label }}</span>
<el-image v-if="shipDetail[item.prop]" :src="shipDetail[item.prop]" :preview-src-list="imageUrls" preview-teleported fit="cover" /> <el-image v-if="shipDetail[item.prop]" :src="shipDetail[item.prop]" :preview-src-list="imageUrls" preview-teleported fit="cover" />
<el-empty v-else :image-size="36" description="暂无附件" /> <el-empty v-else :image-size="36" description="暂无附件" />
<span>{{ item.label }}</span>
</div> </div>
</div> </div>
</section> </section-card>
<el-tabs v-model="activeTab" @tab-change="loadRecords"> <el-tabs type="border-card" v-model="activeTab" class="ship-ledger__tabs" @tab-change="loadRecords">
<el-tab-pane v-for="tab in tabs" :key="tab.name" :label="tab.label" :name="tab.name" /> <el-tab-pane v-for="tab in tabs" :key="tab.name" :label="tab.label" :name="tab.name">
<el-table v-loading="tableLoading" :data="records" border stripe>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column v-for="column in activeColumns" :key="column.prop" :prop="column.prop" :label="column.label" :min-width="column.width || 140" show-overflow-tooltip />
</el-table>
<div class="ship-ledger__pagination"><el-pagination v-model:current-page="page.current" v-model:page-size="page.size" :page-sizes="[10, 20, 50]" :total="page.total" layout="total, sizes, prev, pager, next" @size-change="handlePageChange" @current-change="loadRecords" /></div>
</el-tab-pane>
</el-tabs> </el-tabs>
<el-table v-loading="tableLoading" :data="records" border stripe>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column v-for="column in activeColumns" :key="column.prop" :prop="column.prop" :label="column.label" :min-width="column.width || 140" show-overflow-tooltip />
</el-table>
<div class="ship-ledger__pagination"><el-pagination v-model:current-page="page.current" v-model:page-size="page.size" :page-sizes="[10, 20, 50]" :total="page.total" layout="total, sizes, prev, pager, next" @size-change="handlePageChange" @current-change="loadRecords" /></div>
</div> </div>
</el-dialog> </el-dialog>
</template> </template>
<script> <script>
import { getDetail } from '@/api/transportCapacity/transport-ship'; import { getDetail } from '@/api/transportCapacity/transport-ship';
import SectionCard from '@/components/section-card/main.vue';
import { getList as insurance } from '@/api/vehicle/insurance-record'; import { getList as insurance } from '@/api/vehicle/insurance-record';
import { getList as violation } from '@/api/vehicle/violation-record'; import { getList as violation } from '@/api/vehicle/violation-record';
import { getList as maintenance } from '@/api/vehicle/maintenance-record'; import { getList as maintenance } from '@/api/vehicle/maintenance-record';
@@ -58,6 +63,7 @@ const tabConfigs = [
].map(([name, label, request, cols]) => ({ name, label, request, columns: cols })); ].map(([name, label, request, cols]) => ({ name, label, request, columns: cols }));
export default { export default {
components: { SectionCard },
props: { modelValue: Boolean, ship: { type: Object, default: () => ({}) } }, emits: ['update:modelValue'], props: { modelValue: Boolean, ship: { type: Object, default: () => ({}) } }, emits: ['update:modelValue'],
data() { return { detailLoading: false, tableLoading: false, shipDetail: {}, activeTab: 'insurance', records: [], page: { current: 1, size: 10, total: 0 } }; }, data() { return { detailLoading: false, tableLoading: false, shipDetail: {}, activeTab: 'insurance', records: [], page: { current: 1, size: 10, total: 0 } }; },
computed: { computed: {
@@ -80,11 +86,14 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.ship-ledger__section { margin-bottom: 16px; } .ship-ledger__section { margin-bottom: 12px; }
.ship-ledger__images { display: flex; gap: 16px; overflow-x: auto; padding: 12px 0; } .ship-ledger__tabs { margin-top: 12px;
.ship-ledger__image-item { width: 150px; flex: 0 0 150px; text-align: center; color: #606266; } :deep(.el-tabs__content) { padding: 8px; }
}
.ship-ledger__images { display: flex; gap: 16px; overflow-x: auto; }
.ship-ledger__image-item { width: 160px; flex: 0 0 160px; text-align: center; color: #606266; }
.ship-ledger__image-item > span { display: block; margin-bottom: 8px; } .ship-ledger__image-item > span { display: block; margin-bottom: 8px; }
.ship-ledger__image-item :deep(.el-image) { width: 150px; height: 110px; border: 1px solid #eff1f7; } .ship-ledger__image-item :deep(.el-image) { width: 160px; height: 110px; border: 1px solid #eff1f7; }
.ship-ledger__image-item :deep(.el-empty) { height: 110px; border: 1px solid #eff1f7; } .ship-ledger__image-item :deep(.el-empty) { height: 110px; border: 1px solid #eff1f7; }
.ship-ledger__pagination { display: flex; justify-content: flex-end; margin-top: 16px; } .ship-ledger__pagination { display: flex; justify-content: flex-end; margin-top: 16px; }
</style> </style>
@@ -9,28 +9,47 @@
@update:model-value="$emit('update:modelValue', $event)" @update:model-value="$emit('update:modelValue', $event)"
> >
<div v-loading="detailLoading" class="vehicle-ledger"> <div v-loading="detailLoading" class="vehicle-ledger">
<section class="vehicle-ledger__overview"> <section-card class="vehicle-ledger__overview">
<div class="vehicle-ledger__title"> <div class="vehicle-ledger__title">
<span>{{ vehicleDetail.plateNo || vehicle.plateNo }}</span> <span>{{ vehicleDetail.plateNo || vehicle.plateNo }}</span>
<el-tag :type="plateColorType">{{ vehicleDetail.plateColor || '-' }}</el-tag> <el-tag :type="plateColorType">{{ vehicleDetail.plateColor || '-' }}</el-tag>
<el-tag type="primary">{{ vehicleDetail.businessRelation || '-' }}</el-tag> <el-tag type="primary">{{ vehicleDetail.businessRelation || '-' }}</el-tag>
</div> </div>
<el-descriptions :column="4" border> <el-descriptions :column="4" border>
<el-descriptions-item label="所属组织">{{ vehicleDetail.organizationName || '-' }}</el-descriptions-item> <el-descriptions-item label="所属组织">{{
<el-descriptions-item label="车辆类型">{{ vehicleDetail.vehicleType || '-' }}</el-descriptions-item> vehicleDetail.organizationName || '-'
<el-descriptions-item label="能源类型">{{ vehicleDetail.energyType || '-' }}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item label="车辆类型">{{
vehicleDetail.vehicleType || '-'
}}</el-descriptions-item>
<el-descriptions-item label="能源类型">{{
vehicleDetail.energyType || '-'
}}</el-descriptions-item>
<el-descriptions-item label="外廓尺寸(mm)">{{ dimensions }}</el-descriptions-item> <el-descriptions-item label="外廓尺寸(mm)">{{ dimensions }}</el-descriptions-item>
<el-descriptions-item label="核定载质量(KG)">{{ vehicleDetail.approvedLoadKg ?? '-' }}</el-descriptions-item> <el-descriptions-item label="核定载质量(KG)">{{
<el-descriptions-item label="准牵引总质量(KG)">{{ vehicleDetail.tractionMassKg ?? '-' }}</el-descriptions-item> vehicleDetail.approvedLoadKg ?? '-'
<el-descriptions-item label="行驶证档案编号">{{ vehicleDetail.drivingLicenseNo || '-' }}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item label="道路运输证号">{{ vehicleDetail.roadTransportCertNo || '-' }}</el-descriptions-item> <el-descriptions-item label="准牵引总质量(KG)">{{
<el-descriptions-item label="行驶证有效期">{{ vehicleDetail.drivingLicenseEndDate || '-' }}</el-descriptions-item> vehicleDetail.tractionMassKg ?? '-'
<el-descriptions-item label="道路运输证有效期">{{ vehicleDetail.roadTransportCertEndDate || '-' }}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item label="道路运输证年审有效期">{{ vehicleDetail.annualReviewEndDate || '-' }}</el-descriptions-item> <el-descriptions-item label="行驶证档案编号">{{
vehicleDetail.drivingLicenseNo || '-'
}}</el-descriptions-item>
<el-descriptions-item label="道路运输证号">{{
vehicleDetail.roadTransportCertNo || '-'
}}</el-descriptions-item>
<el-descriptions-item label="行驶证有效期">{{
vehicleDetail.drivingLicenseEndDate || '-'
}}</el-descriptions-item>
<el-descriptions-item label="道路运输证有效期">{{
vehicleDetail.roadTransportCertEndDate || '-'
}}</el-descriptions-item>
<el-descriptions-item label="道路运输证年审有效期">{{
vehicleDetail.annualReviewEndDate || '-'
}}</el-descriptions-item>
</el-descriptions> </el-descriptions>
<div class="vehicle-ledger__images"> <div class="vehicle-ledger__images">
<div v-for="item in imageFields" :key="item.prop" class="vehicle-ledger__image-item"> <div v-for="item in imageFields" :key="item.prop" class="vehicle-ledger__image-item">
<span>{{ item.label }}</span>
<el-image <el-image
v-if="vehicleDetail[item.prop]" v-if="vehicleDetail[item.prop]"
:src="vehicleDetail[item.prop]" :src="vehicleDetail[item.prop]"
@@ -39,35 +58,37 @@
fit="cover" fit="cover"
/> />
<el-empty v-else :image-size="36" description="暂无图片" /> <el-empty v-else :image-size="36" description="暂无图片" />
<span>{{ item.label }}</span>
</div> </div>
</div> </div>
</section> </section-card>
<el-tabs v-model="activeTab" class="vehicle-ledger__tabs" @tab-change="loadRecords"> <el-tabs type="border-card" v-model="activeTab" class="vehicle-ledger__tabs" @tab-change="loadRecords">
<el-tab-pane v-for="tab in tabs" :key="tab.name" :label="tab.label" :name="tab.name" /> <el-tab-pane v-for="tab in tabs" :key="tab.name" :label="tab.label" :name="tab.name">
<el-table v-loading="tableLoading" :data="records" border stripe>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column
v-for="column in activeColumns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:min-width="column.width || 140"
show-overflow-tooltip
/>
</el-table>
<div class="vehicle-ledger__pagination">
<el-pagination
v-model:current-page="page.current"
v-model:page-size="page.size"
:page-sizes="[10, 20, 50]"
:total="page.total"
layout="total, sizes, prev, pager, next"
@size-change="handlePageChange"
@current-change="loadRecords"
/>
</div>
</el-tab-pane>
</el-tabs> </el-tabs>
<el-table v-loading="tableLoading" :data="records" border stripe>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column
v-for="column in activeColumns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:min-width="column.width || 140"
show-overflow-tooltip
/>
</el-table>
<div class="vehicle-ledger__pagination">
<el-pagination
v-model:current-page="page.current"
v-model:page-size="page.size"
:page-sizes="[10, 20, 50]"
:total="page.total"
layout="total, sizes, prev, pager, next"
@size-change="handlePageChange"
@current-change="loadRecords"
/>
</div>
</div> </div>
</el-dialog> </el-dialog>
</template> </template>
@@ -86,56 +107,320 @@ import { getList as getChangeList } from '@/api/vehicle/transport-change-record'
import { getList as getOilElectricList } from '@/api/vehicle/oil-electric-record'; import { getList as getOilElectricList } from '@/api/vehicle/oil-electric-record';
import { getList as getEtcList } from '@/api/vehicle/etc-record'; import { getList as getEtcList } from '@/api/vehicle/etc-record';
import { getList as getOtherExpenseList } from '@/api/vehicle/other-expense-record'; import { getList as getOtherExpenseList } from '@/api/vehicle/other-expense-record';
import SectionCard from '@/components/section-card/main.vue';
const cols = (...items) => items.map(([label, prop, width]) => ({ label, prop, width })); const cols = (...items) => items.map(([label, prop, width]) => ({ label, prop, width }));
const tabConfigs = [ const tabConfigs = [
['insurance', '保险记录', getInsuranceList, cols(['车牌号', 'vehicleNo'], ['保险类型', 'insuranceType'], ['开始日期', 'startDate'], ['结束日期', 'endDate'], ['保费', 'premium'], ['保单号', 'policyNo'])], [
['violation', '违章记录', getViolationList, cols(['车牌号', 'vehicleNo'], ['驾驶人', 'driverName'], ['类型', 'violationType'], ['事项', 'violationItem'], ['违章时间', 'violationTime'], ['罚款金额', 'fineAmount'])], 'insurance',
['maintenance', '维修记录', getMaintenanceList, cols(['车牌号', 'vehicleNo'], ['维修人', 'maintainer'], ['维修时间', 'maintenanceTime'], ['维修位置', 'location'], ['费用', 'cost'], ['维修单位', 'company'])], '保险记录',
['maintenancePlan', '保养记录', getMaintenancePlanList, cols(['车牌号', 'vehicleNo'], ['保养人', 'maintainer'], ['保养时间', 'maintenanceTime'], ['保养项目', 'maintenanceItem'], ['费用', 'cost'], ['下次保养时间', 'nextMaintenanceTime'])], getInsuranceList,
['tire', '换胎记录', getTireList, cols(['车牌号', 'vehicleNo'], ['更换时间', 'replacementTime'], ['轮胎品牌', 'tireBrand'], ['轮胎规格', 'tireSpec'], ['费用', 'cost'])], cols(
['accident', '事故记录', getAccidentList, cols(['车牌号', 'vehicleNo'], ['事故时间', 'accidentTime'], ['事故地点', 'accidentLocation'], ['事故类型', 'accidentType'], ['损失金额', 'lossAmount'])], ['车牌号', 'vehicleNo'],
['annualInspection', '年检记录', getAnnualInspectionList, cols(['车牌号', 'vehicleNo'], ['年检日期', 'inspectionDate'], ['年检有效期至', 'inspectionEndDate'], ['年检机构', 'inspectionOrganization'], ['费用', 'cost'])], ['保险类型', 'insuranceType'],
['mileage', '里程记录', getMileageList, cols(['车牌号', 'vehicleNo'], ['记录时间', 'recordTime'], ['里程数', 'mileage'], ['里程单位', 'mileageUnit'], ['备注', 'remark'])], ['开始日期', 'startDate'],
['change', '变更记录', getChangeList, cols(['车牌号', 'vehicleNo'], ['变更类型', 'changeType'], ['变更时间', 'changeTime'], ['变更前内容', 'beforeChange'], ['变更后内容', 'afterChange'])], ['结束日期', 'endDate'],
['equipment', '设备台账', null, cols(['设备编码', 'equipmentCode'], ['设备名称', 'equipmentName'], ['设备类型', 'equipmentType'], ['安装日期', 'installDate'], ['状态', 'status'])], ['保费', 'premium'],
['oilElectric', '油电台账', getOilElectricList, cols(['车牌号', 'vehicleNo'], ['加油/充电日期', 'recordDate'], ['类型', 'energyType'], ['数量', 'quantity'], ['金额', 'amount'])], ['保单号', 'policyNo']
['etc', 'ETC记录', getEtcList, cols(['车牌号', 'vehicleNo'], ['交易日期', 'transactionDate'], ['交易金额', 'amount'], ['交易地点', 'location'], ['ETC卡号', 'etcCardNo'])], ),
['otherExpense', '其他费用记录', getOtherExpenseList, cols(['车牌号', 'vehicleNo'], ['费用日期', 'expenseDate'], ['费用类型', 'expenseType'], ['金额', 'amount'], ['备注', 'remark'])], ],
[
'violation',
'违章记录',
getViolationList,
cols(
['车牌号', 'vehicleNo'],
['驾驶人', 'driverName'],
['类型', 'violationType'],
['事项', 'violationItem'],
['违章时间', 'violationTime'],
['罚款金额', 'fineAmount']
),
],
[
'maintenance',
'维修记录',
getMaintenanceList,
cols(
['车牌号', 'vehicleNo'],
['维修人', 'maintainer'],
['维修时间', 'maintenanceTime'],
['维修位置', 'location'],
['费用', 'cost'],
['维修单位', 'company']
),
],
[
'maintenancePlan',
'保养记录',
getMaintenancePlanList,
cols(
['车牌号', 'vehicleNo'],
['保养人', 'maintainer'],
['保养时间', 'maintenanceTime'],
['保养项目', 'maintenanceItem'],
['费用', 'cost'],
['下次保养时间', 'nextMaintenanceTime']
),
],
[
'tire',
'换胎记录',
getTireList,
cols(
['车牌号', 'vehicleNo'],
['更换时间', 'replacementTime'],
['轮胎品牌', 'tireBrand'],
['轮胎规格', 'tireSpec'],
['费用', 'cost']
),
],
[
'accident',
'事故记录',
getAccidentList,
cols(
['车牌号', 'vehicleNo'],
['事故时间', 'accidentTime'],
['事故地点', 'accidentLocation'],
['事故类型', 'accidentType'],
['损失金额', 'lossAmount']
),
],
[
'annualInspection',
'年检记录',
getAnnualInspectionList,
cols(
['车牌号', 'vehicleNo'],
['年检日期', 'inspectionDate'],
['年检有效期至', 'inspectionEndDate'],
['年检机构', 'inspectionOrganization'],
['费用', 'cost']
),
],
[
'mileage',
'里程记录',
getMileageList,
cols(
['车牌号', 'vehicleNo'],
['记录时间', 'recordTime'],
['里程数', 'mileage'],
['里程单位', 'mileageUnit'],
['备注', 'remark']
),
],
[
'change',
'变更记录',
getChangeList,
cols(
['车牌号', 'vehicleNo'],
['变更类型', 'changeType'],
['变更时间', 'changeTime'],
['变更前内容', 'beforeChange'],
['变更后内容', 'afterChange']
),
],
[
'equipment',
'设备台账',
null,
cols(
['设备编码', 'equipmentCode'],
['设备名称', 'equipmentName'],
['设备类型', 'equipmentType'],
['安装日期', 'installDate'],
['状态', 'status']
),
],
[
'oilElectric',
'油电台账',
getOilElectricList,
cols(
['车牌号', 'vehicleNo'],
['加油/充电日期', 'recordDate'],
['类型', 'energyType'],
['数量', 'quantity'],
['金额', 'amount']
),
],
[
'etc',
'ETC记录',
getEtcList,
cols(
['车牌号', 'vehicleNo'],
['交易日期', 'transactionDate'],
['交易金额', 'amount'],
['交易地点', 'location'],
['ETC卡号', 'etcCardNo']
),
],
[
'otherExpense',
'其他费用记录',
getOtherExpenseList,
cols(
['车牌号', 'vehicleNo'],
['费用日期', 'expenseDate'],
['费用类型', 'expenseType'],
['金额', 'amount'],
['备注', 'remark']
),
],
].map(([name, label, request, columns]) => ({ name, label, request, columns })); ].map(([name, label, request, columns]) => ({ name, label, request, columns }));
export default { export default {
components: { SectionCard },
props: { modelValue: Boolean, vehicle: { type: Object, default: () => ({}) } }, props: { modelValue: Boolean, vehicle: { type: Object, default: () => ({}) } },
emits: ['update:modelValue'], emits: ['update:modelValue'],
data() { data() {
return { detailLoading: false, tableLoading: false, vehicleDetail: {}, activeTab: 'insurance', records: [], page: { current: 1, size: 10, total: 0 } }; return {
detailLoading: false,
tableLoading: false,
vehicleDetail: {},
activeTab: 'insurance',
records: [],
page: { current: 1, size: 10, total: 0 },
};
}, },
computed: { computed: {
tabs() { return tabConfigs; }, tabs() {
activeConfig() { return tabConfigs.find(item => item.name === this.activeTab) || tabConfigs[0]; }, return tabConfigs;
activeColumns() { return this.activeConfig.columns; }, },
dimensions() { const v = this.vehicleDetail; return [v.outerLength, v.outerWidth, v.outerHeight].filter(item => item !== null && item !== undefined && item !== '').join('×') || '-'; }, activeConfig() {
plateColorType() { return { 黄牌: 'warning', 蓝牌: 'primary', 绿牌: 'success' }[this.vehicleDetail.plateColor] || 'info'; }, return tabConfigs.find(item => item.name === this.activeTab) || tabConfigs[0];
imageFields() { return [['行驶证主页面', 'drivingLicenseImage'], ['行驶证主页反页', 'drivingLicenseMainBack'], ['行驶证副页正页', 'drivingLicenseViceFront'], ['行驶证副页反页', 'drivingLicenseViceBack'], ['道路运输证', 'roadTransportCertImage']].map(([label, prop]) => ({ label, prop })); }, },
imageUrls() { return this.imageFields.map(item => this.vehicleDetail[item.prop]).filter(Boolean); }, activeColumns() {
return this.activeConfig.columns;
},
dimensions() {
const v = this.vehicleDetail;
return (
[v.outerLength, v.outerWidth, v.outerHeight]
.filter(item => item !== null && item !== undefined && item !== '')
.join('×') || '-'
);
},
plateColorType() {
return (
{ 黄牌: 'warning', 蓝牌: 'primary', 绿牌: 'success' }[this.vehicleDetail.plateColor] ||
'info'
);
},
imageFields() {
return [
['行驶证主页面', 'drivingLicenseImage'],
['行驶证主页反页', 'drivingLicenseMainBack'],
['行驶证副页正页', 'drivingLicenseViceFront'],
['行驶证副页反页', 'drivingLicenseViceBack'],
['道路运输证', 'roadTransportCertImage'],
].map(([label, prop]) => ({ label, prop }));
},
imageUrls() {
return this.imageFields.map(item => this.vehicleDetail[item.prop]).filter(Boolean);
},
},
watch: {
modelValue(value) {
if (value) this.initialize();
},
}, },
watch: { modelValue(value) { if (value) this.initialize(); } },
methods: { methods: {
async initialize() { this.activeTab = 'insurance'; this.page.current = 1; this.vehicleDetail = { ...this.vehicle }; if (this.vehicle.id) { this.detailLoading = true; try { const res = await getDetail(this.vehicle.id); this.vehicleDetail = { ...this.vehicle, ...(res.data.data || {}) }; } finally { this.detailLoading = false; } } this.loadRecords(); }, async initialize() {
async loadRecords() { const { request } = this.activeConfig; if (!request) { this.records = []; this.page.total = 0; return; } this.tableLoading = true; try { const res = await request(this.page.current, this.page.size, { vehicleNo: this.vehicleDetail.plateNo || this.vehicle.plateNo, vehicleType: '车辆' }); const data = res.data.data || {}; this.records = data.records || []; this.page.total = data.total || 0; } finally { this.tableLoading = false; } }, this.activeTab = 'insurance';
handlePageChange() { this.page.current = 1; this.loadRecords(); }, this.page.current = 1;
this.vehicleDetail = { ...this.vehicle };
if (this.vehicle.id) {
this.detailLoading = true;
try {
const res = await getDetail(this.vehicle.id);
this.vehicleDetail = { ...this.vehicle, ...(res.data.data || {}) };
} finally {
this.detailLoading = false;
}
}
this.loadRecords();
},
async loadRecords() {
const { request } = this.activeConfig;
if (!request) {
this.records = [];
this.page.total = 0;
return;
}
this.tableLoading = true;
try {
const res = await request(this.page.current, this.page.size, {
vehicleNo: this.vehicleDetail.plateNo || this.vehicle.plateNo,
vehicleType: '车辆',
});
const data = res.data.data || {};
this.records = data.records || [];
this.page.total = data.total || 0;
} finally {
this.tableLoading = false;
}
},
handlePageChange() {
this.page.current = 1;
this.loadRecords();
},
}, },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.vehicle-ledger__overview { padding: 16px; border: 1px solid #eff1f7; } .vehicle-ledger__overview {
.vehicle-ledger__title { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; font-size: 26px; font-weight: 600; } padding: 16px;
.vehicle-ledger__images { display: flex; gap: 16px; margin-top: 16px; overflow-x: auto; } border: 1px solid #eff1f7;
.vehicle-ledger__image-item { width: 150px; flex: 0 0 150px; color: #606266; text-align: center; } }
.vehicle-ledger__image-item > span { display: block; margin-bottom: 8px; } .vehicle-ledger__title {
.vehicle-ledger__image-item :deep(.el-image) { width: 150px; height: 110px; border: 1px solid #eff1f7; } display: flex;
.vehicle-ledger__image-item :deep(.el-empty) { height: 110px; border: 1px solid #eff1f7; } align-items: center;
.vehicle-ledger__tabs { margin-top: 16px; } gap: 12px;
.vehicle-ledger__pagination { display: flex; justify-content: flex-end; margin-top: 16px; } margin-bottom: 16px;
font-size: 26px;
font-weight: 600;
}
.vehicle-ledger__images {
display: flex;
gap: 16px;
margin-top: 16px;
overflow-x: auto;
}
.vehicle-ledger__image-item {
width: 150px;
flex: 0 0 150px;
color: #606266;
text-align: center;
}
.vehicle-ledger__image-item > span {
display: block;
margin-bottom: 8px;
}
.vehicle-ledger__image-item :deep(.el-image) {
width: 150px;
height: 110px;
border: 1px solid #eff1f7;
}
.vehicle-ledger__image-item :deep(.el-empty) {
height: 110px;
border: 1px solid #eff1f7;
}
.vehicle-ledger__tabs {
margin-top: 16px;
:deep(.el-tabs__content) {
padding: 8px;
}
}
.vehicle-ledger__pagination {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
</style> </style>
+2 -20
View File
@@ -4856,26 +4856,8 @@ export default {
.archive-page-form { .archive-page-form {
min-height: 100%; min-height: 100%;
padding: 20px 24px 92px; //padding: 20px 24px 92px;
background: #fff; //background: #fff;
}
.archive-page-form__title {
display: flex;
align-items: center;
margin-bottom: 20px;
color: #303133;
font-size: 18px;
font-weight: 600;
&::before {
width: 4px;
height: 18px;
margin-right: 8px;
background: #409eff;
border-radius: 2px;
content: '';
}
} }
.archive-form__footer { .archive-form__footer {