Merge remote-tracking branch 'websoft/master'

# Conflicts:
#	src/option/business/process-config.js
#	src/views/business/process-config.vue
#	src/views/vehicle/customer-archive.vue
This commit is contained in:
2026-08-06 17:41:03 +08:00
9 changed files with 1392 additions and 1278 deletions

View File

@@ -1,17 +1,18 @@
# 2026-08-06
# 2026-08-06 工作日志
## 复盘 src/views/business/project-apply.vue 弹窗 UI 规范
- 主弹窗为**自定义 el-dialog + el-form**(非 Avue option 驱动width 1440px、top 4vh、append-to-body、destroy-on-close。
- 分区标题复用全局 `.dialog-section-title`4px 主色竖条 + 8px 间距);页面将其改造为「白卡头 + 下方白内容卡」拼合方案radius 6px + 淡阴影 0 1px 2px rgba(0,0,0,.04))。
- 表单label-position="right" / label-width="auto";两列 grid`column-gap: 72px`);表单项 margin-bottom 16pxlabel min-width 160px。
- 备注类 textarea 整行展示 + 200 字上限 + show-word-limit金额/天数有非负与范围校验。
- 底部按钮:非变更弹窗=取消/提交(右对齐);变更弹窗=提交/保存/关闭(左对齐)。
- **与设计规范(AGENTS.md)对照**:基本符合 4.8 弹窗表单规范;但本页 scoped 用 `white-space: normal` 覆盖了全局 `.el-dialog .el-form-item__label { white-space: nowrap }`允许长标签换行与全局「label 不换行」取向不同);两列 grid 统一 min-width 160px未严格「按列分别计算 label 宽度」。
## 运输计划调度弹窗 UX 分析
- 完成 `/business/transport-plan` 调度弹窗的 UX 审查,对比 project-apply 页面风格
- 输出 6 项改进方案P0-P5核心问题为缺少灰底(#f5f6fa)+白卡(section-card)分组设计模式
- 所有改动集中在 `business-crud-page.vue` 一个文件
- 待用户确认后实施第一阶段P0灰底 + P1白卡分组
## 将 project-apply 弹窗标准推广为「全项目通用标准」
用户要求把 project-apply 弹窗视觉(灰底 + 分组白卡 + 标题在卡内)复制到其他页面。落地方式(已执行):
- 新增可复用组件 `src/components/section-card/main.vue`:白卡 + 标题在卡头4px 主色竖条 + 8px 间距),支持 `title` prop 与 `#title`/`#extra` slot已在 `src/main.js` 全局注册为 `<section-card>`
- `src/styles/element-ui.scss` 末尾新增全局标准:① `.el-dialog__body` 统一灰底 `#f5f6fa`(影响全项目所有弹窗);② `.el-dialog .avue-form` 自动包白卡Avue 默认增删改弹窗无需改 option 即统一);③ `.el-dialog .avue-form__group` 分组白卡option.group 渲染为标题在卡内);④ 弹窗内 `.el-form-item` 默认 16px 间距;⑤ `.section-card` 全局样式;⑥ `.form-grid-2` 两列栅格工具类column-gap 72px
- 应用落地business 模块自定义弹窗——`common-route.vue` 的「选择常用地址」弹窗用 section-card 包成「查询条件/常用地址」两组;`loading-manage.vue` 主弹窗 6 个分区全部改用 `<section-card>``common-cargo.vue` 导入弹窗经全局 `.el-dialog .avue-form` 规则自动获得白卡。`project-apply.vue` 本身作为参照未改(其 scoped 样式已符合,全局灰底与其一致)。
- 验证:`vite build` 失败为仓库既有问题(`waybill-import-dialog.vue` 引入的 `xlsx` 依赖缺失),与本次改动无关;改用 `@vue/compiler-sfc` 编译三个改动 .vue均 OK+ sass 编译 `element-ui.scss`OK
- 注意:其他模块的**自定义弹窗**需按同样方式用 `<section-card>` 包裹各组(灰底已由全局规则自动生效);如需继续推进其余模块可照此模式批量处理。
## 已实施的改进(全部在 business-crud-page.vue
- P0: `__dispatch-dialog``.el-dialog__body` 改为 `padding:16px; background:#f5f6fa; overflow-y:auto`(原 padding:0
- P1: 调度弹窗三块分区用全局 `<section-card>` 包裹:基本信息 / 收发货路线 / 待调度列表;"新增"按钮+汇总文字移入列表卡 `#extra` slot
- P2: `__dispatch-route-line` 由黑色实线改为 `#409EFF` 虚线 + 箭头(主色方向感)
- P3: 待调度列表纳入白卡容器(随 P1 完成)
- P4: 调度表操作列 `width` 180→220 预留扩展
- P5: `__dispatch-footer` 增加 `border-top` + padding/margin-top 分隔
-`__dispatch-list` / `__dispatch-top`(border-bottom/32px padding) 样式已清理section-card 已全局注册,无需 import
- 验证SFC 模板单独编译通过6 处 section-card 引用配对平衡);完整 vite build 仍因预存 `xlsx` 幽灵依赖pnpm 未装)失败,与本次改动无关
- 注意:本项目完整 build 依赖 `xlsx@0.18.5`package.json 有声明但 node_modules 缺失),属环境预存问题,非本次引入

