refactor(transportCapacity): 使用 section-card 重构司机管理弹窗布局

- 将司机管理页面新增/编辑弹窗中四个分组改为使用标准 section-card 组件包裹
- 删除手写的扁平样式和红色竖条,统一使用全局 element-ui 样式维护弹窗视觉风格
- 调整司机证件上传区尺寸为 240x151 px,保持身份证比例并左对齐显示
- 优化弹窗中各表单项布局,提高视觉层次与一致性
- 更新 scoped 样式,移除无用的 section-title 样式,保持代码清洁
This commit is contained in:
2026-08-06 16:53:23 +08:00
parent 53676a1150
commit 02b2e3b673
9 changed files with 1920 additions and 1621 deletions

View File

@@ -23,3 +23,85 @@
- 客商材料、变更记录两个非表单分区也改用 `<section-card>`,其中客商材料的操作按钮通过 `#extra` 插槽放在卡头右侧。 - 客商材料、变更记录两个非表单分区也改用 `<section-card>`,其中客商材料的操作按钮通过 `#extra` 插槽放在卡头右侧。
- 清理了失效的 `.archive-form__group``.customer-material-section``.change-record-section``.section-title` 等 scoped 样式。 - 清理了失效的 `.archive-form__group``.customer-material-section``.change-record-section``.section-title` 等 scoped 样式。
- 验证:`@vue/compiler-sfc` 编译 `customer-archive.vue` OKsass 编译 `element-ui.scss` OKsection-card 标签开合平衡。 - 验证:`@vue/compiler-sfc` 编译 `customer-archive.vue` OKsass 编译 `element-ui.scss` OKsection-card 标签开合平衡。
## 继续推广transportCapacity/driver.vue 弹窗
- 用户反馈司机管理页面(`/transportCapacity/driver`)新增/编辑弹窗的 UI 与「重大项目补录」等不一致,仍是红色竖条 + 字段直接落在灰底上的扁平样式,要求统一为「灰底弹窗 + 白底分组卡片」。
- 改造 `src/views/transportCapacity/driver.vue`
- 4 个分组(基础身份信息 / 驾驶证信息 / 从业资格证信息 / 司机联系信息)由原 `<div class="section-title">` + 扁平 `<el-row>` 改为标准 `<section-card title="...">` 包裹。
- 删除手写的 `.section-title`(红色竖条)、`.driver-form__block`(白底分组卡)、自定义 `.driver-dialog { :deep(.el-dialog__body) { background: #f0f2f5; ... } }` ——以上视觉均由 `src/styles/element-ui.scss` 的全局 `.el-dialog__body { background: #f5f6fa; padding: 16px; }` + `.section-card { ... }` 自动覆盖。
- 弹窗内 4 个分组的二级 row 网格不变3列/4列 12栏布局由每张白卡的 body 承载。
- 教训:第一次改动按 `project-apply.vue` 的手写方式做了一版(手写 `dialog-section-title` override + `__block`),与已落的「全项目通用标准」重复。**今后新页面/新弹窗直接用 `<section-card>`,不要再手写卡片样式**。
## 调整 `/base/fee-item` 弹窗左右表单项间距
- 用户反馈费用项弹窗中「费用类型」与「费用项代码」左右列太挤。
- 修改 `src/views/base/fee-item.vue` 的 scoped 样式:将弹窗内 `.avue-form__row` / `.el-row` 的负边距与 `.el-col` 的水平 padding 从 `36px` 加大到 `48px`,等效 gutter 由 72px 增至 96px。
- 该样式限定在 `.avue-crud__dialog` 内,仅影响新增/编辑弹窗,不影响列表搜索栏。
## 调整 `/vehicle/violation-record` 弹窗两栏横向间距
- 用户反馈违章记录新增弹窗左右两列表单项间距太小,列与列之间距离过近。
- 修改 `src/views/vehicle/violation-record.vue` 的 scoped 样式:限定在 `.el-dialog__body .avue-form` 内,将 `.el-row` 负边距与 `.el-col` 水平 padding 均设为 `24px`,等效 gutter 由默认 `20px` 增至 `48px`
- 仅作用于弹窗内的 Avue 表单,不影响列表搜索栏;保持表单项纵向间距不变。
## 将 `/business/process-config` 弹窗改为 section-card 卡片式布局
- 用户反馈过程配置新增弹窗需将「基本信息」「现行过程节点」「节点设置」等分组改为白卡(参考重大项目补录)。
- 改造 `src/views/business/process-config.vue` + `src/option/business/process-config.js`
- 取消本页 `.avue-crud__dialog .avue-form` 的全局自动白卡(避免白底套白底),改为灰底上直接叠 `<section-card>`
- `basicInfo-form``<section-card title="基本信息">` 包裹;将原独立 Avue 字段 `includedNodes` 改为 formslot 并移入该卡片内部,用 `el-select` multiple 自定义渲染,保留 `searchLabel` 使搜索栏不受影响。
- `nodeConfigJson-form` 拆分为 `<section-card title="现行过程节点">``<section-card title="节点设置">` 两张白卡。
- 新增 `includedNodeValues` 计算属性,实现下拉框与 `nodeRows.enabled` / `form.includedNodes` 的双向同步。
- 添加 `.process-config-page__field-label--required` 样式给必填项标红 `*`
- 通过 scoped 覆盖消除 formslot 外层 `.el-form-item` 的额外 `margin-bottom`,由 section-card 自身控制卡片间距。
- 验证:`@vue/compiler-sfc` 编译 `process-config.vue` template/script OKsass 编译其 scoped style OKsection-card 标签开合平衡。
## 修正 process-config 弹窗「卡片看不见」的真正根因(重要坑)
- 现象:用 `<section-card>` 包了基本信息/现行过程节点/节点设置三张白卡,但弹窗里看不出卡片,用户反馈"还是不对"。
- 排查发现:之前方向的"灰底缺失"是错的——全局 `.el-dialog__body { background:#f5f6fa }``.el-dialog .avue-form { 白卡 }` 早就定义在 `src/styles/element-ui.scss`(216/222 行),灰底一直有。
- **真正根因**Avue CRUD 弹窗默认 `append-to-body:true`DOM 挂在 `<body>` 下、在组件模板之外。组件 scoped 样式里的 `:deep(.avue-crud__dialog .avue-form)` 编译为 `[data-v-x] .avue-form`,要求祖先带 scoped 属性,而 append-to-body 弹窗没有该祖先 → 规则不命中 → 全局 `.el-dialog .avue-form` 白卡未被取消 → 白卡套白卡section-card 被白色表单卡盖住看不见。
- 修正:作用于 append-to-body 弹窗的样式必须用 `:global(...)`(完全全局、不依赖 scoped 祖先)。给 option 加 `dialogCustomClass: 'process-config-dialog'``dialogCustomClass` 由 Avue 直接挂在 `.el-dialog` 根元素上),把所有弹窗内覆盖规则改为 `:global(.process-config-dialog .avue-form)` + `!important`(取消全局 `.el-dialog .avue-form` 白卡,露出灰底),并清零 formslot 的 `.el-form-item`/`.el-col`/`.el-form-item__content`/`.avue-form__row` 占位与内边距让卡片满宽贴边。列表内的 `:deep(.el-table)` 仍有效(列表在组件 DOM 内),弹窗内表格需用 `:global(.process-config-dialog .el-table)`
- **二级坑(本次才真正修对)**DOM 路径里**不要写 `.avue-crud__dialog`**。该 class 只存在于 avue 默认 `index.css`,而 Avue 组件 JSdialog-form3.mjs实际只把 `dialogCustomClass` 加到 `.el-dialog` 根、内部是 `.el-dialog__body > .avue-form``.avue-crud__dialog` 在运行时根本不渲染。之前写 `.process-config-dialog .avue-crud__dialog .avue-form` 整体不命中,白卡一直没被取消 → 卡片照样看不见。正确路径:`dialogCustomClass`(=`.el-dialog`) 直接作祖先,下接 `.avue-form`/`.el-form-item`/`.el-col` 等。
- **三级坑**`includedNodes` 列虽已改 formslot 且在 basicInfo 卡片内手动渲染但它没有关掉表单显示Avue 仍会为其渲染一个**空的 `.el-form-item`**(无对应 `#includedNodes-form` 槽),在「基本信息」与「节点设置」卡片间留下空隙。修正:给 `includedNodes``addDisplay:false, editDisplay:false`(保留 `search:true`+`searchLabel` 不影响搜索栏)。
- 教训(可复用):**凡要给 Avue/Element 的 append-to-body 弹窗el-dialog 默认 append-to-body写 scoped 覆盖样式,一律用 `:global()``:deep()` 在弹窗 DOM 上不生效;且选择器祖先只用 `dialogCustomClass`(挂在 `.el-dialog`),不要误加 `.avue-crud__dialog`(运行时不存在)。** 之前 vehicle/customer-archive、loading-manage 等自定义弹窗若也用 append-to-body弹窗内 `:deep` 覆盖同样可能失效(它们当时多靠全局 `.el-dialog .avue-form`/`.section-card` 兜底,未暴露)。
## 继续推广transportCapacity 三页面弹窗 + 上传区整改
- 用户确认 `/transportCapacity/driver` 改造效果正确,并要求:
- `/transportCapacity/vehicle``/transportCapacity/ship` 两个页面的弹窗也按同样 `<section-card>` 标准整改。
- 上传证件区域整改为:① 宽高比 = 中国二代身份证 1.586:185.6 × 53.98 mm② 区域大小取真实身份证的 50%,固定 165 × 104 px③ 适用范围 = **全部**上传区(含机动车行驶证主页正/反/副页正/反、道路运输证、机动车登记本、船舶所有权/安全环保/国籍/最低安全配员/光船租赁/营业运输 共 6 张 + 行驶证 4 张),居中显示不再 100% 占栏。
- 已落地:`src/views/transportCapacity/driver.vue` + `vehicle.vue` + `ship.vue` 三页面:
- vehicle.vue: 2 个分组 `<div class="section-title">` 替换为 `<section-card title="基础车辆信息/车辆资质图片">`;删除 `.section-title``.section-title:first-child` scoped 规则。
- ship.vue: 2 个分组替换为 `<section-card title="基础船舶信息/船舶证书信息">`6 个子证书仍保留 `.cert-block` + `.cert-block__title` 做内部子分段(外层白卡 + 内层 1px border-top 分隔线,视觉层次合理);删除 `.section-title``.section-title--wide``.section-title:first-child` scoped 规则。
- driver.vue: 之前已用 `<section-card>` 完成,本次仅收尾上传区尺寸。
- 三页面的 `uploader` 样式统一改造:`.xxx-uploader` 取消强制 width:100%;新增 `.xxx-uploader--large { display:block; width:165px; margin:0 auto }``.xxx-uploader--large .el-upload { width:100%; height:104px }``--large` 选择器特定性 0,0,1,0 写在最后覆盖默认规则,源码位置在 `.xxx-uploader` 之后,确保 CSS 顺序覆盖生效)。
- 保留每个 class-prefix 的非 large 默认高度170px、placeholder 边框/背景/居中等样式。
- 验证:`@vue/compiler-sfc` 编译三个 .vue 均 OK`<section-card>` 标签数量 driver 4/4、vehicle 2/2、ship 2/2 平衡vite dev 服务对 `/transportCapacity/{driver,vehicle,ship}` 均返回 200prettier 通过。
- 沉淀到 MEMORY.md 的设计约定:「上传证件区域 = `aspect-ratio: 1.586` + 固定 165 × 104 px + 居中」为全 transportCapacity 模块统一的硬规范。后续若其他模块也使用 `<image-upload-field large>` 应套用同款规范。
## 上调 transportCapacity 三页面上传证件区域尺寸165×104 → 240×151并左对齐
- 用户反馈上一轮的 165×104真实身份证 50%)偏小、看不清楚。
- 三页面(`driver/vehicle/ship.vue`)的 `.xxx-uploader--large` 尺寸统一上调到 **240 × 151 px**(约真实身份证 75%,保持 1.586:1对齐方式由 `margin: 0 auto` 居中改为 `margin: 0` 左对齐。
- 仅改三页面 scoped 样式里的 `width / height / margin` 三处数值,未动模板与组件逻辑;非 large 默认高度 170px 等样式保留。
- 同步更新 MEMORY.md「上传证件区域尺寸规范」为 240×151 + 左对齐。
## 规划:将 process-config.vue 弹窗布局对标 project-apply.vue尚未落地
- 用户要求参考 `/business/project-apply.vue` 重构 `process-config.vue`**主要是 UI 布局**。
- 现状回顾process-config 弹窗卡片**已用 `<section-card>` 包裹**basicInfo / 现行过程节点 / 节点设置),但弹窗机制仍是 **Avue `avue-crud` 表单槽(`#basicInfo-form`/`#nodeConfigJson-form`+ `before-open`/`row-save`/`row-update`**;字段用 `el-row/el-col :span=12` + 手写 `.process-config-page__field`flex 左标签右控件)。
- 目标对标 project-apply① 列表只做表格/搜索,新增/编辑/查看改为**自定义 `el-dialog` + 手写 `el-form`**(去掉 Avue 表单槽与 before-open/row-save/row-update② 字段布局从 `el-row/el-col + 手写 label` 改为 **CSS 两列网格 `.__grid`column-gap 72px+ 原生 `el-form-item` labelright/auto**;③ 卡片继续用 `<section-card>`(项目统一标准,不回头手写白卡);④ 底部「取消/提交」(查看态隐藏提交)。
- 逻辑NODE_TEMPLATES、parseNodeConfig、validateRow、loadProjectOptions、includedNodeValues 计算属性)全部保留,仅把 Avue 钩子改为自定义 `openConfigDialog/submitConfig``api.submit/copy/enable/disable/remove/getDetail` 不变。
- 待确认决策:卡片实现用 `<section-card>`(推荐,符合项目标准)还是 1:1 还原 project-apply 的手写白卡(与项目标准不一致)。用户仅要求先规划,未授权落地。
## 已落地process-config.vue 弹窗重构为自定义 el-dialog对标 project-apply卡片用 section-card
- 已按方案执行:`src/views/business/process-config.vue` 改为「列表 avue-crud仅表格/搜索)+ 自定义 el-dialog 新增/编辑/查看」结构。
- 关键改造:① 删除 `@before-open/@row-save/@row-update``#basicInfo-form`/`#nodeConfigJson-form` 表单槽;② 新增 `el-dialog.process-config-dialog`1440px / top4vh / append-to-body / destroy-on-close+ 手写 `el-form.ref=configForm`label right/auto+ 两列 CSS 网格 `.process-config-form__grid`column-gap 72px③ 三张白卡仍用 `<section-card>`(基本信息 / 现行过程节点 / 节点设置);④ 底部「取消 / 提交」(查看态隐藏提交)。
- 逻辑保留NODE_TEMPLATES、parseNodeConfig、validateRow、loadProjectOptions、includedNodeValues 计算属性、节点表格/时间轴渲染均不变;仅把 Avue 钩子换成 `openConfigDialog/resetConfigDialog/submitConfig`,新增 `formRules` + `configDialogTitle` + `handleValidateFail`
- 样式:删除原 `:global(.process-config-dialog .avue-form …)` 及其 `.el-form-item/.el-col/.avue-form__row` 覆盖(不再用 Avue 表单);新增 `:global(.process-config-dialog .el-dialog__body){ max-height:76vh; overflow-y:auto }`(灰底 16px 已由全局规则提供);`.process-config-form` 作用域样式grid/label min-width160/节点表头 #f5f7fa/偶数行 #fafafa/timeline/require-*)。
- 注意:自定义 el-dialog 是 `append-to-body`,作用于弹窗的覆盖样式仍须 `:global()``.el-dialog__body``.process-config-form` 及其 `:deep()` 因内容由本组件渲染、带 scoped 属性可命中。
- 验证:`@vue/compiler-sfc` 编译 template+script OK、scoped style 括号配平 OK无 beforeOpen/rowSave/forms lot 残留引用。
## 全局组件 image-upload-field 增加点击放大预览
- 用户要求证件图片可点击放大查看。确认方案:用 Element Plus 原生 el-imagepreview-src-list+ 改全局组件一次、driver/vehicle/ship 三页自动生效。
- 改造 `src/components/image-upload-field/main.vue`:① 两模式form / card`<img>` 都换成 `el-image`preview-src-list=[value], fit=contain, preview-teleported, z-index=3000, @click.stop② form 模式图片下方加 `el-link`「更换」、card 模式 head 的 actionText 改 `el-link` 可点;③ 新增 `triggerUpload()``el-upload` 隐藏 `input.el-upload__input``.click()` 触发文件框el-upload 仅 expose abort/submit/clearFiles/handleStart/handleRemove无 openFileDialog须 DOM 方式);④ `@click.stop` 阻止图片点击冒泡到 el-upload 误触发重传;⑤ readonly 时仅预览、无上传入口。
- 兼容性:三页 scoped 样式同时写了 `.xxx-uploader .el-upload`(后代)与 `.xxx-uploader.el-upload`(同元素)两种选择器,把 `.xxx-uploader` 从 el-upload 移到外层 div 后高度样式仍生效;`.xxx-uploader--large` 同理。
- 验证:`@vue/compiler-sfc` 解析 main.vue + 三页均 OKvite dev 对三页均返回 200确认 el-upload 内部隐藏 input 类名为 `.el-upload__input`(源码 `ns.e('input')`)。