View File

@@ -1,16 +1,66 @@
# 项目长期记忆tms-erp-web-ws / Saber3 客商模块)
## 设计约定(经用户确认的决策)
- 弹窗内多字段表单分组:**「灰色分组块 + 白色内层卡片」双色方案**(用户最终确认)。
- 外层 `.archive-form__group`:灰底 `#f0f2f5` + 8px 圆角 + `16px 20px` 内边距,分组标题(`.dialog-section-title`,自带 4px 主色竖条)浮于灰底上。
- 内层字段区:直接把分组内唯一的 `> .el-row` 设为白底(`#fff` + 6px 圆角 + 极淡阴影 `0 1px 2px rgba(0,0,0,.04)` + `18px 16px 4px` 内边距),形成「白卡片浮于灰底」的层次,而非逐个输入框强制白底。
- 理由:契合 AGENTS.md「去卡片化」哲学不嵌套 `el-card`),同时用户明确要灰底+白内容对比,全局 `.dialog-section-title` 直接复用即可。
- 适用:新增/编辑弹窗中字段超过 ~10 个、需按业务语义分组时
### 弹窗内多字段表单分组:使用全局 `<section-card>` 组件(项目统一标准)
**整体结构**
- 整个弹窗 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 列网格留白。
### 上传证件区域尺寸规范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/styles/element-ui.scss``.dialog-section-title`
- 通用分组白卡组件:`src/components/section-card/main.vue`(已全局注册为 `<section-card>`,支持 `title` / `#title` / `#extra` slot)。
- 弹窗灰底 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。

View File