View File

@@ -1,16 +1,66 @@
# 项目长期记忆tms-erp-web-ws / Saber3 客商模块) # 项目长期记忆tms-erp-web-ws / Saber3 客商模块)
## 设计约定(经用户确认的决策) ## 设计约定(经用户确认的决策)
- 弹窗内多字段表单分组:**「灰色分组块 + 白色内层卡片」双色方案**(用户最终确认)。
- 外层 `.archive-form__group`:灰底 `#f0f2f5` + 8px 圆角 + `16px 20px` 内边距,分组标题(`.dialog-section-title`,自带 4px 主色竖条)浮于灰底上。 ### 弹窗内多字段表单分组:使用全局 `<section-card>` 组件(项目统一标准)
- 内层字段区:直接把分组内唯一的 `> .el-row` 设为白底(`#fff` + 6px 圆角 + 极淡阴影 `0 1px 2px rgba(0,0,0,.04)` + `18px 16px 4px` 内边距),形成「白卡片浮于灰底」的层次,而非逐个输入框强制白底。
- 理由:契合 AGENTS.md「去卡片化」哲学不嵌套 `el-card`),同时用户明确要灰底+白内容对比,全局 `.dialog-section-title` 直接复用即可。 **整体结构**
- 适用:新增/编辑弹窗中字段超过 ~10 个、需按业务语义分组时 - 整个弹窗 body 由 `src/styles/element-ui.scss` 全局 `.el-dialog__body { background: #f5f6fa; padding: 16px }` 统一灰底(无需页面再覆盖)
- 每个业务分组 = 一张白底卡片。用全局组件 `<section-card title="分组名">...</section-card>`,自动渲染 4px 主色竖条 + 8px 间距的卡头与白底 body圆角 6px + 极淡阴影),与 `loading-manage.vue` / `customer-archive.vue` 等已落地页面一致。
- 严禁再手写 `.xxx-form__block` + `.dialog-section-title` override 拼凑卡片(项目已有 `<section-card>` 标准组件)。
**HTML 结构**(每组三件套):
```vue
<section-card title="分组标题">
<el-row :gutter="18">
<el-col :span="8">...</el-col>
</el-row>
<!-- 同一组的所有 row 都放同一张白卡内 -->
</section-card>
```
**适用**:新增/编辑弹窗中字段超过 ~10 个、需按业务语义分组时。
### 简单分组(仅 3 项)
- 联系信息等仅 3 项的分组,可用 `el-col :span="8"` 让三项均分一行,避免 4 列网格留白。 - 联系信息等仅 3 项的分组,可用 `el-col :span="8"` 让三项均分一行,避免 4 列网格留白。
### 上传证件区域尺寸规范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/views/vehicle/customer-archive.vue`(手写 `el-form`,非 Avue option 驱动;分组结构见 `.archive-form__group`)。 - 通用分组白卡组件:`src/components/section-card/main.vue`(已全局注册为 `<section-card>`,支持 `title` / `#title` / `#extra` slot)。
- 全局弹窗分区标题规范:`src/styles/element-ui.scss``.dialog-section-title` - 弹窗灰底 body + section-card 全局样式:`src/styles/element-ui.scss``.el-dialog__body`、`.section-card { &__header / &__bar / &__title / &__extra / &__body }`
- 司机档案弹窗:`src/views/transportCapacity/driver.vue`(手写 `el-form` + `<section-card>` 4 大分组:基础身份信息 / 驾驶证信息 / 从业资格证信息 / 司机联系信息)。
- 车辆档案弹窗:`src/views/transportCapacity/vehicle.vue`(手写 `el-form` + `<section-card>` 2 大分组:基础车辆信息 / 车辆资质图片6 张大图按身份证比例 240×151 左对齐)。
- 船舶档案弹窗:`src/views/transportCapacity/ship.vue`(手写 `el-form` + `<section-card>` 2 大分组:基础船舶信息 / 船舶证书信息;内部 6 张 cert 图片按身份证比例 240×151 左对齐;子证书分段用 `.cert-block` + border-top
- 客商档案弹窗:`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 代理跨域Saber3 axios 开 `withCredentials=true`,直连绝对地址后端须让 CORS `Allow-Origin` 为具体域名(不能用 `*`),否则浏览器拒绝。改 `.env`/`vite.config.mjs` 必须重启 dev。

View File

@@ -1,7 +1,9 @@
<template> <template>
<el-form-item v-if="mode === 'form'" :label="label" :prop="prop"> <el-form-item v-if="mode === 'form'" :label="label" :prop="prop">
<div :class="[`${classPrefix}-uploader`, { [`${classPrefix}-uploader--large`]: large }]">
<el-upload <el-upload
:class="[`${classPrefix}-uploader`, { [`${classPrefix}-uploader--large`]: large }]" ref="uploadRef"
class="upload-inner"
action="/api/blade-resource/oss/endpoint/put-file" action="/api/blade-resource/oss/endpoint/put-file"
name="file" name="file"
:headers="uploadHeaders" :headers="uploadHeaders"
@@ -12,17 +14,44 @@
:on-success="handleSuccess" :on-success="handleSuccess"
:on-error="handleError" :on-error="handleError"
> >
<img v-if="value" :src="value" :class="`${classPrefix}-uploader__image`" /> <el-image
v-if="value"
:src="value"
:class="`${classPrefix}-uploader__image`"
:preview-src-list="[value]"
fit="contain"
preview-teleported
:z-index="3000"
@click.stop
/>
<div v-else :class="`${classPrefix}-uploader__empty`">{{ emptyText }}</div> <div v-else :class="`${classPrefix}-uploader__empty`">{{ emptyText }}</div>
</el-upload> </el-upload>
<el-link
v-if="value && !readonly"
type="primary"
class="upload-replace-link"
@click.stop="triggerUpload"
>
更换
</el-link>
</div>
</el-form-item> </el-form-item>
<el-form-item v-else :prop="prop" :class="`${classPrefix}-upload-item`"> <el-form-item v-else :prop="prop" :class="`${classPrefix}-upload-item`">
<div :class="[`${classPrefix}-upload-card`, { [`${classPrefix}-upload-card--large`]: large }]"> <div :class="[`${classPrefix}-upload-card`, { [`${classPrefix}-upload-card--large`]: large }]">
<div v-if="label" :class="`${classPrefix}-upload-card__head`"> <div v-if="label" :class="`${classPrefix}-upload-card__head`">
<span :class="`${classPrefix}-upload-card__label`">{{ label }}</span> <span :class="`${classPrefix}-upload-card__label`">{{ label }}</span>
<span :class="`${classPrefix}-upload-card__action`">{{ actionText }}</span> <el-link
v-if="!readonly"
type="primary"
:class="`${classPrefix}-upload-card__action`"
@click.stop="triggerUpload"
>
{{ actionText }}
</el-link>
<span v-else :class="`${classPrefix}-upload-card__action`">{{ actionText }}</span>
</div> </div>
<el-upload <el-upload
ref="uploadRef"
:class="`${classPrefix}-upload-card__uploader`" :class="`${classPrefix}-upload-card__uploader`"
action="/api/blade-resource/oss/endpoint/put-file" action="/api/blade-resource/oss/endpoint/put-file"
name="file" name="file"
@@ -34,7 +63,16 @@
:on-success="handleSuccess" :on-success="handleSuccess"
:on-error="handleError" :on-error="handleError"
> >
<img v-if="value" :src="value" :class="`${classPrefix}-upload-card__image`" /> <el-image
v-if="value"
:src="value"
:class="`${classPrefix}-upload-card__image`"
:preview-src-list="[value]"
fit="contain"
preview-teleported
:z-index="3000"
@click.stop
/>
<div v-else :class="`${classPrefix}-upload-card__empty`">{{ emptyText }}</div> <div v-else :class="`${classPrefix}-upload-card__empty`">{{ emptyText }}</div>
</el-upload> </el-upload>
</div> </div>
@@ -86,6 +124,11 @@ export default {
this.closeUploadLoading(); this.closeUploadLoading();
}, },
methods: { methods: {
triggerUpload() {
const uploadEl = this.$refs.uploadRef && this.$refs.uploadRef.$el;
const input = uploadEl && uploadEl.querySelector('.el-upload__input');
if (input) input.click();
},
showUploadLoading() { showUploadLoading() {
if (this.uploadLoading) { if (this.uploadLoading) {
return; return;

View File

@@ -77,7 +77,8 @@ export const option = applyTableMenuWidth({
rules: textRule('项目', 1000, true), rules: textRule('项目', 1000, true),
}, },
{ {
label: '包含过程节点', label: '',
searchLabel: '包含过程节点',
prop: 'includedNodes', prop: 'includedNodes',
search: true, search: true,
searchOrder: 3, searchOrder: 3,
@@ -85,6 +86,10 @@ export const option = applyTableMenuWidth({
multiple: true, multiple: true,
dicData: nodeOptions, dicData: nodeOptions,
minWidth: 180, minWidth: 180,
formslot: true,
span: 24,
addDisplay: false,
editDisplay: false,
rules: selectRule('包含过程节点'), rules: selectRule('包含过程节点'),
}, },
{ {
@@ -144,5 +149,6 @@ export const option = applyTableMenuWidth({
]), ]),
dialogTop: '10px', dialogTop: '10px',
dialogWidth: '96%', dialogWidth: '96%',
dialogCustomClass: 'process-config-dialog',
searchSpan: 8, searchSpan: 8,
}, 5); }, 5);

View File

@@ -8,10 +8,6 @@
v-model="form" v-model="form"
ref="crud" ref="crud"
:permission="permissionList" :permission="permissionList"
:before-open="beforeOpen"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@search-change="searchChange" @search-change="searchChange"
@search-reset="searchReset" @search-reset="searchReset"
@selection-change="selectionChange" @selection-change="selectionChange"
@@ -26,7 +22,7 @@
icon="el-icon-plus" icon="el-icon-plus"
plain plain
v-if="hasPermission('process_config_add')" v-if="hasPermission('process_config_add')"
@click="$refs.crud.rowAdd()" @click="openConfigDialog('add')"
>新增 >新增
</el-button> </el-button>
<el-button <el-button
@@ -53,297 +49,18 @@
</el-tag> </el-tag>
</template> </template>
<template #basicInfo-form>
<div class="process-config-page__section">
<div class="dialog-section-title">基本信息</div>
<el-row :gutter="16">
<el-col :span="12">
<div class="process-config-page__field">
<span class="process-config-page__field-label">配置名称</span>
<el-input
v-model="form.configName"
:disabled="dialogReadonly"
maxlength="100"
placeholder="请输入"
/>
</div>
</el-col>
<el-col :span="12">
<div class="process-config-page__field">
<span class="process-config-page__field-label">项目</span>
<el-select
v-model="selectedProjectId"
clearable
filterable
remote
:remote-method="loadProjectOptions"
:loading="projectLoading"
:disabled="dialogReadonly"
placeholder="请选择"
style="width: 100%"
@change="handleProjectChange"
>
<el-option
v-for="item in projectOptions"
:key="item.id"
:label="formatProjectLabel(item)"
:value="String(item.id)"
/>
</el-select>
</div>
</el-col>
<el-col :span="12">
<div class="process-config-page__field">
<span class="process-config-page__field-label">默认后台完成运输天数</span>
<div class="process-config-page__finish-days">
<span>默认</span>
<el-input
v-model="form.defaultFinishDays"
:disabled="dialogReadonly"
maxlength="3"
placeholder="X"
@input="handleFinishDaysInput"
/>
<span>天后台完成运输</span>
</div>
</div>
</el-col>
<el-col :span="12">
<div class="process-config-page__field">
<span class="process-config-page__field-label">备注</span>
<el-input
v-model="form.remark"
:disabled="dialogReadonly"
:rows="3"
maxlength="500"
placeholder="请输入"
show-word-limit
type="textarea"
/>
</div>
</el-col>
</el-row>
</div>
</template>
<template #nodeConfigJson-form>
<div class="process-config-page__custom-form">
<div class="dialog-section-title">现行过程节点</div>
<div class="process-config-page__timeline" v-if="enabledNodes.length">
<div class="process-config-page__timeline-line"></div>
<div
v-for="node in enabledNodes"
:key="node.key"
class="process-config-page__timeline-item"
>
<span class="process-config-page__timeline-dot"></span>
<div class="process-config-page__timeline-name">{{ node.name }}</div>
<div class="process-config-page__timeline-desc">
<div v-for="text in timelineDescriptions(node)" :key="text">{{ text }}</div>
</div>
</div>
</div>
<el-empty v-else description="请在节点设置中勾选过程节点" :image-size="72" />
<div class="dialog-section-title">节点设置</div>
<el-table
class="process-config-page__node-table"
border
:data="nodeRows"
:span-method="nodeSpanMethod"
>
<el-table-column label="节点设置" width="136">
<template #default="{ row, $index }">
<div class="process-config-page__node-cell">
<el-checkbox
v-model="row.enabled"
:disabled="dialogReadonly"
@change="syncNodeFields"
/>
<span class="process-config-page__node-name">{{ row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="操作要求" min-width="660">
<template #default="{ row }">
<div class="process-config-page__requirements">
<template v-if="row.key === 'accept'">
<div class="process-config-page__require-line">
<span class="process-config-page__require-label">是否确认</span>
<el-radio-group
v-model="row.confirmMode"
:disabled="dialogReadonly || !row.enabled"
@change="syncNodeFields"
>
<el-radio label="yes"></el-radio>
<el-radio label="no_confirm_accept">无需确认接单</el-radio>
</el-radio-group>
</div>
<div class="process-config-page__require-line">
<span class="process-config-page__require-label">确认接单人</span>
<el-checkbox
v-model="row.confirmDriver"
:disabled="dialogReadonly || !row.enabled || row.confirmMode !== 'yes'"
@change="syncNodeFields"
>司机</el-checkbox
>
</div>
</template>
<template v-else-if="row.key === 'return'">
<div class="process-config-page__require-line">
<span class="process-config-page__require-label">是否确认</span>
<el-radio-group
v-model="row.confirmMode"
:disabled="dialogReadonly || !row.enabled"
@change="syncNodeFields"
>
<el-radio label="yes"></el-radio>
<el-radio label="no_confirm_complete">无需确认完成</el-radio>
</el-radio-group>
</div>
<div class="process-config-page__require-line">
<span class="process-config-page__require-label">确认完成</span>
<el-checkbox
v-model="row.confirmInternal"
:disabled="dialogReadonly || !row.enabled || row.confirmMode !== 'yes'"
@change="syncNodeFields"
>内部人员</el-checkbox
>
</div>
</template>
<template v-else>
<div class="process-config-page__require-grid">
<div class="process-config-page__require-line">
<span class="process-config-page__require-label">是否打卡</span>
<el-radio-group
v-model="row.punch"
:disabled="dialogReadonly || !row.enabled"
@change="syncNodeFields"
>
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</div>
<div class="process-config-page__require-line" v-if="row.supportCargo">
<span class="process-config-page__require-label">上传货量</span>
<el-radio-group
v-model="row.uploadCargo"
:disabled="dialogReadonly || !row.enabled"
@change="syncNodeFields"
>
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</div>
<div
class="process-config-page__require-line process-config-page__require-line--wide"
v-if="row.key === 'transit'"
>
<span class="process-config-page__require-label">打卡频次</span>
<el-input
v-model="row.frequencyDays"
:disabled="dialogReadonly || !row.enabled || !row.punch"
maxlength="3"
@input="value => handleFrequencyInput(row, value)"
/>
<span>天打卡1次</span>
</div>
<div class="process-config-page__require-line">
<span class="process-config-page__require-label">打卡定位</span>
<el-radio-group
v-model="row.location"
:disabled="dialogReadonly || !row.enabled || !row.punch"
@change="syncNodeFields"
>
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</div>
<div class="process-config-page__require-line" v-if="row.supportCargo">
<span class="process-config-page__require-label">上传货量类型</span>
<el-checkbox-group
v-model="row.cargoTypes"
:disabled="dialogReadonly || !row.enabled || !row.uploadCargo"
@change="syncNodeFields"
>
<el-checkbox label="重量">重量</el-checkbox>
<el-checkbox label="体积">体积</el-checkbox>
<el-checkbox label="数量">数量</el-checkbox>
</el-checkbox-group>
</div>
<div
class="process-config-page__require-line process-config-page__require-line--wide"
v-if="row.key === 'transit'"
>
<span class="process-config-page__require-label">打卡时段</span>
<el-time-picker
v-model="row.timeStart"
:disabled="dialogReadonly || !row.enabled || !row.punch"
format="HH:mm"
value-format="HH:mm"
placeholder="开始"
/>
<span>-</span>
<el-time-picker
v-model="row.timeEnd"
:disabled="dialogReadonly || !row.enabled || !row.punch"
format="HH:mm"
value-format="HH:mm"
placeholder="结束"
/>
</div>
</div>
</template>
</div>
</template>
</el-table-column>
<el-table-column label="附件要求" min-width="320">
<template #default="{ row }">
<div class="process-config-page__attachments" v-if="row.supportVoucher">
<div class="process-config-page__require-line">
<span class="process-config-page__require-label">上传凭证</span>
<el-radio-group
v-model="row.uploadVoucher"
:disabled="dialogReadonly || !row.enabled"
@change="syncNodeFields"
>
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</div>
<div class="process-config-page__require-line">
<span class="process-config-page__require-label">凭证类型</span>
<el-checkbox-group
v-model="row.voucherTypes"
:disabled="dialogReadonly || !row.enabled || !row.uploadVoucher"
@change="syncNodeFields"
>
<el-checkbox v-for="item in row.voucherOptions" :key="item" :label="item">
{{ item }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</template>
</el-table-column>
</el-table>
</div>
</template>
<template #menu="{ row, index }"> <template #menu="{ row, index }">
<el-link <el-link
type="primary" type="primary"
v-if="hasPermission('process_config_view')" v-if="hasPermission('process_config_view')"
@click="$refs.crud.rowView(row, index)" @click="openConfigDialog('view', row)"
> >
查看 查看
</el-link> </el-link>
<el-link <el-link
type="primary" type="primary"
v-if="canEdit(row)" v-if="canEdit(row)"
@click="$refs.crud.rowEdit(row, index)" @click="openConfigDialog('edit', row)"
> >
编辑 编辑
</el-link> </el-link>
@@ -384,6 +101,317 @@
@current-change="currentChange" @current-change="currentChange"
@load="onLoad(page, query)" @load="onLoad(page, query)"
/> />
<el-dialog
v-model="configBox"
:title="configDialogTitle"
append-to-body
destroy-on-close
width="1440px"
top="4vh"
class="process-config-dialog"
@closed="resetConfigDialog"
>
<el-form
ref="configForm"
:model="form"
:rules="formRules"
label-position="right"
label-width="auto"
class="process-config-form"
:disabled="dialogReadonly"
>
<section-card title="基本信息">
<div class="process-config-form__grid">
<el-form-item label="配置名称" prop="configName">
<el-input
v-model="form.configName"
maxlength="100"
placeholder="请输入"
/>
</el-form-item>
<el-form-item label="项目" prop="projectNames">
<el-select
v-model="selectedProjectId"
clearable
filterable
remote
:remote-method="loadProjectOptions"
:loading="projectLoading"
placeholder="请选择"
style="width: 100%"
@change="handleProjectChange"
>
<el-option
v-for="item in projectOptions"
:key="item.id"
:label="formatProjectLabel(item)"
:value="String(item.id)"
/>
</el-select>
</el-form-item>
<el-form-item label="默认后台完成运输天数">
<div class="process-config-form__finish-days">
<span>默认</span>
<el-input
v-model="form.defaultFinishDays"
maxlength="3"
placeholder="X"
@input="handleFinishDaysInput"
/>
<span>天后台完成运输</span>
</div>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="form.remark"
:rows="3"
maxlength="500"
placeholder="请输入"
show-word-limit
type="textarea"
/>
</el-form-item>
<el-form-item label="包含过程节点" prop="includedNodes" class="process-config-form__span-2">
<el-select
v-model="includedNodeValues"
multiple
clearable
filterable
collapse-tags
placeholder="请选择包含过程节点"
style="width: 100%"
>
<el-option
v-for="item in nodeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</div>
</section-card>
<section-card title="现行过程节点">
<div class="process-config-form__timeline" v-if="enabledNodes.length">
<div class="process-config-form__timeline-line"></div>
<div
v-for="node in enabledNodes"
:key="node.key"
class="process-config-form__timeline-item"
>
<span class="process-config-form__timeline-dot"></span>
<div class="process-config-form__timeline-name">{{ node.name }}</div>
<div class="process-config-form__timeline-desc">
<div v-for="text in timelineDescriptions(node)" :key="text">{{ text }}</div>
</div>
</div>
</div>
<el-empty v-else description="请在节点设置中勾选过程节点" :image-size="72" />
</section-card>
<section-card title="节点设置">
<el-table
class="process-config-form__node-table"
border
:data="nodeRows"
:span-method="nodeSpanMethod"
>
<el-table-column label="节点设置" width="136">
<template #default="{ row, $index }">
<div class="process-config-form__node-cell">
<el-checkbox
v-model="row.enabled"
:disabled="dialogReadonly"
@change="syncNodeFields"
/>
<span class="process-config-form__node-name">{{ row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="操作要求" min-width="660">
<template #default="{ row }">
<div class="process-config-form__requirements">
<template v-if="row.key === 'accept'">
<div class="process-config-form__require-line">
<span class="process-config-form__require-label">是否确认</span>
<el-radio-group
v-model="row.confirmMode"
:disabled="dialogReadonly || !row.enabled"
@change="syncNodeFields"
>
<el-radio label="yes"></el-radio>
<el-radio label="no_confirm_accept">无需确认接单</el-radio>
</el-radio-group>
</div>
<div class="process-config-form__require-line">
<span class="process-config-form__require-label">确认接单人</span>
<el-checkbox
v-model="row.confirmDriver"
:disabled="dialogReadonly || !row.enabled || row.confirmMode !== 'yes'"
@change="syncNodeFields"
>司机</el-checkbox
>
</div>
</template>
<template v-else-if="row.key === 'return'">
<div class="process-config-form__require-line">
<span class="process-config-form__require-label">是否确认</span>
<el-radio-group
v-model="row.confirmMode"
:disabled="dialogReadonly || !row.enabled"
@change="syncNodeFields"
>
<el-radio label="yes"></el-radio>
<el-radio label="no_confirm_complete">无需确认完成</el-radio>
</el-radio-group>
</div>
<div class="process-config-form__require-line">
<span class="process-config-form__require-label">确认完成</span>
<el-checkbox
v-model="row.confirmInternal"
:disabled="dialogReadonly || !row.enabled || row.confirmMode !== 'yes'"
@change="syncNodeFields"
>内部人员</el-checkbox
>
</div>
</template>
<template v-else>
<div class="process-config-form__require-grid">
<div class="process-config-form__require-line">
<span class="process-config-form__require-label">是否打卡</span>
<el-radio-group
v-model="row.punch"
:disabled="dialogReadonly || !row.enabled"
@change="syncNodeFields"
>
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</div>
<div class="process-config-form__require-line" v-if="row.supportCargo">
<span class="process-config-form__require-label">上传货量</span>
<el-radio-group
v-model="row.uploadCargo"
:disabled="dialogReadonly || !row.enabled"
@change="syncNodeFields"
>
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</div>
<div
class="process-config-form__require-line process-config-form__require-line--wide"
v-if="row.key === 'transit'"
>
<span class="process-config-form__require-label">打卡频次</span>
<el-input
v-model="row.frequencyDays"
:disabled="dialogReadonly || !row.enabled || !row.punch"
maxlength="3"
@input="value => handleFrequencyInput(row, value)"
/>
<span>天打卡1次</span>
</div>
<div class="process-config-form__require-line">
<span class="process-config-form__require-label">打卡定位</span>
<el-radio-group
v-model="row.location"
:disabled="dialogReadonly || !row.enabled || !row.punch"
@change="syncNodeFields"
>
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</div>
<div class="process-config-form__require-line" v-if="row.supportCargo">
<span class="process-config-form__require-label">上传货量类型</span>
<el-checkbox-group
v-model="row.cargoTypes"
:disabled="dialogReadonly || !row.enabled || !row.uploadCargo"
@change="syncNodeFields"
>
<el-checkbox label="重量">重量</el-checkbox>
<el-checkbox label="体积">体积</el-checkbox>
<el-checkbox label="数量">数量</el-checkbox>
</el-checkbox-group>
</div>
<div
class="process-config-form__require-line process-config-form__require-line--wide"
v-if="row.key === 'transit'"
>
<span class="process-config-form__require-label">打卡时段</span>
<el-time-picker
v-model="row.timeStart"
:disabled="dialogReadonly || !row.enabled || !row.punch"
format="HH:mm"
value-format="HH:mm"
placeholder="开始"
/>
<span>-</span>
<el-time-picker
v-model="row.timeEnd"
:disabled="dialogReadonly || !row.enabled || !row.punch"
format="HH:mm"
value-format="HH:mm"
placeholder="结束"
/>
</div>
</div>
</template>
</div>
</template>
</el-table-column>
<el-table-column label="附件要求" min-width="320">
<template #default="{ row }">
<div class="process-config-form__attachments" v-if="row.supportVoucher">
<div class="process-config-form__require-line">
<span class="process-config-form__require-label">上传凭证</span>
<el-radio-group
v-model="row.uploadVoucher"
:disabled="dialogReadonly || !row.enabled"
@change="syncNodeFields"
>
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</div>
<div class="process-config-form__require-line">
<span class="process-config-form__require-label">凭证类型</span>
<el-checkbox-group
v-model="row.voucherTypes"
:disabled="dialogReadonly || !row.enabled || !row.uploadVoucher"
@change="syncNodeFields"
>
<el-checkbox v-for="item in row.voucherOptions" :key="item" :label="item">
{{ item }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</template>
</el-table-column>
</el-table>
</section-card>
</el-form>
<template #footer>
<div class="process-config-dialog__footer">
<el-button @click="configBox = false">取消</el-button>
<el-button
v-if="!dialogReadonly"
type="primary"
:loading="submitLoading"
@click="submitConfig"
>
提交
</el-button>
</div>
</template>
</el-dialog>
</basic-container> </basic-container>
</template> </template>
@@ -392,6 +420,7 @@ import * as api from '@/api/business/process-config';
import { getList as getProjectList } from '@/api/business/project-apply'; import { getList as getProjectList } from '@/api/business/project-apply';
import { exportBlob } from '@/api/common'; import { exportBlob } from '@/api/common';
import { config, option } from '@/option/business/process-config'; import { config, option } from '@/option/business/process-config';
import { nodeOptions } from '@/option/business/common';
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import { downloadXls } from '@/utils/util'; import { downloadXls } from '@/utils/util';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
@@ -488,6 +517,41 @@ export default {
total: 0, total: 0,
}, },
selectionList: [], selectionList: [],
configBox: false,
dialogType: 'add',
submitLoading: false,
formRules: {
configName: [
{ required: true, message: '请输入配置名称', trigger: 'blur' },
{ max: 100, message: '配置名称不能超过100个字符', trigger: 'blur' },
],
projectNames: [{ required: true, message: '请选择项目', trigger: 'change' }],
defaultFinishDays: [
{
validator: (rule, value, callback) => {
if (value && !/^[1-9]\d{0,2}$/.test(String(value))) {
callback(new Error('请输入1-999的正整数'));
} else {
callback();
}
},
trigger: 'blur',
},
],
remark: [{ max: 500, message: '备注不能超过500个字符', trigger: 'blur' }],
includedNodes: [
{
validator: (rule, value, callback) => {
if (!this.includedNodeValues || !this.includedNodeValues.length) {
callback(new Error('请至少选择一个过程节点'));
} else {
callback();
}
},
trigger: 'change',
},
],
},
}; };
}, },
computed: { computed: {
@@ -497,6 +561,23 @@ export default {
addBtn: this.hasPermission('process_config_add'), addBtn: this.hasPermission('process_config_add'),
}; };
}, },
includedNodeValues: {
get() {
return String(this.form.includedNodes || '')
.split(',')
.map(item => item.trim())
.filter(Boolean);
},
set(value) {
const selected = value || [];
this.form.includedNodes = selected.join(',');
const selectedNames = new Set(selected);
this.nodeRows.forEach(node => {
node.enabled = selectedNames.has(node.name);
});
this.syncNodeFields();
},
},
isAdmin() { isAdmin() {
const authority = this.userInfo && this.userInfo.authority; const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority) return Array.isArray(authority)
@@ -509,6 +590,10 @@ export default {
enabledNodes() { enabledNodes() {
return this.nodeRows.filter(item => item.enabled); return this.nodeRows.filter(item => item.enabled);
}, },
configDialogTitle() {
const text = { add: '新增', edit: '编辑', view: '查看' }[this.dialogType] || '新增';
return `${text}过程配置`;
},
}, },
created() { created() {
this.resetNodeRows(); this.resetNodeRows();
@@ -769,48 +854,75 @@ export default {
} }
return true; return true;
}, },
stopSubmitLoading(loading) { openConfigDialog(type, row = {}) {
if (typeof loading === 'function') { this.dialogType = type;
loading(); this.dialogReadonly = type === 'view';
} this.configBox = true;
}, if (type === 'add') {
rowSave(row, done, loading) { this.form = {
this.syncNodeFields(); configName: '',
const submitRow = this.normalizeRow(row); projectIds: '',
if (!this.validateRow(submitRow)) { projectNames: '',
this.stopSubmitLoading(loading); defaultFinishDays: '',
remark: '',
status: 0,
};
this.selectedProjectId = '';
this.resetNodeRows();
return; return;
} }
api.submit(submitRow).then( if (['edit', 'view'].includes(type)) {
api.getDetail(row.id).then(res => {
this.form = res.data.data || {};
this.selectedProjectId = this.form.projectIds ? String(this.form.projectIds) : '';
this.ensureSelectedProjectOption();
this.parseNodeConfig(this.form);
});
}
},
resetConfigDialog() {
this.$refs.configForm?.clearValidate();
this.form = {};
this.selectedProjectId = '';
this.resetNodeRows();
},
handleValidateFail() {
this.$message.warning('请完善表单必填项后再提交');
this.$nextTick(() => {
const field = this.$el.querySelector('.is-error');
if (field && typeof field.scrollIntoView === 'function') {
field.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
},
submitConfig() {
this.$refs.configForm.validate(valid => {
if (!valid) {
this.handleValidateFail();
return;
}
this.syncNodeFields();
const submitRow = this.normalizeRow(this.form);
if (!this.validateRow(submitRow)) {
return;
}
this.submitLoading = true;
api
.submit(submitRow)
.then(
() => { () => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' }); this.$message({ type: 'success', message: '操作成功!' });
done(); this.configBox = false;
this.onLoad(this.page);
}, },
error => { error => {
window.console.log(error); window.console.log(error);
this.stopSubmitLoading(loading);
} }
); )
}, .finally(() => {
rowUpdate(row, index, done, loading) { this.submitLoading = false;
this.syncNodeFields(); });
const submitRow = this.normalizeRow(row); });
if (!this.validateRow(submitRow)) {
this.stopSubmitLoading(loading);
return;
}
api.submit(submitRow).then(
() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
window.console.log(error);
this.stopSubmitLoading(loading);
}
);
}, },
rowDel(row) { rowDel(row) {
this.$confirm('确定将选择数据删除?', { this.$confirm('确定将选择数据删除?', {
@@ -854,34 +966,6 @@ export default {
} }
this.selectionClear(); this.selectionClear();
}, },
beforeOpen(done, type) {
this.dialogReadonly = type === 'view';
if (type === 'add') {
this.form = {
configName: '',
projectIds: '',
projectNames: '',
defaultFinishDays: '',
remark: '',
status: 0,
};
this.selectedProjectId = '';
this.resetNodeRows();
done();
return;
}
if (['edit', 'view'].includes(type)) {
api.getDetail(this.form.id).then(res => {
this.form = res.data.data || {};
this.selectedProjectId = this.form.projectIds ? String(this.form.projectIds) : '';
this.ensureSelectedProjectOption();
this.parseNodeConfig(this.form);
done();
});
return;
}
done();
},
searchReset() { searchReset() {
this.query = {}; this.query = {};
this.page.currentPage = 1; this.page.currentPage = 1;
@@ -986,24 +1070,44 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.process-config-page { .process-config-page {
&__section { :deep(.avue-crud__header) {
margin-top: 12px;
background: transparent;
}
:deep(.el-table) {
--el-table-border-color: #eff1f7;
}
}
.process-config-form {
padding: 0;
:deep(.el-input),
:deep(.el-select),
:deep(.el-date-editor) {
width: 100%; width: 100%;
} }
&__field { :deep(.el-form-item) {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 16px; margin-bottom: 16px;
} }
&__field-label { :deep(.el-form-item__label) {
flex: 0 0 150px; min-width: 160px;
line-height: 32px; white-space: normal;
color: #303133; line-height: 20px;
font-weight: 500; }
text-align: right;
white-space: nowrap; &__grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
column-gap: 72px;
row-gap: 0;
}
&__span-2 {
grid-column: 1 / -1;
} }
&__finish-days { &__finish-days {
@@ -1011,15 +1115,11 @@ export default {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
.el-input { :deep(.el-input) {
width: 88px; width: 88px;
} }
} }
&__custom-form {
width: 100%;
}
&__timeline { &__timeline {
position: relative; position: relative;
display: flex; display: flex;
@@ -1067,7 +1167,21 @@ export default {
} }
&__node-table { &__node-table {
margin-top: 8px; margin-top: 0;
:deep(.el-table__header th) {
background: #f5f7fa;
color: #303133;
font-weight: 600;
}
:deep(.el-table__cell) {
border-color: #eff1f7;
}
:deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
} }
&__node-cell { &__node-cell {
@@ -1107,11 +1221,11 @@ export default {
min-height: 30px; min-height: 30px;
gap: 10px; gap: 10px;
.el-input { :deep(.el-input) {
width: 62px; width: 62px;
} }
.el-time-editor.el-input { :deep(.el-time-editor.el-input) {
width: 116px; width: 116px;
} }
} }
@@ -1126,18 +1240,20 @@ export default {
font-weight: 500; font-weight: 500;
text-align: right; text-align: right;
} }
}
:deep(.avue-crud__header) { // 弹窗内容区:灰底 + 16px 内边距(与 project-apply 一致),超出在窗内滚动
margin-top: 12px; :global(.process-config-dialog .el-dialog__body) {
background: transparent; max-height: 76vh;
} overflow-y: auto;
overflow-x: hidden;
padding: 16px;
background: #f5f6fa;
}
:deep(.el-table) { .process-config-dialog__footer {
--el-table-border-color: #eff1f7; display: flex;
} justify-content: flex-end;
gap: 8px;
:deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
} }
</style> </style>

View File

@@ -92,21 +92,13 @@
</span> </span>
</template> </template>
<template #menu="{ row }"> <template #menu="{ row }">
<el-link <el-link type="primary" v-if="hasPermission('driver_view')" @click="openDriver(row, true)">
type="primary"
v-if="hasPermission('driver_view')"
@click="openDriver(row, true)"
>
查看 查看
</el-link> </el-link>
<el-link type="primary" v-if="hasPermission('driver_edit')" @click="openDriver(row)"> <el-link type="primary" v-if="hasPermission('driver_edit')" @click="openDriver(row)">
修改 修改
</el-link> </el-link>
<el-link <el-link type="primary" v-if="hasPermission('driver_status')" @click="handleStatus(row)">
type="primary"
v-if="hasPermission('driver_status')"
@click="handleStatus(row)"
>
{{ row.status === 1 ? '停用' : '启用' }} {{ row.status === 1 ? '停用' : '启用' }}
</el-link> </el-link>
</template> </template>
@@ -129,7 +121,7 @@
:disabled="readonly" :disabled="readonly"
class="driver-form" class="driver-form"
> >
<div class="section-title">基础身份信息</div> <section-card title="基础身份信息">
<el-row :gutter="18"> <el-row :gutter="18">
<el-col :span="8"> <el-col :span="8">
<el-form-item label="司机姓名" prop="driverName"> <el-form-item label="司机姓名" prop="driverName">
@@ -148,7 +140,11 @@
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
<el-form-item label="出生年月" prop="birthday"> <el-form-item label="出生年月" prop="birthday">
<el-date-picker v-model="driverForm.birthday" type="date" value-format="YYYY-MM-DD" /> <el-date-picker
v-model="driverForm.birthday"
type="date"
value-format="YYYY-MM-DD"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@@ -164,7 +160,12 @@
<el-col :span="8"> <el-col :span="8">
<el-form-item label="民族" prop="nation"> <el-form-item label="民族" prop="nation">
<el-select v-model="driverForm.nation" clearable filterable> <el-select v-model="driverForm.nation" clearable filterable>
<el-option v-for="item in nationOptions" :key="item" :label="item" :value="item" /> <el-option
v-for="item in nationOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -246,8 +247,9 @@
/> />
</el-col> </el-col>
</el-row> </el-row>
</section-card>
<div class="section-title">驾驶证信息</div> <section-card title="驾驶证信息">
<el-row :gutter="18"> <el-row :gutter="18">
<el-col :span="6"> <el-col :span="6">
<el-form-item label="准驾车型" prop="drivingType"> <el-form-item label="准驾车型" prop="drivingType">
@@ -316,8 +318,7 @@
:headers="uploadHeaders" :headers="uploadHeaders"
large large
@success=" @success="
(url, file) => (url, file) => handleDrivingLicenseUploadSuccess('drivingLicenseFront', url, file)
handleDrivingLicenseUploadSuccess('drivingLicenseFront', url, file)
" "
/> />
</el-col> </el-col>
@@ -335,8 +336,9 @@
/> />
</el-col> </el-col>
</el-row> </el-row>
</section-card>
<div class="section-title">从业资格证信息</div> <section-card title="从业资格证信息">
<el-row :gutter="18"> <el-row :gutter="18">
<el-col :span="6"> <el-col :span="6">
<el-form-item label="从业资格认证类型" prop="qualificationType"> <el-form-item label="从业资格认证类型" prop="qualificationType">
@@ -413,8 +415,9 @@
/> />
</el-col> </el-col>
</el-row> </el-row>
</section-card>
<div class="section-title">司机联系信息</div> <section-card title="司机联系信息">
<el-row :gutter="18"> <el-row :gutter="18">
<el-col :span="8"> <el-col :span="8">
<el-form-item label="司机类型" prop="driverType"> <el-form-item label="司机类型" prop="driverType">
@@ -472,6 +475,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
</section-card>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="driverBox = false">取消</el-button> <el-button @click="driverBox = false">取消</el-button>
@@ -1330,19 +1334,6 @@ export default {
} }
.driver-form { .driver-form {
.section-title {
margin: 18px 0 14px;
padding-left: 10px;
border-left: 3px solid #e74b5f;
color: #303133;
font-weight: 600;
line-height: 18px;
}
.section-title:first-child {
margin-top: 0;
}
:deep(.el-date-editor.el-input), :deep(.el-date-editor.el-input),
:deep(.el-date-editor.el-input__wrapper), :deep(.el-date-editor.el-input__wrapper),
:deep(.el-cascader), :deep(.el-cascader),
@@ -1358,9 +1349,9 @@ export default {
} }
} }
// 大尺寸证件照:按身份证 1.586:1 比例缩放(约真实尺寸 75%240×151左对齐显示
:deep(.driver-uploader) { :deep(.driver-uploader) {
display: block; display: block;
width: 100%;
} }
:deep(.driver-uploader .el-upload), :deep(.driver-uploader .el-upload),
@@ -1375,9 +1366,17 @@ export default {
overflow: hidden; overflow: hidden;
} }
// 大尺寸模式:缩成 1.586:1 比例,左对齐(不再 100% 占栏)
:deep(.driver-uploader--large) {
display: block;
width: 240px;
margin: 0;
}
:deep(.driver-uploader--large .el-upload), :deep(.driver-uploader--large .el-upload),
:deep(.driver-uploader--large.el-upload) { :deep(.driver-uploader--large.el-upload) {
height: 260px; width: 100%;
height: 151px;
} }
:deep(.driver-uploader__image) { :deep(.driver-uploader__image) {
@@ -1396,6 +1395,7 @@ export default {
color: #606266; color: #606266;
} }
// 弹窗内可滚动(弹窗 body 灰底已由全局 .el-dialog__body 规则生效)
.driver-dialog { .driver-dialog {
:deep(.el-dialog__body) { :deep(.el-dialog__body) {
max-height: 72vh; max-height: 72vh;

View File

@@ -48,7 +48,9 @@
<template #menu-right> <template #menu-right>
<el-radio-group v-model="query.expireStatus" class="ship-stat" @change="handleExpireChange"> <el-radio-group v-model="query.expireStatus" class="ship-stat" @change="handleExpireChange">
<el-radio-button label="">全部{{ expiryStat.total }}</el-radio-button> <el-radio-button label="">全部{{ expiryStat.total }}</el-radio-button>
<el-radio-button label="within30">30天内到期{{ expiryStat.within30 }}</el-radio-button> <el-radio-button label="within30"
>30天内到期{{ expiryStat.within30 }}</el-radio-button
>
<el-radio-button label="expired">已到期{{ expiryStat.expired }}</el-radio-button> <el-radio-button label="expired">已到期{{ expiryStat.expired }}</el-radio-button>
</el-radio-group> </el-radio-group>
</template> </template>
@@ -87,11 +89,7 @@
</span> </span>
</template> </template>
<template #menu="{ row }"> <template #menu="{ row }">
<el-link <el-link type="primary" v-if="hasPermission('transport_ship_edit')" @click="openShip(row)">
type="primary"
v-if="hasPermission('transport_ship_edit')"
@click="openShip(row)"
>
修改 修改
</el-link> </el-link>
<el-link <el-link
@@ -128,11 +126,16 @@
:disabled="readonly" :disabled="readonly"
class="ship-form" class="ship-form"
> >
<div class="section-title">基础船舶信息</div> <section-card title="基础船舶信息">
<el-row :gutter="18"> <el-row :gutter="18">
<el-col :span="8"> <el-col :span="8">
<el-form-item label="船舶所属组织" prop="organizationName"> <el-form-item label="船舶所属组织" prop="organizationName">
<el-select v-model="shipForm.organizationName" filterable clearable placeholder="请选择"> <el-select
v-model="shipForm.organizationName"
filterable
clearable
placeholder="请选择"
>
<el-option <el-option
v-for="item in organizationOptions" v-for="item in organizationOptions"
:key="item.value" :key="item.value"
@@ -267,8 +270,9 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
</section-card>
<div class="section-title section-title--wide">船舶证书信息</div> <section-card title="船舶证书信息">
<div class="cert-block"> <div class="cert-block">
<div class="cert-block__title">船舶所有权证书</div> <div class="cert-block__title">船舶所有权证书</div>
<el-row :gutter="18"> <el-row :gutter="18">
@@ -304,7 +308,9 @@
v-model="shipForm.shipIdentifierNo" v-model="shipForm.shipIdentifierNo"
maxlength="50" maxlength="50"
@input=" @input="
shipForm.shipIdentifierNo = String(shipForm.shipIdentifierNo || '').toUpperCase() shipForm.shipIdentifierNo = String(
shipForm.shipIdentifierNo || ''
).toUpperCase()
" "
/> />
</el-form-item> </el-form-item>
@@ -541,6 +547,7 @@
/> />
</el-col> </el-col>
</el-row> </el-row>
</section-card>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="shipBox = false">取消</el-button> <el-button @click="shipBox = false">取消</el-button>
@@ -658,8 +665,12 @@ export default {
netTonnage: [{ required: true, message: '请输入净吨', trigger: 'change' }], netTonnage: [{ required: true, message: '请输入净吨', trigger: 'change' }],
shipInspectionNo: [{ required: true, message: '请输入船检登记号', trigger: 'blur' }], shipInspectionNo: [{ required: true, message: '请输入船检登记号', trigger: 'blur' }],
shipType: [{ required: true, message: '请输入船舶类型', trigger: 'blur' }], shipType: [{ required: true, message: '请输入船舶类型', trigger: 'blur' }],
nationalityCertStartDate: [{ validator: this.validateNationalityCertDate, trigger: 'change' }], nationalityCertStartDate: [
safeManningCertStartDate: [{ validator: this.validateSafeManningCertDate, trigger: 'change' }], { validator: this.validateNationalityCertDate, trigger: 'change' },
],
safeManningCertStartDate: [
{ validator: this.validateSafeManningCertDate, trigger: 'change' },
],
leaseStartDate: [{ validator: this.validateLeaseDate, trigger: 'change' }], leaseStartDate: [{ validator: this.validateLeaseDate, trigger: 'change' }],
businessTransportCertNo: [{ required: true, message: '请输入证书编号', trigger: 'blur' }], businessTransportCertNo: [{ required: true, message: '请输入证书编号', trigger: 'blur' }],
businessTransportCertIssueDate: [ businessTransportCertIssueDate: [
@@ -779,7 +790,11 @@ export default {
); );
}, },
validateDateRangeByProp(startProp, endProp, longTermProp, emptyMessage, callback) { validateDateRangeByProp(startProp, endProp, longTermProp, emptyMessage, callback) {
if (this.shipForm[longTermProp] === 1 && !this.shipForm[startProp] && !this.shipForm[endProp]) { if (
this.shipForm[longTermProp] === 1 &&
!this.shipForm[startProp] &&
!this.shipForm[endProp]
) {
callback(); callback();
return; return;
} }
@@ -968,7 +983,7 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.transport-ship-page { .transport-ship-page {
.ship-stat { .ship-stat {
flex-shrink: 0; flex-shrink: 0;
} }
@@ -977,26 +992,9 @@ export default {
:deep(.el-table td .cell) { :deep(.el-table td .cell) {
white-space: nowrap; white-space: nowrap;
} }
} }
.ship-form {
.section-title {
margin: 18px 0 14px;
padding-left: 10px;
border-left: 3px solid #e74b5f;
color: #303133;
font-weight: 600;
line-height: 18px;
}
.section-title:first-child {
margin-top: 0;
}
.section-title--wide {
margin-top: 24px;
}
.ship-form {
:deep(.el-date-editor.el-input), :deep(.el-date-editor.el-input),
:deep(.el-date-editor.el-input__wrapper), :deep(.el-date-editor.el-input__wrapper),
:deep(.el-select), :deep(.el-select),
@@ -1004,64 +1002,64 @@ export default {
:deep(.el-input-number) { :deep(.el-input-number) {
width: 100%; width: 100%;
} }
} }
.date-range-inline { .date-range-inline {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
width: 100%; width: 100%;
} }
.date-range-inline :deep(.el-date-editor) { .date-range-inline :deep(.el-date-editor) {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.navigation-area-field { .navigation-area-field {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
width: 100%; width: 100%;
} }
.navigation-area-field :deep(.el-select) { .navigation-area-field :deep(.el-select) {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.navigation-info-icon { .navigation-info-icon {
color: #909399; color: #909399;
font-size: 18px; font-size: 18px;
cursor: help; cursor: help;
flex-shrink: 0; flex-shrink: 0;
} }
.cert-block { .cert-block {
padding: 16px 0 8px; padding: 16px 0 8px;
border-top: 1px solid #e5e6eb; border-top: 1px solid #e5e6eb;
} }
.cert-block__title { .cert-block__title {
margin-bottom: 12px; margin-bottom: 12px;
color: #303133; color: #303133;
font-weight: 600; font-weight: 600;
line-height: 20px; line-height: 20px;
} }
.cert-upload-grid { .cert-upload-grid {
padding-top: 8px; padding-top: 8px;
} }
.cert-upload-spacer { .cert-upload-spacer {
height: 1px; height: 1px;
} }
.ship-upload-item { .ship-upload-item {
margin-bottom: 14px; margin-bottom: 14px;
} }
.ocr-tip { .ocr-tip {
min-height: 114px; min-height: 114px;
padding: 18px 20px; padding: 18px 20px;
background: #ffd84d; background: #ffd84d;
@@ -1069,11 +1067,11 @@ export default {
font-size: 16px; font-size: 16px;
line-height: 1.45; line-height: 1.45;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
} }
// 大尺寸证件照:按身份证 1.586:1 比例缩放(约真实尺寸 75%240×151左对齐显示
:deep(.ship-uploader) { :deep(.ship-uploader) {
display: block; display: block;
width: 100%;
} }
:deep(.ship-uploader .el-upload), :deep(.ship-uploader .el-upload),
@@ -1090,7 +1088,14 @@ export default {
:deep(.ship-uploader--large .el-upload), :deep(.ship-uploader--large .el-upload),
:deep(.ship-uploader--large.el-upload) { :deep(.ship-uploader--large.el-upload) {
height: 260px; width: 100%;
height: 151px;
}
:deep(.ship-uploader--large) {
display: block;
width: 240px;
margin: 0;
} }
:deep(.ship-uploader__image) { :deep(.ship-uploader__image) {
@@ -1109,34 +1114,34 @@ export default {
color: #606266; color: #606266;
} }
:deep(.ship-navigation-tooltip) { :deep(.ship-navigation-tooltip) {
padding: 8px 10px; padding: 8px 10px;
} }
.navigation-cargo-table { .navigation-cargo-table {
border-collapse: collapse; border-collapse: collapse;
font-size: 12px; font-size: 12px;
} }
.navigation-cargo-table th, .navigation-cargo-table th,
.navigation-cargo-table td { .navigation-cargo-table td {
border: 1px solid #909399; border: 1px solid #909399;
padding: 6px 10px; padding: 6px 10px;
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
} }
.navigation-cargo-table th { .navigation-cargo-table th {
font-weight: 600; font-weight: 600;
} }
.date-warning { .date-warning {
color: #f56c6c; color: #f56c6c;
font-weight: 600; font-weight: 600;
} }
.date-danger { .date-danger {
color: #d40000; color: #d40000;
font-weight: 600; font-weight: 600;
} }
</style> </style>

View File

@@ -49,7 +49,9 @@
@change="handleExpireChange" @change="handleExpireChange"
> >
<el-radio-button label="">全部{{ expiryStat.total }}</el-radio-button> <el-radio-button label="">全部{{ expiryStat.total }}</el-radio-button>
<el-radio-button label="within30">30天内到期{{ expiryStat.within30 }}</el-radio-button> <el-radio-button label="within30"
>30天内到期{{ expiryStat.within30 }}</el-radio-button
>
<el-radio-button label="expired">已到期{{ expiryStat.expired }}</el-radio-button> <el-radio-button label="expired">已到期{{ expiryStat.expired }}</el-radio-button>
</el-radio-group> </el-radio-group>
</template> </template>
@@ -120,7 +122,7 @@
:disabled="readonly" :disabled="readonly"
class="vehicle-form" class="vehicle-form"
> >
<div class="section-title">基础车辆信息</div> <section-card title="基础车辆信息">
<el-row :gutter="18"> <el-row :gutter="18">
<el-col :span="24"> <el-col :span="24">
<el-form-item label="所有组织" prop="organizationName"> <el-form-item label="所有组织" prop="organizationName">
@@ -306,8 +308,9 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
</section-card>
<div class="section-title">车辆资质图片</div> <section-card title="车辆资质图片">
<el-row :gutter="18"> <el-row :gutter="18">
<el-col :span="6"> <el-col :span="6">
<el-form-item label="行驶证档案编号" prop="drivingLicenseNo"> <el-form-item label="行驶证档案编号" prop="drivingLicenseNo">
@@ -484,6 +487,7 @@
/> />
</el-col> </el-col>
</el-row> </el-row>
</section-card>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="vehicleBox = false">取消</el-button> <el-button @click="vehicleBox = false">取消</el-button>
@@ -646,7 +650,9 @@ export default {
businessRelation: [{ required: true, message: '请选择业务关系', trigger: 'change' }], businessRelation: [{ required: true, message: '请选择业务关系', trigger: 'change' }],
energyType: [{ required: true, message: '请选择能源类型', trigger: 'change' }], energyType: [{ required: true, message: '请选择能源类型', trigger: 'change' }],
drivingLicenseNo: [{ required: true, message: '请输入行驶证档案编号', trigger: 'blur' }], drivingLicenseNo: [{ required: true, message: '请输入行驶证档案编号', trigger: 'blur' }],
drivingLicenseEndDate: [{ required: true, message: '请选择行驶证有效期', trigger: 'change' }], drivingLicenseEndDate: [
{ required: true, message: '请选择行驶证有效期', trigger: 'change' },
],
roadTransportCertNo: [{ required: true, message: '请输入道路运输证号', trigger: 'blur' }], roadTransportCertNo: [{ required: true, message: '请输入道路运输证号', trigger: 'blur' }],
roadTransportCertEndDate: [ roadTransportCertEndDate: [
{ required: true, message: '请选择道路运输证有效期', trigger: 'change' }, { required: true, message: '请选择道路运输证有效期', trigger: 'change' },
@@ -707,7 +713,9 @@ export default {
}, },
validatePlateNo(rule, value, callback) { validatePlateNo(rule, value, callback) {
const plateProvince = String(this.vehicleForm.plateProvince || '').trim(); const plateProvince = String(this.vehicleForm.plateProvince || '').trim();
const plateNoBody = String(this.vehicleForm.plateNoBody || '').trim().toUpperCase(); const plateNoBody = String(this.vehicleForm.plateNoBody || '')
.trim()
.toUpperCase();
const plateColor = String(this.vehicleForm.plateColor || '').trim(); const plateColor = String(this.vehicleForm.plateColor || '').trim();
if (!plateProvince || !plateNoBody || !plateColor) { if (!plateProvince || !plateNoBody || !plateColor) {
callback(new Error('请完整填写车牌号')); callback(new Error('请完整填写车牌号'));
@@ -871,10 +879,13 @@ export default {
}, },
syncPlateNo(validate = true) { syncPlateNo(validate = true) {
const plateProvince = String(this.vehicleForm.plateProvince || '').trim(); const plateProvince = String(this.vehicleForm.plateProvince || '').trim();
const plateNoBody = String(this.vehicleForm.plateNoBody || '').trim().toUpperCase(); const plateNoBody = String(this.vehicleForm.plateNoBody || '')
.trim()
.toUpperCase();
const plateColor = String(this.vehicleForm.plateColor || '').trim(); const plateColor = String(this.vehicleForm.plateColor || '').trim();
this.vehicleForm.plateNoBody = plateNoBody; this.vehicleForm.plateNoBody = plateNoBody;
this.vehicleForm.plateNo = plateProvince && plateNoBody ? `${plateProvince}${plateNoBody}` : ''; this.vehicleForm.plateNo =
plateProvince && plateNoBody ? `${plateProvince}${plateNoBody}` : '';
this.vehicleForm.plateColor = plateColor; this.vehicleForm.plateColor = plateColor;
if (validate) { if (validate) {
this.$refs.vehicleForm?.validateField('plateNo'); this.$refs.vehicleForm?.validateField('plateNo');
@@ -1070,19 +1081,6 @@ export default {
} }
.vehicle-form { .vehicle-form {
.section-title {
margin: 18px 0 14px;
padding-left: 10px;
border-left: 3px solid #e74b5f;
color: #303133;
font-weight: 600;
line-height: 18px;
}
.section-title:first-child {
margin-top: 0;
}
:deep(.el-date-editor.el-input), :deep(.el-date-editor.el-input),
:deep(.el-date-editor.el-input__wrapper), :deep(.el-date-editor.el-input__wrapper),
:deep(.el-select), :deep(.el-select),
@@ -1149,9 +1147,9 @@ export default {
} }
} }
// 大尺寸证件照:按身份证 1.586:1 比例缩放(约真实尺寸 75%240×151左对齐显示
:deep(.vehicle-uploader) { :deep(.vehicle-uploader) {
display: block; display: block;
width: 100%;
} }
:deep(.vehicle-uploader .el-upload), :deep(.vehicle-uploader .el-upload),
@@ -1166,9 +1164,17 @@ export default {
overflow: hidden; overflow: hidden;
} }
// 大尺寸模式:缩成 1.586:1 比例,左对齐(不再 100% 占栏)
:deep(.vehicle-uploader--large) {
display: block;
width: 240px;
margin: 0;
}
:deep(.vehicle-uploader--large .el-upload), :deep(.vehicle-uploader--large .el-upload),
:deep(.vehicle-uploader--large.el-upload) { :deep(.vehicle-uploader--large.el-upload) {
height: 260px; width: 100%;
height: 151px;
} }
:deep(.vehicle-uploader__image) { :deep(.vehicle-uploader__image) {

View File

@@ -503,7 +503,7 @@ export default {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
min-height: 88px; //min-height: 88px;
padding: 16px 28px 16px 22px; padding: 16px 28px 16px 22px;
border: 1px dashed #8b8b8b; border: 1px dashed #8b8b8b;
border-radius: 8px; border-radius: 8px;
@@ -600,7 +600,6 @@ export default {
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
color: #303133; color: #303133;
font-size: 16px;
line-height: 1.25; line-height: 1.25;
} }
@@ -622,14 +621,6 @@ export default {
display: block; display: block;
} }
:global(.insurance-record-page__upload-button) {
min-width: 112px;
height: 40px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
}
@media (max-width: 768px) { @media (max-width: 768px) {
:global(.insurance-record-page__upload) { :global(.insurance-record-page__upload) {
align-items: stretch; align-items: stretch;