@@ -1,7 +1,9 @@
<template>
<el-form-item v-if="mode === 'form'" :label="label" :prop="prop">
<div :class="[`${classPrefix}-uploader`, { [`${classPrefix}-uploader--large`]: large }]">
<el-upload
:class="[`${classPrefix}-uploader`, { [`${classPrefix}-uploader--large`]: large }]"
ref="uploadRef"
class="upload-inner"
action="/api/blade-resource/oss/endpoint/put-file"
name="file"
:headers="uploadHeaders"
@@ -12,17 +14,44 @@
:on-success="handleSuccess"
: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>
</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 v-else :prop="prop" :class="`${classPrefix}-upload-item`">
<div :class="[`${classPrefix}-upload-card`, { [`${classPrefix}-upload-card--large`]: large }]">
<div v-if="label" :class="`${classPrefix}-upload-card__head`">
<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>
<el-upload
ref="uploadRef"
:class="`${classPrefix}-upload-card__uploader`"
action="/api/blade-resource/oss/endpoint/put-file"
name="file"
@@ -34,7 +63,16 @@
:on-success="handleSuccess"
: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>
</el-upload>
</div>
@@ -86,6 +124,11 @@ export default {
this.closeUploadLoading();
},
methods: {
triggerUpload() {
const uploadEl = this.$refs.uploadRef && this.$refs.uploadRef.$el;
const input = uploadEl && uploadEl.querySelector('.el-upload__input');
if (input) input.click();
},
showUploadLoading() {
if (this.uploadLoading) {
return;

View File

@@ -212,9 +212,10 @@
// 灰底弹窗体 + 分组白卡标题融入卡头4px 主色竖条 + 8px 间距)
// =====================================================================
// 1) 所有弹窗内容区统一灰底
// 1) 所有弹窗内容区统一灰底 + 16px 内边距(与 project-apply 一致,避免内容贴边)
.el-dialog__body {
background-color: #f5f6fa;
padding: 16px;
}
// 2) Avue 默认弹窗表单自动包成白卡,无需逐页改 option 即统一标准

View File

@@ -2115,7 +2115,7 @@
</div>
</div>
<div class="business-crud-page__dispatch-summary">
<div class="business-crud-page__dispatch-summary-left">
<section-card title="基本信息">
<div class="business-crud-page__dispatch-summary-grid">
<div class="business-crud-page__dispatch-summary-item">
<div class="business-crud-page__dispatch-summary-label">客户</div>
@@ -2171,7 +2171,8 @@
</div>
</div>
</div>
</div>
</section-card>
<section-card title="收发货路线">
<div class="business-crud-page__dispatch-route">
<div class="business-crud-page__dispatch-route-item">
<span class="business-crud-page__dispatch-route-badge is-start">起</span>
@@ -2203,12 +2204,12 @@
</div>
</div>
</div>
</section-card>
</div>
</div>
<div class="business-crud-page__dispatch-list">
<div class="business-crud-page__dispatch-list-head">
<div class="business-crud-page__dispatch-list-title">待调度列表</div>
<section-card title="待调度列表">
<template #extra>
<el-button
type="primary"
:disabled="dispatchSummaryRemainingQuantity <= 0"
@@ -2216,10 +2217,10 @@
>
新增
</el-button>
<div class="business-crud-page__dispatch-list-summary">
<span class="business-crud-page__dispatch-list-summary">
{{ dispatchSummaryText }}
</div>
</div>
</span>
</template>
<el-table
:data="dispatchRows"
border
@@ -2312,7 +2313,7 @@
align="center"
show-overflow-tooltip
/>
<el-table-column label="操作" width="180" align="center" fixed="right">
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="{ row, $index }">
<div class="business-crud-page__dispatch-actions">
<el-link type="primary" @click="handleDispatchEditRow(row, $index)">编辑</el-link>
@@ -2321,7 +2322,7 @@
</template>
</el-table-column>
</el-table>
</div>
</section-card>
</div>
<template #footer>
<div class="business-crud-page__dispatch-footer">
@@ -9304,7 +9305,10 @@ export default {
}
:deep(.el-dialog__body) {
padding: 0;
padding: 16px;
background: #f5f6fa;
max-height: calc(100vh - 140px);
overflow-y: auto;
}
}
@@ -9315,8 +9319,7 @@ export default {
}
&__dispatch-top {
padding: 22px 32px 14px;
border-bottom: 1px solid #e9edf5;
padding: 22px 0 0;
}
&__dispatch-heading {
@@ -9452,14 +9455,25 @@ export default {
}
&__dispatch-route-line {
position: relative;
width: 72px;
height: 4px;
background: #3b3b3b;
border-radius: 999px;
}
height: 2px;
background-image: linear-gradient(
to right,
var(--el-color-primary) 0,
var(--el-color-primary) 6px,
transparent 6px,
transparent 10px
);
&__dispatch-list {
padding: 12px 18px 18px;
&::after {
content: '';
position: absolute;
right: -1px;
top: -4px;
border: 5px solid transparent;
border-left-color: var(--el-color-primary);
}
}
&__dispatch-list-head {
@@ -9513,6 +9527,9 @@ export default {
justify-content: flex-end;
gap: 12px;
padding-right: 0;
padding-top: 16px;
margin-top: 16px;
border-top: 1px solid #e9edf5;
}
&__dispatch-item-dialog {

View File

@@ -91,21 +91,13 @@
</span>
</template>
<template #menu="{ row }">
<el-link
type="primary"
v-if="hasPermission('driver_view')"
@click="openDriver(row, true)"
>
<el-link type="primary" v-if="hasPermission('driver_view')" @click="openDriver(row, true)">
查看
</el-link>
<el-link type="primary" v-if="hasPermission('driver_edit')" @click="openDriver(row)">
修改
</el-link>
<el-link
type="primary"
v-if="hasPermission('driver_status')"
@click="handleStatus(row)"
>
<el-link type="primary" v-if="hasPermission('driver_status')" @click="handleStatus(row)">
{{ row.status === 1 ? '停用' : '启用' }}
</el-link>
</template>
@@ -115,7 +107,7 @@
:title="dialogTitle"
append-to-body
v-model="driverBox"
width="92%"
width="90%"
top="4vh"
class="driver-dialog"
@closed="resetDriver"
@@ -128,7 +120,7 @@
:disabled="readonly"
class="driver-form"
>
<div class="section-title">基础身份信息</div>
<section-card title="基础身份信息">
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="司机姓名" prop="driverName">
@@ -147,7 +139,11 @@
</el-col>
<el-col :span="8">
<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-col>
</el-row>
@@ -163,7 +159,12 @@
<el-col :span="8">
<el-form-item label="民族" prop="nation">
<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-form-item>
</el-col>
@@ -245,8 +246,9 @@
/>
</el-col>
</el-row>
</section-card>
<div class="section-title">驾驶证信息</div>
<section-card title="驾驶证信息">
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="准驾车型" prop="drivingType">
@@ -315,8 +317,7 @@
:headers="uploadHeaders"
large
@success="
(url, file) =>
handleDrivingLicenseUploadSuccess('drivingLicenseFront', url, file)
(url, file) => handleDrivingLicenseUploadSuccess('drivingLicenseFront', url, file)
"
/>
</el-col>
@@ -334,8 +335,9 @@
/>
</el-col>
</el-row>
</section-card>
<div class="section-title">从业资格证信息</div>
<section-card title="从业资格证信息">
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="从业资格认证类型" prop="qualificationType">
@@ -412,8 +414,9 @@
/>
</el-col>
</el-row>
</section-card>
<div class="section-title">司机联系信息</div>
<section-card title="司机联系信息">
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="司机类型" prop="driverType">
@@ -471,6 +474,7 @@
</el-form-item>
</el-col>
</el-row>
</section-card>
</el-form>
<template #footer>
<el-button @click="driverBox = false">取消</el-button>
@@ -1329,19 +1333,6 @@ export default {
}
.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__wrapper),
:deep(.el-cascader),
@@ -1357,9 +1348,9 @@ export default {
}
}
// 大尺寸证件照:按身份证 1.586:1 比例缩放(约真实尺寸 75%240×151左对齐显示
:deep(.driver-uploader) {
display: block;
width: 100%;
}
:deep(.driver-uploader .el-upload),
@@ -1374,9 +1365,17 @@ export default {
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) {
height: 260px;
width: 100%;
height: 151px;
}
:deep(.driver-uploader__image) {
@@ -1395,6 +1394,7 @@ export default {
color: #606266;
}
// 弹窗内可滚动(弹窗 body 灰底已由全局 .el-dialog__body 规则生效)
.driver-dialog {
:deep(.el-dialog__body) {
max-height: 72vh;

View File

@@ -47,7 +47,9 @@
<template #menu-right>
<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="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-group>
</template>
@@ -86,11 +88,7 @@
</span>
</template>
<template #menu="{ row }">
<el-link
type="primary"
v-if="hasPermission('transport_ship_edit')"
@click="openShip(row)"
>
<el-link type="primary" v-if="hasPermission('transport_ship_edit')" @click="openShip(row)">
修改
</el-link>
<el-link
@@ -127,11 +125,16 @@
:disabled="readonly"
class="ship-form"
>
<div class="section-title">基础船舶信息</div>
<section-card title="基础船舶信息">
<el-row :gutter="18">
<el-col :span="8">
<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
v-for="item in organizationOptions"
:key="item.value"
@@ -266,8 +269,9 @@
</el-form-item>
</el-col>
</el-row>
</section-card>
<div class="section-title section-title--wide">船舶证书信息</div>
<section-card title="船舶证书信息">
<div class="cert-block">
<div class="cert-block__title">船舶所有权证书</div>
<el-row :gutter="18">
@@ -303,7 +307,9 @@
v-model="shipForm.shipIdentifierNo"
maxlength="50"
@input="
shipForm.shipIdentifierNo = String(shipForm.shipIdentifierNo || '').toUpperCase()
shipForm.shipIdentifierNo = String(
shipForm.shipIdentifierNo || ''
).toUpperCase()
"
/>
</el-form-item>
@@ -540,6 +546,7 @@
/>
</el-col>
</el-row>
</section-card>
</el-form>
<template #footer>
<el-button @click="shipBox = false">取消</el-button>
@@ -657,8 +664,12 @@ export default {
netTonnage: [{ required: true, message: '请输入净吨', trigger: 'change' }],
shipInspectionNo: [{ required: true, message: '请输入船检登记号', trigger: 'blur' }],
shipType: [{ required: true, message: '请输入船舶类型', trigger: 'blur' }],
nationalityCertStartDate: [{ validator: this.validateNationalityCertDate, trigger: 'change' }],
safeManningCertStartDate: [{ validator: this.validateSafeManningCertDate, trigger: 'change' }],
nationalityCertStartDate: [
{ validator: this.validateNationalityCertDate, trigger: 'change' },
],
safeManningCertStartDate: [
{ validator: this.validateSafeManningCertDate, trigger: 'change' },
],
leaseStartDate: [{ validator: this.validateLeaseDate, trigger: 'change' }],
businessTransportCertNo: [{ required: true, message: '请输入证书编号', trigger: 'blur' }],
businessTransportCertIssueDate: [
@@ -778,7 +789,11 @@ export default {
);
},
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();
return;
}
@@ -967,7 +982,7 @@ export default {
</script>
<style lang="scss" scoped>
.transport-ship-page {
.transport-ship-page {
.ship-stat {
flex-shrink: 0;
}
@@ -976,26 +991,9 @@ export default {
:deep(.el-table td .cell) {
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__wrapper),
:deep(.el-select),
@@ -1003,64 +1001,64 @@ export default {
:deep(.el-input-number) {
width: 100%;
}
}
}
.date-range-inline {
.date-range-inline {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
}
.date-range-inline :deep(.el-date-editor) {
.date-range-inline :deep(.el-date-editor) {
flex: 1;
min-width: 0;
}
}
.navigation-area-field {
.navigation-area-field {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
}
.navigation-area-field :deep(.el-select) {
.navigation-area-field :deep(.el-select) {
flex: 1;
min-width: 0;
}
}
.navigation-info-icon {
.navigation-info-icon {
color: #909399;
font-size: 18px;
cursor: help;
flex-shrink: 0;
}
}
.cert-block {
.cert-block {
padding: 16px 0 8px;
border-top: 1px solid #e5e6eb;
}
}
.cert-block__title {
.cert-block__title {
margin-bottom: 12px;
color: #303133;
font-weight: 600;
line-height: 20px;
}
}
.cert-upload-grid {
.cert-upload-grid {
padding-top: 8px;
}
}
.cert-upload-spacer {
.cert-upload-spacer {
height: 1px;
}
}
.ship-upload-item {
.ship-upload-item {
margin-bottom: 14px;
}
}
.ocr-tip {
.ocr-tip {
min-height: 114px;
padding: 18px 20px;
background: #ffd84d;
@@ -1068,11 +1066,11 @@ export default {
font-size: 16px;
line-height: 1.45;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
}
// 大尺寸证件照:按身份证 1.586:1 比例缩放(约真实尺寸 75%240×151左对齐显示
:deep(.ship-uploader) {
display: block;
width: 100%;
}
:deep(.ship-uploader .el-upload),
@@ -1089,7 +1087,14 @@ export default {
: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) {
@@ -1108,34 +1113,34 @@ export default {
color: #606266;
}
:deep(.ship-navigation-tooltip) {
:deep(.ship-navigation-tooltip) {
padding: 8px 10px;
}
}
.navigation-cargo-table {
.navigation-cargo-table {
border-collapse: collapse;
font-size: 12px;
}
}
.navigation-cargo-table th,
.navigation-cargo-table td {
.navigation-cargo-table th,
.navigation-cargo-table td {
border: 1px solid #909399;
padding: 6px 10px;
text-align: center;
white-space: nowrap;
}
}
.navigation-cargo-table th {
.navigation-cargo-table th {
font-weight: 600;
}
}
.date-warning {
.date-warning {
color: #f56c6c;
font-weight: 600;
}
}
.date-danger {
.date-danger {
color: #d40000;
font-weight: 600;
}
}
</style>

View File

@@ -48,7 +48,9 @@
@change="handleExpireChange"
>
<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-group>
</template>
@@ -119,7 +121,7 @@
:disabled="readonly"
class="vehicle-form"
>
<div class="section-title">基础车辆信息</div>
<section-card title="基础车辆信息">
<el-row :gutter="18">
<el-col :span="24">
<el-form-item label="所有组织" prop="organizationName">
@@ -305,8 +307,9 @@
</el-form-item>
</el-col>
</el-row>
</section-card>
<div class="section-title">车辆资质图片</div>
<section-card title="车辆资质图片">
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="行驶证档案编号" prop="drivingLicenseNo">
@@ -483,6 +486,7 @@
/>
</el-col>
</el-row>
</section-card>
</el-form>
<template #footer>
<el-button @click="vehicleBox = false">取消</el-button>
@@ -645,7 +649,9 @@ export default {
businessRelation: [{ required: true, message: '请选择业务关系', trigger: 'change' }],
energyType: [{ required: true, message: '请选择能源类型', trigger: 'change' }],
drivingLicenseNo: [{ required: true, message: '请输入行驶证档案编号', trigger: 'blur' }],
drivingLicenseEndDate: [{ required: true, message: '请选择行驶证有效期', trigger: 'change' }],
drivingLicenseEndDate: [
{ required: true, message: '请选择行驶证有效期', trigger: 'change' },
],
roadTransportCertNo: [{ required: true, message: '请输入道路运输证号', trigger: 'blur' }],
roadTransportCertEndDate: [
{ required: true, message: '请选择道路运输证有效期', trigger: 'change' },
@@ -706,7 +712,9 @@ export default {
},
validatePlateNo(rule, value, callback) {
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();
if (!plateProvince || !plateNoBody || !plateColor) {
callback(new Error('请完整填写车牌号'));
@@ -870,10 +878,13 @@ export default {
},
syncPlateNo(validate = true) {
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();
this.vehicleForm.plateNoBody = plateNoBody;
this.vehicleForm.plateNo = plateProvince && plateNoBody ? `${plateProvince}${plateNoBody}` : '';
this.vehicleForm.plateNo =
plateProvince && plateNoBody ? `${plateProvince}${plateNoBody}` : '';
this.vehicleForm.plateColor = plateColor;
if (validate) {
this.$refs.vehicleForm?.validateField('plateNo');
@@ -1069,19 +1080,6 @@ export default {
}
.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__wrapper),
:deep(.el-select),
@@ -1148,9 +1146,9 @@ export default {
}
}
// 大尺寸证件照:按身份证 1.586:1 比例缩放(约真实尺寸 75%240×151左对齐显示
:deep(.vehicle-uploader) {
display: block;
width: 100%;
}
:deep(.vehicle-uploader .el-upload),
@@ -1165,9 +1163,17 @@ export default {
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) {
height: 260px;
width: 100%;
height: 151px;
}
:deep(.vehicle-uploader__image) {

View File

@@ -503,7 +503,7 @@ export default {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 88px;
//min-height: 88px;
padding: 16px 28px 16px 22px;
border: 1px dashed #8b8b8b;
border-radius: 8px;
@@ -600,7 +600,6 @@ export default {
flex-direction: column;
gap: 4px;
color: #303133;
font-size: 16px;
line-height: 1.25;
}
@@ -622,14 +621,6 @@ export default {
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) {
:global(.insurance-record-page__upload) {
align-items: stretch;