Compare commits

..

19 Commits

Author SHA1 Message Date
52d4b6b486 Merge remote-tracking branch 'websoft/master'
# Conflicts:
#	src/option/transportCapacity/transport-ship.js
#	src/option/transportCapacity/transport-vehicle.js
2026-08-07 08:28:42 +08:00
6a02e9126b 1、车船模块fix bug
2、运力模块fix bug
3、新增设备台账
4、其他bug 修复
5、调整组织、人员模块
2026-08-07 08:28:21 +08:00
bb580b2bfc style(business): 优化派遣摘要及路线项布局样式
- 调整派遣摘要网格列宽,改善整体布局比例
- 增加派遣路线容器间距,提升视觉分隔效果
- 修改派遣路线项最大宽度和内边距,提升内容展示空间
- 添加派遣路线项最小高度及虚线边框,增强区块感和可视性
- 调整派遣路线项边角圆角,优化界面圆润度
2026-08-07 01:41:35 +08:00
6c91889e33 style(business): 优化调度路线组件样式及结构调整
- 用车辆图标替换调度路线横线,提升视觉表现
- 调整调度路线布局,使用 grid 增强对齐和间距控制
- 修改调度路线节点样式,统一圆形背景和阴影效果
- 优化标题、地址、联系人文本样式,增加省略号和颜色区分
- 调整调度路线图标容器样式,增加边框和柔和阴影
- 使用伪元素实现两侧虚线提醒,增强组件连接感
2026-08-07 01:29:32 +08:00
4bef3430e0 refactor(business): 优化派车信息展示和样式布局
- 将基本信息部分整体包裹在section-card内并调整布局顺序
- 调整收发货路线结构,合并地址和联系人信息至路线节点内
- 使用CSS grid替代部分flex布局,优化网格间距和对齐
- 更新文字换行和溢出处理,增强可读性
- 删除冗余的收发货路线详细信息区域,精简代码结构
- 统一section-card的样式和内部内容伸缩布局
- 调整附件显示方式,保证链接样式一致性
2026-08-07 01:21:46 +08:00
17d12d279d refactor(business): 优化派车信息展示和样式布局
- 将基本信息部分整体包裹在section-card内并调整布局顺序
- 调整收发货路线结构,合并地址和联系人信息至路线节点内
- 使用CSS grid替代部分flex布局,优化网格间距和对齐
- 更新文字换行和溢出处理,增强可读性
- 删除冗余的收发货路线详细信息区域,精简代码结构
- 统一section-card的样式和内部内容伸缩布局
- 调整附件显示方式,保证链接样式一致性
2026-08-07 01:03:29 +08:00
9cf005cece refactor(business): 优化派车信息展示和样式布局
- 将基本信息部分整体包裹在section-card内并调整布局顺序
- 调整收发货路线结构,合并地址和联系人信息至路线节点内
- 使用CSS grid替代部分flex布局,优化网格间距和对齐
- 更新文字换行和溢出处理,增强可读性
- 删除冗余的收发货路线详细信息区域,精简代码结构
- 统一section-card的样式和内部内容伸缩布局
- 调整附件显示方式,保证链接样式一致性
2026-08-07 00:54:19 +08:00
d8db9b1edd feat(table): 调整表格操作列宽度并统一文字链接间距
- /business/project-apply 主表操作列宽度固定为 220,移除 applyTableMenuWidth 调用
- /business/temporary-credit-limit 通过传入 menuWidth prop 固定操作列宽度为 180
- /base/common-address 操作列宽度由 220 降至 180
- /business/process-config 操作列宽度由 320 调整为 180,修复末尾语法错误
- /vehicle/insurance-record 操作列宽度先降至 140 后改回 160,后全部车/船务管理模块记录页统一为 180
- 多个车/船务管理模块相关 option 文件及 vue 文件操作列宽统调为 180
- 新增 business-crud-page 组件 menuWidth prop,支持外部传值控制操作列宽度
- 全站操作列文字链接去除 el-link 默认 margin,避免和 gap 叠加导致文字间距过宽
- 优化操作列按钮过多时自动换行展示逻辑,确保按钮不被裁切
2026-08-07 00:44:26 +08:00
cf04d48010 feat(table): 调整表格操作列宽度并统一文字链接间距
- /business/project-apply 主表操作列宽度固定为 220,移除 applyTableMenuWidth 调用
- /business/temporary-credit-limit 通过传入 menuWidth prop 固定操作列宽度为 180
- /base/common-address 操作列宽度由 220 降至 180
- /business/process-config 操作列宽度由 320 调整为 180,修复末尾语法错误
- /vehicle/insurance-record 操作列宽度先降至 140 后改回 160,后全部车/船务管理模块记录页统一为 180
- 多个车/船务管理模块相关 option 文件及 vue 文件操作列宽统调为 180
- 新增 business-crud-page 组件 menuWidth prop,支持外部传值控制操作列宽度
- 全站操作列文字链接去除 el-link 默认 margin,避免和 gap 叠加导致文字间距过宽
- 优化操作列按钮过多时自动换行展示逻辑,确保按钮不被裁切
2026-08-06 23:55:30 +08:00
454608d7be feat(dispatch): 优化运输计划调度弹窗视觉布局并提升紧凑度
- 弹窗标题和单号合并一行,移除分隔符,提升信息整体感
- 基本信息和收发货路线由左右双列改为上下堆叠,改善视觉层次
- 基本信息网格调整为4列布局,货物信息扩大占2列,附件独占一行
- 计划执行时间范围由‘-’改为‘ ~ ’,更清晰表达时间区间
- 路线节点展示改为横向徽章加下方双列详细信息,避免空白和居中留白
- 调整字体大小、颜色及间距,整体风格更简洁且信息更紧凑
- 移除无用边距和min-height约束,使弹窗整体更加紧凑精简
- 更新代理配置,开发期/api请求代理指向指定内网地址,统一路径无需重写,
  以解决跨域问题并保持与后端网关路径一致
- 进行了完整构建验证,确保修改不会影响现有功能稳定性
2026-08-06 23:10:06 +08:00
d82d06dc78 feat(dispatch): 优化运输计划调度弹窗视觉布局并提升紧凑度
- 弹窗标题和单号合并一行,移除分隔符,提升信息整体感
- 基本信息和收发货路线由左右双列改为上下堆叠,改善视觉层次
- 基本信息网格调整为4列布局,货物信息扩大占2列,附件独占一行
- 计划执行时间范围由‘-’改为‘ ~ ’,更清晰表达时间区间
- 路线节点展示改为横向徽章加下方双列详细信息,避免空白和居中留白
- 调整字体大小、颜色及间距,整体风格更简洁且信息更紧凑
- 移除无用边距和min-height约束,使弹窗整体更加紧凑精简
- 更新代理配置,开发期/api请求代理指向指定内网地址,统一路径无需重写,
  以解决跨域问题并保持与后端网关路径一致
- 进行了完整构建验证,确保修改不会影响现有功能稳定性
2026-08-06 23:06:45 +08:00
f03f764472 feat(dispatch): 优化运输计划调度弹窗视觉布局并提升紧凑度
- 弹窗标题和单号合并一行,移除分隔符,提升信息整体感
- 基本信息和收发货路线由左右双列改为上下堆叠,改善视觉层次
- 基本信息网格调整为4列布局,货物信息扩大占2列,附件独占一行
- 计划执行时间范围由‘-’改为‘ ~ ’,更清晰表达时间区间
- 路线节点展示改为横向徽章加下方双列详细信息,避免空白和居中留白
- 调整字体大小、颜色及间距,整体风格更简洁且信息更紧凑
- 移除无用边距和min-height约束,使弹窗整体更加紧凑精简
- 更新代理配置,开发期/api请求代理指向指定内网地址,统一路径无需重写,
  以解决跨域问题并保持与后端网关路径一致
- 进行了完整构建验证,确保修改不会影响现有功能稳定性
2026-08-06 19:53:34 +08:00
1e4de76978 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
2026-08-06 17:41:03 +08:00
e6d4edf362 项目、合同fix 2026-08-06 17:40:03 +08:00
1862115e1d feat(business-crud-page): 优化运输计划调度弹窗UI与交互体验
- 使用全局<section-card>组件重构调度弹窗三块分区,提升视觉一致性
- 将弹窗灰底设置为#f5f6fa,调整padding及溢出滚动,增强弹窗内容可读性
- 收发货路线样式由黑实线改为主色号虚线并添加箭头,增强方向感
- 待调度列表纳入白卡容器,新增按钮及汇总文字移入列表卡extra slot
- 操作列宽度由180调整至220,预留扩展空间
- 增加分隔线和间距分割调度表底部区域,优化布局层次
2026-08-06 17:39:04 +08:00
49236cad56 style(ui): 调整对话框宽度以优化界面布局
- 将driver.vue中driver-dialog宽度从92%调整为90%
- 将process-config.vue中process-config-dialog宽度从1440px调整为70%
- 保持对话框顶部间距和关闭事件不变
2026-08-06 16:59:34 +08:00
5e92124710 Merge remote-tracking branch 'websoft/master' 2026-08-06 11:52:14 +08:00
badfed7ff1 Merge remote-tracking branch 'websoft/master' 2026-08-06 02:57:58 +08:00
c339852e04 1、基础数据修复bug
2、客商模块修复bug
2026-08-06 02:57:55 +08:00
72 changed files with 5825 additions and 2245 deletions

View File

@@ -1,107 +1,116 @@
# 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 宽度」。
## 全站表格操作列文字链接统一为蓝色
- 需求:客户档案页(/vehicle/customer-archive操作列颜色统一蓝色且全站统一风格
- 实现:在 `src/styles/element-ui.scss` 新增全局规则,覆盖 Avue `#menu` 槽位(`.avue-crud__menu .el-link`)与手写表格操作列(`.el-table .el-table__body .cell:has(.el-link) .el-link`)中所有 `el-link``--el-link-text-color` / hover / active / disabled 变量为主色 `#409eff` 及其浅/深色,从而把 warning/success/danger 等类型强制转蓝
- 单点生效,无需逐页改 `type`。dev server(2888) 已运行会热更新
- 注意:选择器以「表格体内含 el-link 的单元格」为目标,本仓库表格内 el-link 仅用于操作列,无副作用;若未来某数据单元格用 el-link 作强调色需另行排除
## 将 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>` 包裹各组(灰底已由全局规则自动生效);如需继续推进其余模块可照此模式批量处理。
## 运输计划调度弹窗 UX 分析
- 完成 `/business/transport-plan` 调度弹窗的 UX 审查,对比 project-apply 页面风格
- 输出 6 项改进方案P0-P5核心问题为缺少灰底(#f5f6fa)+白卡(section-card)分组设计模式
- 所有改动集中在 `business-crud-page.vue` 一个文件
- 待用户确认后实施第一阶段P0灰底 + P1白卡分组
## 继续推广vehicle/customer-archive.vue 弹窗
- 用户反馈 `/vehicle/customer-archive` 新增客商档案弹窗需与 `/business/project-apply` 一致。
- 已将该页主弹窗内的表单分组由旧版「灰底分组块 + 内层白卡 + 标题在卡内」全部替换为 `<section-card>`
- 工商信息 / 联系信息 / 财务/信用信息 / 其他信息 四个分组改为 section-card删除外层浮于灰底的「基本信息」大标题分组已足够清晰避免嵌套
- 客商材料、变更记录两个非表单分区也改用 `<section-card>`,其中客商材料的操作按钮通过 `#extra` 插槽放在卡头右侧。
- 清理了失效的 `.archive-form__group``.customer-material-section``.change-record-section``.section-title` 等 scoped 样式。
- 验证:`@vue/compiler-sfc` 编译 `customer-archive.vue` OKsass 编译 `element-ui.scss` OKsection-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 缺失),属环境预存问题,非本次引入
## 继续推广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>`,不要再手写卡片样式**。
## 运输计划调度弹窗二次视觉紧凑化
- 户反馈:弹窗“空、不紧凑、内容多但不好看”
-动仍集中在 `business-crud-page.vue`
- 模板:
- 标题与单号/状态标签合并到同一行,移除 `|` 分隔符
- 基本信息与收发货路线由左右双列改为上下堆叠的白卡,各自独占一行
- 基本信息网格改为 4 列:客户 / 项目 / 合同编号 / 计划执行时间;货物信息占 2 列;附件独占一行
- 日期展示由 `-` 改为 ` ~ `,更明确是区间
- 路线改为“横向节点 + 下方双列明细”:起点/终点徽章+名称居中,发货地址/收货地址左对齐分两列,避免居中留白
- 样式:
- 顶部标题字号 22px→18px顶部 padding 22px→0
- 信息标签字号 14px→12px、颜色 #666#909399,值行高 1.45→1.4
- 基本信息网格 3 列/48px 列间距/18px 行间距 → 4 列/24px 列间距/12px 行间距
- 路线徽章 46px→32px、字号 16px→13px、圆角 6px→4px
- 路线标题字号 22px→16px取消居中最长名称省略
- 路线区域取消 `min-height:200px` 和居中,改为紧凑堆叠
- 卡片间统一 `margin-bottom: 12px`
- 验证:`npm run build` 成功
## 调整 `/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` 内,仅影响新增/编辑弹窗,不影响列表搜索栏
## /business/project-apply 主表操作列收窄
- 需求project-apply 主表avue-crud #menu)操作列太宽,缩小一点
- 原:`buildTableOption()` 调用 `applyTableMenuWidth(option, 4)` → 强制 `menuWidth: 320`four 档因按钮数≥4
- 改动:`src/views/business/project-apply.vue``buildTableOption()` 改为直接返回对象并显式设 `menuWidth: 220`,移除 `applyTableMenuWidth` 调用及对应 import仅本文件使用
- 说明220 低于 AGENTS.md「>3 按钮≥320px」约定但属用户明确要求操作列链接已全局支持 flex-wrap按钮多时会自动换行不会裁切。
- 影响范围:仅 project-apply 主表;子表(附件/变更/用户权限)操作列 width 维持 110/100/100 不变;全局 `TABLE_MENU_WIDTH` 未改。
## 调整 `/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/temporary-credit-limit 操作列宽度固定为 180
- 需求:该页操作列宽度也给 180
- 背景:该页用共享组件 `business-crud-page.vue`,其 `option` 在 data() 中由 `cloneOption(crudOption, estimateMenuButtonCount(config))` 生成菜单宽度按按钮数估算≥4 按钮→320option 文件本身无 menuWidth
- 改动:
1. `business-crud-page.vue` 新增可选 prop `menuWidth`Number默认 nulldata() 中生成 `option` 时若 `menuWidth != null` 则强制覆盖 `opt.menuWidth`。默认 null 不影响其他页面。
2. `temporary-credit-limit.vue` 调用处加 `:menu-width="180"`
- 复用提示:后续任何使用 `business-crud-page` 的页面想固定操作列宽度,直接传 `:menu-width="N"` 即可,无需再改 option 或组件逻辑。
- 注意180 同样低于 AGENTS「>3 按钮≥320」约定按用户明确要求链接支持换行兜底。
## 将 `/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 标签开合平衡。
## /base/common-address 操作列宽度 220 → 180
- 需求:「这个也调一下宽度」。上一条刚把 temporary-credit-limit 设为 180用户用「也」字故对齐为 180
- 机制:该页是独立 `avue-crud``option` 来自 `@/option/base/common-address.js``createOption()`,其中已显式设 `menuWidth: 220`
- 改动:将 `src/option/base/common-address.js``menuWidth: 220``180`。操作列仅 3 个链接(查看/编辑/删除)
- 说明:低于 AGENTS「>3 按钮≥320」约定按用户明确要求链接支持换行兜底
## 修正 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` 兜底,未暴露)。
## 操作列宽度的三种设置入口(备忘
- 独立 avue-crud + 自管 option直接改 option 文件的 `menuWidth`(如 common-address
- 独立 avue-crud + buildTableOption 调 `applyTableMenuWidth(option, N)`helper 会按按钮数强制下限≥4→320要更窄需绕过 helper 直接设 `menuWidth`(如 project-apply 改 220
- 共用 `business-crud-page` 组件:新增可选 prop `menuWidth`(默认 null 不动既有逻辑),调用处传 `:menu-width="N"`(如 temporary-credit-limit 传 180
## 继续推广transportCapacity 三页面弹窗 + 上传区整改
- 用户确认 `/transportCapacity/driver` 改造效果正确,并要求
- `/transportCapacity/vehicle``/transportCapacity/ship` 两个页面的弹窗也按同样 `<section-card>` 标准整改
- 上传证件区域整改为:① 宽高比 = 中国二代身份证 1.586:185.6 × 53.98 mm② 区域大小取真实身份证的 50%,固定 165 × 104 px③ 适用范围 = **全部**上传区(含机动车行驶证主页正/反/副页正/反、道路运输证、机动车登记本、船舶所有权/安全环保/国籍/最低安全配员/光船租赁/营业运输 共 6 张 + 行驶证 4 张),居中显示不再 100% 占栏
## /business/process-config 操作列宽度 320 → 180
- 需求:「这个也调一下宽度」。延续上几条把操作列设 180 的系列要求
- 机制:该页是独立 `avue-crud``option` 来自 `@/option/business/process-config.js`,原先包在 `applyTableMenuWidth({...}, 5)`five 档→强制 `menuWidth: 320`,因菜单有 6 个链接:查看/编辑/复制/启用/停用/删除)
- 改动:移除 `applyTableMenuWidth` 包裹与对应 importoption 改为普通对象并显式设 `menuWidth: 180`
- 说明180 明显低于 AGENTS「>3 按钮≥320」约定按用户明确要求链接已全局支持 flex-wrap 换行兜底6 个按钮会多行展示、不裁切。
- 已落地:`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 边框/背景/居中等样式。
## 修复 process-config.js 末尾语法错误(红色警)
- 现象:用户反馈 `src/option/business/process-config.js` 最后一行有红色警
- 根因:上一轮把 `applyTableMenuWidth({...}, 5)` 去掉外层函数包裹时,结尾 `}, 5);` 被改成 `});`,多留了一个 `)`(因 `export const option = {` 本身没有左括号),导致括号不匹配
- 改动:末尾 `});``};`。移除 `applyTableMenuWidth` import 时无误
- 验证node 动态 import 不再报 SyntaxError仅剩 node 无法解析 extensionless `./common` 的模块解析提示,与语法无关),说明语法已恢复
- 验证:`@vue/compiler-sfc` 编译三个 .vue 均 OK`<section-card>` 标签数量 driver 4/4、vehicle 2/2、ship 2/2 平衡vite dev 服务对 `/transportCapacity/{driver,vehicle,ship}` 均返回 200prettier 通过。
## /vehicle/insurance-record 操作列宽度 160 → 140紧凑
- 需求:「这个也调一下宽度,紧凑一点」。本页与上几条不同,用户明确要求更紧凑(其他页给的 180 比这页还宽)。
- 机制:独立 `avue-crud``option` 来自 `@/option/vehicle/insurance-record.js`;无自定义 `#menu` 槽,行菜单用 Avue 内置(仅显式 `viewBtn: true`edit/del 默认开启→3 个链接:查看/编辑/删除)。原已显式 `menuWidth: 160`
- 改动:将 `menuWidth: 160``140`
- 说明:低于 AGENTS「>3 按钮≥320」约定按用户明确要求链接已全局支持 flex-wrap 换行兜底。
- 沉淀到 MEMORY.md 的设计约定:「上传证件区域 = `aspect-ratio: 1.586` + 固定 165 × 104 px + 居中」为全 transportCapacity 模块统一的硬规范。后续若其他模块也使用 `<image-upload-field large>` 应套用同款规范。
## 车/船务管理模块下所有记录页操作列宽度统一为 160
- 需求:用户截图列出「车/船务管理」下 11 个子菜单,要求操作栏宽度都设为 160。
- 对应页面/option 文件:
- 保险记录 insurance-recordoption → `src/option/vehicle/insurance-record.js`(由 140 改回 160
- 违章记录 violation-recordoption → `src/option/vehicle/violation-record.js`(原 160无需改动
- 维修记录 maintenance-recordinline option → `src/views/vehicle/maintenance-record.vue`(原 160无需改动
- 保养记录 maintenance-planinline option → `src/views/vehicle/maintenance-plan.vue`(原 160无需改动
- 换胎记录 tire-replacement-recordoption → `src/option/vehicle/tire-replacement-record.js`(原 160无需改动
- 事故记录 accident-recordoption → `src/option/vehicle/accident-record.js`(原 160无需改动
- 年检记录 annual-inspection-recordoption → `src/option/vehicle/annual-inspection-record.js`(原 160无需改动
- 里程记录 mileage-recordoption → `src/option/vehicle/mileage-record.js`(原 160无需改动
- 变更记录 transport-change-recordoption → `src/option/vehicle/transport-change-record.js`(原 160无需改动
- 油电记录 oil-electric-recordoption → `src/option/vehicle/oil-electric-record.js`(原 160无需改动
- ETC记录 etc-recordoption → `src/option/vehicle/etc-record.js`(原 160无需改动
- 实际改动:仅 `insurance-record.js` 从 140 恢复到 160其余 10 个已经是 160。
- 注意:`src/option/vehicle/other-expense-record.js` 不在截图菜单内,保持不动。
## 上调 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')`)。
## 车/船务管理模块操作列宽度 160 → 180用户反馈 160 偏小
- 需求:上一轮统一 160 后,用户觉得还是小,全部改成 180
- 改动11 个文件 `menuWidth: 160``180`9 个 option 文件 + maintenance-record.vue / maintenance-plan.vue 两处 inline option
- 现状:车/船务管理模块全部 11 个记录页操作列均为 180
## 操作列文字链接间距过宽问题
- 现象:用户截图反馈操作列文字链接之间间距太宽,要求统一风格。
- 根因:全局已设 `.avue-crud .avue-crud__menu { gap: 8px; }` 与手写表格操作列 `gap: 8px`,但 Avue 默认 `.el-link``.cell` 上可能存在 `margin-right`,与 gap 叠加后显得过宽。
- 改动:`src/styles/element-ui.scss` 新增规则,对 `.avue-crud .avue-crud__menu .el-link` 和手写表格操作列 `.el-link` 强制 `margin: 0`,使间距完全由 8px gap 控制,不再叠加。
- 影响范围全站表格操作列Avue #menu 与手写操作列),仅作用于操作列链接,不修改普通数据单元格。

View File

@@ -170,12 +170,14 @@ Avue 表格/表单配置独立存放于 `src/option/` 目录,与 `views` 和 `
- 表格操作列同时展示“查看、编辑、删除”等三个按钮时,操作列宽度统一不小于 `220px`;操作列最大按钮数量超过 3 个时,操作列宽度统一加宽到不小于 `320px`;最大按钮数量超过 4 个时,仍按 `320px` 保持列宽,并从第 5 个按钮开始换行展示,禁止出现按钮裁切或显示不全。
- 表格操作列按钮统一仅展示文字,禁止配置 `icon` / `:icon` 或在操作按钮、操作下拉项中嵌入图标;顶部工具栏按钮不受此条限制。
- 纯文字操作入口统一使用 `<el-link>`,禁止使用带 `text` 属性的 `<el-button>`;提交、确认、上传等具有明确命令语义或需要加载状态的按钮可继续使用 `<el-button>`
- 所有表格操作列中的文字链接统一保持 `8px` 间距操作按钮超过操作列可用宽度时必须自动换行禁止链接连续粘连或被裁切。Avue 自定义 `#menu` 插槽和手写 `el-table-column` 操作列均适用。
- `.avue-crud__header` 顶部间距统一为 `12px`
- 分页组件整体靠右展示,必须展示接口返回的数据总条数,`X条/页` 的页容量选择器必须放在总条数右侧。
- `.basic-container__card` 的直接子级 `.el-card__body` 内边距统一去除,保持 `padding: 0`
- 所有 CRUD 页面搜索栏需与下方表格拆分为独立区域,两者垂直间距统一为 `8px`
- 所有 CRUD 页面搜索栏操作按钮(搜索、清空等)需在搜索表单下方独占一行并放置在最右侧;搜索条件超过一行时必须显示“展开/折叠”,默认折叠且仅展示一行搜索条件;一行布局默认展示 4 个筛选项Avue 配置统一使用 `searchIndex: 4`
- 所有 CRUD 页面中新增、批量导入等顶部按钮组行不需要背景色,应保持透明背景。
- 所有 CRUD 页面顶部工具栏的“新增/新建”主操作按钮统一使用蓝底白字的实心主按钮(`type="primary"` 且不使用 `plain`),视觉标准与“新建运单”一致;批量导入、导出等辅助工具按钮仍使用描边样式。
- 所有 `.basic-container__card` 内的 Avue 内层 `el-card` 需去卡片化,仅保留外层容器卡片样式,禁止出现二级卡片边框、阴影或额外内边距。
### 4.5 全局注册组件

View File

@@ -45,6 +45,13 @@ export const reject = id =>
params: { id },
});
export const updateAttachments = row =>
request({
url: `${baseUrl}/update-attachments`,
method: 'post',
data: row,
});
export const withdraw = id =>
request({
url: `${baseUrl}/withdraw`,

View File

@@ -61,6 +61,13 @@ export const getUser = id => {
});
};
export const getCustomerOptions = () => {
return request({
url: '/blade-system/user/customer-options',
method: 'get',
});
};
export const getUserPlatform = id => {
return request({
url: '/blade-system/user/platform-detail',

View File

@@ -51,6 +51,14 @@ export const changeStatus = (id, status) => {
});
};
export const auditCertification = (id, certificationStatus, rejectReason) => {
return request({
url: '/blade-transport/transport-vehicle/audit-certification',
method: 'post',
params: { id, certificationStatus, rejectReason },
});
};
export const getExpiryStat = params => {
return request({
url: '/blade-transport/transport-vehicle/expiry-stat',

View File

@@ -40,6 +40,16 @@ export const submitApproval = id => {
});
};
export const withdrawApproval = id => {
return request({
url: '/blade-transport/customer-archive/withdraw-approval',
method: 'post',
params: {
id,
},
});
};
export const approve = id => {
return request({
url: '/blade-transport/customer-archive/approve',

View File

@@ -0,0 +1,23 @@
import request from '@/axios';
export const getList = (current, size, params) =>
request({
url: '/blade-transport/equipment-ledger/list',
method: 'get',
params: { ...params, current, size },
});
export const getDetail = id =>
request({ url: '/blade-transport/equipment-ledger/detail', method: 'get', params: { id } });
export const getNextEquipmentCode = () =>
request({ url: '/blade-transport/equipment-ledger/next-equipment-code', method: 'get' });
export const add = row =>
request({ url: '/blade-transport/equipment-ledger/submit', method: 'post', data: row });
export const update = row =>
request({ url: '/blade-transport/equipment-ledger/submit', method: 'post', data: row });
export const remove = ids =>
request({ url: '/blade-transport/equipment-ledger/remove', method: 'post', params: { ids } });

View File

@@ -85,6 +85,9 @@ const MIME_MAP = {
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
ppt: 'application/vnd.ms-powerpoint',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
eml: 'message/rfc822',
msg: 'application/vnd.ms-outlook',
zip: 'application/zip',
txt: 'text/plain',
};

View File

@@ -56,10 +56,10 @@ export const getCargoTypeOption = ctx => ({
type: 'radio',
dataType: 'number',
dicData: typeLevelDic,
search: true,
searchSpan: 6,
search: false,
value: 2,
span: 24,
order: 100,
hide: true,
minWidth: 120,
editDisabled: true,
@@ -70,20 +70,34 @@ export const getCargoTypeOption = ctx => ({
label: '货物类型',
prop: 'cargoName',
minWidth: 150,
search: true,
searchPlaceholder: '请输入',
searchSpan: 6,
searchOrder: 2,
order: 70,
placeholder: '请输入',
maxlength: 50,
showWordLimit: true,
showOverflowTooltip: true,
rules: [{ validator: validateCargoName, trigger: 'blur' }],
rules: [
{ required: true, message: '请输入货物类型名称', trigger: 'blur' },
{ validator: validateCargoName, trigger: 'blur' },
],
},
{
label: '货物类型编码',
prop: 'cargoCode',
minWidth: 140,
search: true,
searchPlaceholder: '请输入',
searchSpan: 6,
searchOrder: 1,
maxlength: 4,
order: 60,
placeholder: '请输入',
editDisabled: true,
rules: [
{ required: true, message: '请输入货物类型编码', trigger: 'blur' },
{
validator: (rule, value, callback) => {
const form = ctx.form || {};
@@ -115,6 +129,8 @@ export const getCargoTypeOption = ctx => ({
hide: true,
display: false,
span: 12,
order: 90,
placeholder: '请输入',
rules: [{ required: true, message: '请选择上级货物类型', trigger: 'change' }],
},
{
@@ -128,16 +144,19 @@ export const getCargoTypeOption = ctx => ({
minWidth: 160,
},
{
label: '上级货物编码',
label: '上级货物类型编码',
prop: 'parentCargoCode',
slot: true,
readonly: true,
editDisabled: true,
addDisabled: true,
addDisplay: false,
editDisplay: false,
display: false,
span: 12,
order: 80,
minWidth: 160,
placeholder: '请输入',
rules: [{ required: true, message: '请选择上级货物类型', trigger: 'change' }],
},
{
@@ -156,6 +175,7 @@ export const getCargoTypeOption = ctx => ({
minRows: 4,
span: 24,
minWidth: 180,
placeholder: '请输入',
maxlength: 200,
showWordLimit: true,
overHidden: true,

View File

@@ -27,18 +27,154 @@ export const createOption = () => ({
delBtn: false,
addBtn: false,
dialogClickModal: false,
menuWidth: 160,
menuFixed: 'right',
menuWidth: 180,
column: [
{
label: '地址名称',
prop: 'addressName',
search: true,
searchOrder: 8,
span: 24,
minWidth: 150,
showOverflowTooltip: true,
placeholder: '请输入',
maxlength: 100,
showWordLimit: true,
rules: [
{ required: true, message: '请输入地址名称', trigger: 'blur' },
{ max: 100, message: '地址名称不能超过100个字', trigger: 'blur' },
],
},
{
label: '地址编号',
prop: 'addressCode',
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '类型',
prop: 'addressType',
type: 'select',
formslot: true,
search: true,
searchOrder: 7,
span: 24,
minWidth: 140,
dicData: addressTypeOptions,
rules: [{ required: true, message: '请选择类型', trigger: 'change' }],
},
{
label: '站点编码',
prop: 'siteCodeDisplay',
search: false,
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '详细地址',
prop: 'detailAddress',
search: true,
searchOrder: 5,
formslot: true,
type: 'textarea',
minRows: 3,
span: 24,
minWidth: 220,
showOverflowTooltip: true,
rules: [
{ required: true, message: '请输入详细地址', trigger: 'blur' },
{ max: 255, message: '详细地址不能超过255个字', trigger: 'blur' },
],
},
{
label: '经度',
prop: 'longitude',
minWidth: 120,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '纬度',
prop: 'latitude',
minWidth: 120,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '行政区划',
prop: 'regionName',
search: true,
searchOrder: 4,
display: false,
minWidth: 150,
rules: [{ required: true, message: '请选择行政区划', trigger: 'change' }],
},
{
label: '联系人',
prop: 'contactName',
search: true,
searchOrder: 2,
minWidth: 110,
placeholder: '请输入',
maxlength: 50,
rules: [{ max: 50, message: '联系人不能超过50个字', trigger: 'blur' }],
},
{
label: '联系方式',
prop: 'contactPhone',
search: true,
searchOrder: 1,
minWidth: 130,
placeholder: '请输入',
maxlength: 20,
rules: [{ max: 20, message: '联系方式不能超过20个字', trigger: 'blur' }],
},
{
label: '组织',
prop: 'deptName',
minWidth: 150,
addDisplay: false,
editDisplay: false,
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
minRows: 4,
span: 24,
minWidth: 160,
overHidden: true,
showOverflowTooltip: true,
placeholder: '请输入',
maxlength: 200,
showWordLimit: true,
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
},
{
label: '更新时间',
prop: 'updateTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
width: 180,
addDisplay: false,
editDisplay: false,
},
{
label: '创建时间',
prop: 'createTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
width: 180,
addDisplay: false,
editDisplay: false,
},
{
label: '港口',
prop: 'portId',
@@ -75,133 +211,24 @@ export const createOption = () => ({
span: 24,
rules: [{ required: true, message: '请选择机场标准名称', trigger: 'change' }],
},
{
label: '地址名称',
prop: 'addressName',
search: true,
span: 24,
minWidth: 150,
maxlength: 100,
showWordLimit: true,
rules: [
{ required: true, message: '请输入地址名称', trigger: 'blur' },
{ max: 100, message: '地址名称不能超过100个字', trigger: 'blur' },
],
},
{
label: '地址编号',
prop: 'addressCode',
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '站点编码',
prop: 'siteCodeDisplay',
search: false,
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '站点编码',
prop: 'siteCode',
search: true,
searchOrder: 6,
hide: true,
addDisplay: false,
editDisplay: false,
},
{
label: '行政区划',
prop: 'regionName',
search: true,
display: false,
minWidth: 150,
rules: [{ required: true, message: '请选择行政区划', trigger: 'change' }],
},
{
label: '详细地址',
prop: 'detailAddress',
search: true,
formslot: true,
type: 'textarea',
minRows: 3,
span: 24,
minWidth: 220,
rules: [
{ required: true, message: '请输入详细地址', trigger: 'blur' },
{ max: 255, message: '详细地址不能超过255个字', trigger: 'blur' },
],
},
{
label: '组织',
prop: 'deptId',
type: 'select',
search: true,
searchOrder: 3,
hide: true,
display: false,
dicData: [],
},
{
label: '组织',
prop: 'deptName',
minWidth: 150,
addDisplay: false,
editDisplay: false,
},
{
label: '联系人',
prop: 'contactName',
search: true,
minWidth: 110,
maxlength: 50,
rules: [{ max: 50, message: '联系人不能超过50个字', trigger: 'blur' }],
},
{
label: '联系方式',
prop: 'contactPhone',
search: true,
minWidth: 130,
maxlength: 20,
rules: [{ max: 20, message: '联系方式不能超过20个字', trigger: 'blur' }],
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
minRows: 4,
span: 24,
hide: true,
maxlength: 200,
showWordLimit: true,
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
},
{
label: '更新人',
prop: 'updateUserName',
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '更新时间',
prop: 'updateTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
minWidth: 160,
addDisplay: false,
editDisplay: false,
},
{
label: '创建时间',
prop: 'createTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
minWidth: 160,
addDisplay: false,
editDisplay: false,
},
],
});

View File

@@ -6,6 +6,15 @@ import {
textRule,
} from './common';
const listDicFormatter = res => {
const data = res?.data || res;
if (Array.isArray(data)) return data;
if (Array.isArray(data?.records)) return data.records;
if (Array.isArray(data?.data)) return data.data;
if (Array.isArray(data?.data?.records)) return data.data.records;
return [];
};
export const config = {
title: '常用货物',
permission: 'common_cargo',
@@ -17,12 +26,29 @@ export const config = {
};
export const option = createCrudOption([
{
label: '货物名称',
prop: 'cargoName',
minWidth: 150,
placeholder: '请输入',
rules: textRule('货物名称', 100, true),
},
{
label: '货物编号',
prop: 'cargoCode',
formslot: true,
minWidth: 120,
maxlength: 20,
showWordLimit: true,
rules: [
{ required: true, message: '请输入货物编号', trigger: 'blur' },
{ max: 20, message: '货物编号不能超过20个字符', trigger: 'blur' },
],
},
{
label: '一级货物类型',
prop: 'firstCargoTypeName',
formslot: true,
search: true,
searchOrder: 3,
minWidth: 150,
editDisabled: true,
rules: [{ required: true, message: '请选择一级货物类型', trigger: 'change' }],
@@ -39,8 +65,6 @@ export const option = createCrudOption([
label: '二级货物类型',
prop: 'secondCargoTypeName',
formslot: true,
search: true,
searchOrder: 4,
minWidth: 150,
editDisabled: true,
rules: [{ required: true, message: '请选择二级货物类型', trigger: 'change' }],
@@ -48,47 +72,118 @@ export const option = createCrudOption([
{
label: '二级货物类型编码',
prop: 'secondCargoTypeCode',
hide: true,
display: false,
addDisplay: false,
editDisabled: true,
},
// 搜索字段与表格、表单字段独立,避免筛选配置影响数据展示和编辑布局。
{
label: '货物名称',
prop: 'cargoName',
prop: 'searchCargoName',
searchProp: 'cargoName',
search: true,
searchOrder: 1,
minWidth: 150,
rules: textRule('货物名称', 100, true),
searchOrder: 5,
searchPlaceholder: '请输入',
hide: true,
display: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '货物编号',
prop: 'cargoCode',
formslot: true,
prop: 'searchCargoCode',
searchProp: 'cargoCode',
search: true,
searchOrder: 4,
searchPlaceholder: '请输入',
hide: true,
display: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '一级货物类型',
prop: 'searchFirstCargoTypeName',
searchProp: 'firstCargoTypeName',
type: 'select',
searchType: 'select',
search: true,
searchOrder: 3,
searchPlaceholder: '请选择',
dicUrl: '/blade-system/cargo-type/list?current=1&size=9999&typeLevel=1',
dicFormatter: listDicFormatter,
props: {
label: 'cargoName',
value: 'cargoName',
},
filterable: true,
hide: true,
display: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '二级货物类型',
prop: 'searchSecondCargoTypeName',
searchProp: 'secondCargoTypeName',
type: 'select',
searchType: 'select',
search: true,
searchOrder: 2,
minWidth: 120,
maxlength: 20,
showWordLimit: true,
rules: [
{ required: true, message: '请输入货物编号', trigger: 'blur' },
{ max: 20, message: '货物编号不能超过20个字符', trigger: 'blur' },
],
searchPlaceholder: '请选择',
dicUrl: '/blade-system/cargo-type/list?current=1&size=9999&typeLevel=2',
dicFormatter: listDicFormatter,
props: {
label: 'cargoName',
value: 'cargoName',
},
filterable: true,
hide: true,
display: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '组织',
prop: 'deptName',
prop: 'searchDeptId',
searchProp: 'deptId',
type: 'select',
searchType: 'select',
search: true,
searchOrder: 5,
searchOrder: 1,
searchPlaceholder: '请选择',
dicData: [],
props: {
label: 'label',
value: 'value',
},
filterable: true,
hide: true,
display: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '包装品牌',
prop: 'packageBrand',
slot: true,
minWidth: 150,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '品牌',
prop: 'brand',
search: false,
hide: true,
minWidth: 120,
placeholder: '请输入',
rules: textRule('品牌', 50),
},
{
@@ -96,10 +191,25 @@ export const option = createCrudOption([
prop: 'packageType',
type: 'select',
dicData: packageOptions,
hide: true,
minWidth: 100,
},
{
label: '货值',
label: '规格',
prop: 'specification',
minWidth: 120,
placeholder: '请输入',
rules: textRule('规格', 50),
},
{
label: '型号',
prop: 'model',
minWidth: 120,
placeholder: '请输入',
rules: textRule('型号', 50),
},
{
label: '单价',
prop: 'cargoValue',
formslot: true,
minWidth: 120,
@@ -129,29 +239,26 @@ export const option = createCrudOption([
minWidth: 120,
},
{
label: '规格',
prop: 'specification',
minWidth: 120,
rules: textRule('规格', 50),
},
{
label: '型号',
prop: 'model',
minWidth: 120,
rules: textRule('型号', 50),
},
{
label: '说明',
prop: 'descriptionOne',
minWidth: 160,
rules: textRule('说明', 100),
},
{
label: '数据来源',
prop: 'dataSource',
label: '尺寸',
prop: 'sizeText',
minWidth: 120,
addDisplay: false,
editDisplay: false,
rules: textRule('尺寸', 100),
},
{
label: '其他说明1',
prop: 'descriptionOne',
minWidth: 160,
placeholder: '请输入',
rules: textRule('其他说明1', 100),
},
{
label: '其他说明2',
prop: 'descriptionTwo',
minWidth: 160,
placeholder: '请输入',
rules: textRule('其他说明2', 100),
},
{
label: '备注',
@@ -159,12 +266,25 @@ export const option = createCrudOption([
type: 'textarea',
minRows: 4,
span: 24,
hide: true,
maxlength: 200,
showWordLimit: true,
placeholder: '请输入',
rules: textRule('备注', 200),
},
...auditColumns,
{
label: '组织',
prop: 'deptName',
minWidth: 150,
addDisplay: false,
editDisplay: false,
},
...auditColumns
.filter(column => ['updateTime', 'createTime'].includes(column.prop))
.sort(
(first, second) =>
['updateTime', 'createTime'].indexOf(first.prop) -
['updateTime', 'createTime'].indexOf(second.prop)
),
]);
export const excelOption = {

View File

@@ -5,6 +5,8 @@ export const config = {
permission: 'common_route',
exportUrl: '/blade-transport/common-route/export-common-route',
exportName: '常用线路',
importUrl: '/blade-transport/common-route/import-common-route',
templateUrl: '/blade-transport/common-route/export-template',
enableAllDept: false,
};
@@ -12,17 +14,14 @@ export const option = createCrudOption([
{
label: '线路名称',
prop: 'routeName',
search: true,
searchOrder: 1,
span: 24,
span: 12,
minWidth: 150,
overHidden: true,
rules: textRule('线路名称', 100, true),
},
{
label: '线路编号',
prop: 'routeCode',
search: true,
searchOrder: 2,
minWidth: 120,
addDisplay: false,
editDisabled: true,
@@ -30,8 +29,6 @@ export const option = createCrudOption([
{
label: '发货地',
prop: 'departureName',
search: true,
searchOrder: 3,
minWidth: 140,
addDisplay: false,
editDisplay: false,
@@ -40,10 +37,9 @@ export const option = createCrudOption([
label: '发货地址',
prop: 'departureAddress',
formslot: true,
search: true,
searchOrder: 4,
span: 24,
minWidth: 220,
overHidden: true,
rules: textRule('发货地址', 255, true),
},
{
@@ -53,6 +49,8 @@ export const option = createCrudOption([
precision: 6,
minWidth: 120,
hide: true,
addDisplay: false,
editDisplay: false,
},
{
label: '发货纬度',
@@ -61,6 +59,8 @@ export const option = createCrudOption([
precision: 6,
minWidth: 120,
hide: true,
addDisplay: false,
editDisplay: false,
},
{
label: '发货联系人',
@@ -77,8 +77,6 @@ export const option = createCrudOption([
{
label: '收货地',
prop: 'arrivalName',
search: true,
searchOrder: 5,
minWidth: 140,
addDisplay: false,
editDisplay: false,
@@ -87,10 +85,9 @@ export const option = createCrudOption([
label: '收货地址',
prop: 'arrivalAddress',
formslot: true,
search: true,
searchOrder: 6,
span: 24,
minWidth: 220,
overHidden: true,
rules: textRule('收货地址', 255, true),
},
{
@@ -100,6 +97,8 @@ export const option = createCrudOption([
precision: 6,
minWidth: 120,
hide: true,
addDisplay: false,
editDisplay: false,
},
{
label: '收货纬度',
@@ -108,6 +107,8 @@ export const option = createCrudOption([
precision: 6,
minWidth: 120,
hide: true,
addDisplay: false,
editDisplay: false,
},
{
label: '收货联系人',
@@ -121,25 +122,160 @@ export const option = createCrudOption([
minWidth: 140,
rules: [phoneRule('收货联系方式'), ...textRule('收货联系方式', 50)],
},
{
label: '组织',
prop: 'deptName',
search: true,
searchOrder: 7,
minWidth: 150,
addDisplay: false,
editDisplay: false,
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
minRows: 4,
span: 24,
hide: true,
minWidth: 180,
maxlength: 500,
showWordLimit: true,
rules: textRule('备注', 500),
},
...auditColumns,
{
label: '组织',
prop: 'deptName',
minWidth: 150,
addDisplay: false,
editDisplay: false,
},
// 搜索字段独立于表格和表单字段,避免筛选配置影响数据展示和编辑布局。
{
label: '线路名称',
prop: 'searchRouteName',
searchProp: 'routeName',
search: true,
searchOrder: 7,
searchPlaceholder: '请输入',
hide: true,
display: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '线路编号',
prop: 'searchRouteCode',
searchProp: 'routeCode',
search: true,
searchOrder: 6,
searchPlaceholder: '请输入',
hide: true,
display: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '发货地',
prop: 'searchDepartureName',
searchProp: 'departureName',
search: true,
searchOrder: 5,
searchPlaceholder: '请输入',
hide: true,
display: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '发货地址',
prop: 'searchDepartureAddress',
searchProp: 'departureAddress',
search: true,
searchOrder: 4,
searchPlaceholder: '请输入',
hide: true,
display: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '收货地',
prop: 'searchArrivalName',
searchProp: 'arrivalName',
search: true,
searchOrder: 3,
searchPlaceholder: '请输入',
hide: true,
display: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '收货地址',
prop: 'searchArrivalAddress',
searchProp: 'arrivalAddress',
search: true,
searchOrder: 2,
searchPlaceholder: '请输入',
hide: true,
display: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '组织',
prop: 'searchDeptId',
searchProp: 'deptId',
search: true,
searchOrder: 1,
searchType: 'cascader',
searchPlaceholder: '请选择',
hide: true,
display: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
dicData: [],
props: {
label: 'label',
value: 'value',
children: 'children',
},
checkStrictly: true,
emitPath: false,
showAllLevels: false,
filterable: true,
},
...auditColumns
.filter(column => column.prop !== 'createUserName')
.map(column =>
['createTime', 'updateTime'].includes(column.prop) ? { ...column, minWidth: 190 } : column
)
.sort(
(first, second) =>
['updateUserName', 'updateTime', 'createTime'].indexOf(first.prop) -
['updateUserName', 'updateTime', 'createTime'].indexOf(second.prop)
),
]);
export const excelOption = {
submitBtn: false,
emptyBtn: false,
column: [
{
label: '导入模板',
prop: 'excelTemplate',
formslot: true,
span: 24,
},
{
label: 'Excel文件',
prop: 'excelFile',
type: 'upload',
drag: true,
loadText: '模板上传中,请稍等',
span: 24,
propsHttp: {
res: 'data',
},
tip: '请上传 .xls,.xlsx 标准格式文件',
},
],
};

View File

@@ -87,8 +87,10 @@ export const approvalStatusOptions = [
];
export const effectiveTypeOptions = [
{ label: '临时', value: 'temporary' },
{ label: '正式', value: 'formal' },
{ label: '未生效', value: 'not_effective' },
{ label: '临时生效', value: 'temporary_effective' },
{ label: '正式生效', value: 'formal_effective' },
{ label: '已过期', value: 'expired' },
];
export const businessTypeOptions = [

View File

@@ -6,11 +6,18 @@ import {
createCrudOption,
effectiveTypeOptions,
selectRule,
settlementModeOptions,
signTypeOptions,
textRule,
} from './common';
const contractSettlementModeOptions = [
{ label: '现结', value: '现结' },
{ label: '日结', value: '日结' },
{ label: '周结', value: '周结' },
{ label: '月结', value: '月结' },
{ label: '按固定天数周期', value: '按固定天数周期' },
];
const listDicFormatter = res => {
const data = res?.data || res;
if (Array.isArray(data)) return data;
@@ -25,7 +32,13 @@ export const config = {
permission: 'contract_manage',
exportUrl: '/blade-transport/contract-manage/export-contract-manage',
exportName: '合同管理',
enableAllDept: false,
defaultForm: {
contractCategory: '客户合同',
},
projectPlaceholder: '请选择',
organizationPlaceholder: '请选择',
enableAllDept: true,
allDeptReadonly: false,
batchDelete: false,
enableContractForm: true,
enableProjectSelect: true,
@@ -39,6 +52,7 @@ export const config = {
enableChangeRecord: true,
enableOrganizationSelect: true,
enableContractCreateActions: true,
enableContractExpiryTags: true,
actions: ['copy'],
statusProp: 'approvalStatus',
statusTextProp: 'approvalStatusName',
@@ -63,9 +77,25 @@ export const config = {
label: '撤回',
statusProp: 'approvalStatus',
status: ['reviewing'],
stage: ['temporary'],
stage: ['temporary', 'formal'],
permission: 'contract_manage_withdraw',
},
{
action: 'approve',
label: '通过',
statusProp: 'approvalStatus',
status: ['reviewing'],
stage: ['temporary', 'formal'],
permission: 'contract_manage_approve',
},
{
action: 'reject',
label: '驳回',
statusProp: 'approvalStatus',
status: ['reviewing'],
stage: ['temporary', 'formal'],
permission: 'contract_manage_reject',
},
{
action: 'submitFormal',
label: '重新提交转正',
@@ -143,16 +173,18 @@ export const option = createCrudOption([
prop: 'contractNo',
search: true,
searchOrder: 6,
searchPlaceholder: '请输入',
order: 185,
disabled: true,
placeholder: '不填写,则系统自动生成',
minWidth: 160,
minWidth: 200,
},
{
label: '合同名称',
prop: 'contractName',
search: true,
searchOrder: 7,
searchPlaceholder: '请输入',
order: 180,
minWidth: 180,
rules: textRule('合同名称', 100, true),
@@ -163,6 +195,7 @@ export const option = createCrudOption([
search: true,
searchOrder: 8,
searchType: 'select',
searchPlaceholder: '请选择',
formslot: true,
order: 140,
dicUrl: '/blade-transport/project-apply/list?current=1&size=9999',
@@ -187,6 +220,7 @@ export const option = createCrudOption([
search: true,
searchOrder: 3,
searchType: 'cascader',
searchPlaceholder: '全部',
formslot: true,
order: 120,
dicData: [],
@@ -208,6 +242,7 @@ export const option = createCrudOption([
type: 'select',
search: true,
searchOrder: 9,
searchPlaceholder: '请选择',
order: 170,
dicData: contractCategoryOptions,
minWidth: 130,
@@ -217,10 +252,8 @@ export const option = createCrudOption([
label: '签约类型',
prop: 'signType',
type: 'select',
search: true,
searchOrder: 1,
search: false,
span: 6,
row: true,
order: 190,
dicData: signTypeOptions,
minWidth: 120,
@@ -232,6 +265,7 @@ export const option = createCrudOption([
type: 'select',
search: true,
searchOrder: 5,
searchPlaceholder: '请选择',
dicData: effectiveTypeOptions,
hide: true,
display: false,
@@ -241,7 +275,8 @@ export const option = createCrudOption([
prop: 'partyA',
type: 'select',
order: 160,
dicUrl: '/blade-transport/customer-archive/list?current=1&size=9999&approvalStatus=approved',
dicUrl:
'/blade-transport/customer-archive/list?current=1&size=9999&customerType=客户&approvalStatus=approved&status=1',
dicFormatter: listDicFormatter,
props: {
label: 'fullName',
@@ -256,7 +291,8 @@ export const option = createCrudOption([
prop: 'partyB',
type: 'select',
order: 150,
dicUrl: '/blade-transport/customer-archive/list?current=1&size=9999&approvalStatus=approved',
dicUrl:
'/blade-transport/customer-archive/list?current=1&size=9999&customerType=承运商&approvalStatus=approved&status=1',
dicFormatter: listDicFormatter,
props: {
label: 'fullName',
@@ -334,14 +370,14 @@ export const option = createCrudOption([
rules: [{ required: true, message: '请选择签订日期', trigger: 'change' }],
},
{
label: '结算方式',
label: '结算类型',
prop: 'settlementMode',
type: 'select',
dicData: settlementModeOptions,
dicData: contractSettlementModeOptions,
hide: true,
order: 90,
minWidth: 120,
rules: selectRule('结算方式'),
rules: selectRule('结算类型'),
},
{
label: '合同格式',
@@ -392,7 +428,7 @@ export const option = createCrudOption([
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
minWidth: 160,
minWidth: 190,
addDisplay: false,
editDisplay: false,
},
@@ -402,6 +438,7 @@ export const option = createCrudOption([
type: 'select',
search: true,
searchOrder: 2,
searchPlaceholder: '请选择',
slot: true,
dicData: contractStageOptions,
minWidth: 120,
@@ -414,6 +451,7 @@ export const option = createCrudOption([
type: 'select',
search: true,
searchOrder: 4,
searchPlaceholder: '请选择',
slot: true,
dicData: contractApprovalStatusOptions,
minWidth: 130,
@@ -572,3 +610,6 @@ export const option = createCrudOption([
option.span = 8;
option.dialogWidth = 1480;
option.dialogTop = '10px';
option.dialogCloseBtn = true;
option.menuWidth = 320;

View File

@@ -6,21 +6,20 @@ import {
selectRule,
textRule,
} from './common';
import { applyTableMenuWidth } from '@/utils/table-menu';
export const config = {
title: '过程配置',
permission: 'process_config',
exportUrl: '/blade-transport/process-config/export-process-config',
exportName: '过程配置',
enableAllDept: false,
enableAllDept: true,
actions: ['copy', 'enable', 'disable'],
statusProp: 'status',
statusTextProp: 'statusName',
deleteStatus: [0, 2],
};
export const option = applyTableMenuWidth({
export const option = {
...createCrudOption([
{
label: '',
@@ -29,21 +28,13 @@ export const option = applyTableMenuWidth({
span: 24,
hide: true,
},
{
label: '配置编号',
prop: 'configCode',
search: true,
searchOrder: 4,
minWidth: 130,
addDisplay: false,
editDisplay: false,
editDisabled: true,
},
{
label: '配置名称',
prop: 'configName',
search: true,
searchOrder: 6,
searchSpan: 6,
searchPlaceholder: '请输入',
span: 12,
minWidth: 150,
addDisplay: false,
@@ -51,13 +42,16 @@ export const option = applyTableMenuWidth({
rules: textRule('配置名称', 100, true),
},
{
label: '项目ID集合',
prop: 'projectIds',
hide: true,
display: false,
label: '配置编号',
prop: 'configCode',
search: true,
searchOrder: 4,
searchSpan: 6,
searchPlaceholder: '请输入',
minWidth: 130,
addDisplay: false,
editDisplay: false,
rules: textRule('项目ID集合', 1000, true),
editDisabled: true,
},
{
label: '项目',
@@ -65,7 +59,9 @@ export const option = applyTableMenuWidth({
formslot: true,
search: true,
searchOrder: 5,
searchType: 'select',
searchType: 'input',
searchSpan: 6,
searchPlaceholder: '请输入',
type: 'textarea',
minRows: 2,
span: 12,
@@ -77,53 +73,98 @@ export const option = applyTableMenuWidth({
rules: textRule('项目', 1000, true),
},
{
label: '',
searchLabel: '包含过程节点',
label: '过程节点',
prop: 'includedNodes',
search: true,
searchOrder: 3,
type: 'select',
multiple: true,
dicData: nodeOptions,
minWidth: 180,
formslot: true,
span: 24,
minWidth: 260,
overHidden: false,
slot: true,
addDisplay: false,
editDisplay: false,
rules: selectRule('包含过程节点'),
rules: selectRule('过程节点'),
},
{
label: '默认完成运输天数',
label: '默认运输完成天数',
prop: 'defaultFinishDays',
formslot: true,
type: 'number',
min: 1,
min: 0,
max: 999,
span: 12,
minWidth: 170,
addDisplay: false,
editDisplay: false,
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
minRows: 4,
span: 24,
minWidth: 180,
addDisplay: false,
editDisplay: false,
maxlength: 500,
showWordLimit: true,
rules: textRule('备注', 500),
},
{
label: '组织',
prop: 'deptName',
minWidth: 150,
addDisplay: false,
editDisplay: false,
},
...auditColumns
.filter(column => ['updateTime', 'createTime'].includes(column.prop))
.map(column => ({ ...column, minWidth: 190 }))
.sort(
(first, second) =>
['updateTime', 'createTime'].indexOf(first.prop) -
['updateTime', 'createTime'].indexOf(second.prop)
),
{
label: '项目ID集合',
prop: 'projectIds',
hide: true,
display: false,
addDisplay: false,
editDisplay: false,
rules: textRule('项目ID集合', 1000, true),
},
{
label: '状态',
prop: 'status',
type: 'select',
search: true,
searchOrder: 1,
searchSpan: 6,
searchPlaceholder: '请选择',
dicData: processStatusOptions,
slot: true,
minWidth: 100,
hide: true,
fixed: false,
addDisplay: false,
editDisplay: false,
},
{
label: '组织',
prop: 'deptName',
prop: 'searchDeptId',
searchProp: 'deptId',
type: 'select',
search: true,
searchOrder: 2,
minWidth: 150,
searchSpan: 6,
searchPlaceholder: '请选择',
hide: true,
display: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
dicData: [],
filterable: true,
},
{
label: '',
@@ -132,23 +173,10 @@ export const option = applyTableMenuWidth({
span: 24,
hide: true,
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
minRows: 4,
span: 24,
hide: true,
addDisplay: false,
editDisplay: false,
maxlength: 500,
showWordLimit: true,
rules: textRule('备注', 500),
},
...auditColumns,
]),
saveBtnText: '保存',
updateBtnText: '保存',
dialogTop: '10px',
dialogWidth: '96%',
dialogCustomClass: 'process-config-dialog',
searchSpan: 8,
}, 5);
menuWidth: 200,
};

View File

@@ -29,6 +29,14 @@ const userDicFormatter = res => {
}));
};
const formatUserRealName = (row, role) =>
row[`${role}RealName`] ||
row[`${role}UserRealName`] ||
row[`${role}User`]?.realName ||
row[role]?.realName ||
row[`${role}UserName`] ||
'';
export const config = {
title: '项目管理',
permission: 'project_apply',
@@ -94,8 +102,9 @@ export const option = createCrudOption([
label: '立项申请单号',
prop: 'applyNo',
search: true,
searchPlaceholder: '请输入',
searchOrder: 7,
minWidth: 160,
minWidth: 220,
addDisplay: false,
editDisabled: true,
},
@@ -110,6 +119,7 @@ export const option = createCrudOption([
label: '项目名称',
prop: 'projectName',
search: true,
searchPlaceholder: '请输入',
searchOrder: 6,
minWidth: 180,
maxlength: 50,
@@ -128,6 +138,7 @@ export const option = createCrudOption([
prop: 'projectType',
type: 'select',
search: true,
searchPlaceholder: '请选择',
searchOrder: 3,
dicData: projectTypeOptions,
minWidth: 120,
@@ -156,7 +167,7 @@ export const option = createCrudOption([
label: '承办部门',
prop: 'undertakeDeptName',
search: true,
searchLabel: '承办单位',
searchPlaceholder: '请输入',
searchOrder: 5,
minWidth: 150,
rules: textRule('承办部门', 50, true),
@@ -170,6 +181,7 @@ export const option = createCrudOption([
label: '项目负责人',
prop: 'principalUserName',
minWidth: 130,
formatter: row => formatUserRealName(row, 'principal'),
rules: textRule('项目负责人', 50, true),
},
{
@@ -177,6 +189,7 @@ export const option = createCrudOption([
prop: 'principalUserId',
type: 'select',
search: true,
searchPlaceholder: '请选择',
searchOrder: 4,
filterable: true,
dicUrl: '/blade-system/user/user-list',
@@ -193,6 +206,7 @@ export const option = createCrudOption([
prop: 'handlerUserName',
hide: true,
minWidth: 130,
formatter: row => formatUserRealName(row, 'handler'),
rules: textRule('项目经办人', 50, true),
},
{
@@ -336,6 +350,7 @@ export const option = createCrudOption([
prop: 'approvalStatus',
type: 'select',
search: true,
searchPlaceholder: '请选择',
searchOrder: 2,
slot: true,
dicData: projectApprovalStatusOptions,
@@ -362,6 +377,7 @@ export const option = createCrudOption([
prop: 'effectiveType',
type: 'select',
search: true,
searchPlaceholder: '请选择',
searchOrder: 1,
dicData: effectiveTypeOptions,
value: 'temporary',

View File

@@ -28,6 +28,9 @@ export const config = {
exportName: '临时额度申请',
enableAllDept: false,
enableProjectSelect: true,
projectQueryParams: {
temporaryCreditLimitSelectable: true,
},
enableAttachmentTable: true,
enableDraftSave: true,
saveAsDraft: true,
@@ -108,6 +111,7 @@ export const option = {
value: 'projectName',
},
filterable: true,
span: 12,
minWidth: 180,
rules: textRule('项目名称', 100, true),
},
@@ -122,14 +126,16 @@ export const option = {
prop: 'projectCode',
hide: true,
disabled: true,
span: 12,
minWidth: 150,
rules: textRule('项目编号', 50, true),
rules: textRule('项目编号', 50),
},
{
label: '承办部门',
prop: 'undertakeDeptName',
hide: true,
disabled: true,
span: 12,
minWidth: 150,
rules: textRule('承办部门', 50, true),
},
@@ -138,6 +144,7 @@ export const option = {
prop: 'projectFundLimit',
hide: true,
disabled: true,
span: 12,
minWidth: 200,
rules: [nonNegativeRule('项目资金使用额度')],
},
@@ -146,6 +153,7 @@ export const option = {
prop: 'usedFundLimit',
hide: true,
disabled: true,
span: 12,
minWidth: 220,
value: 0,
rules: [nonNegativeRule('已使用项目资金额度')],
@@ -155,6 +163,7 @@ export const option = {
prop: 'remainingFundLimit',
hide: true,
disabled: true,
span: 12,
minWidth: 230,
value: 0,
rules: [nonNegativeRule('剩余项目资金使用额度')],
@@ -163,6 +172,7 @@ export const option = {
label: '申请临时额度(万元)',
prop: 'applyLimit',
formslot: true,
span: 12,
minWidth: 180,
rules: [
{ required: true, message: '请输入申请临时额度', trigger: 'blur' },
@@ -175,6 +185,7 @@ export const option = {
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
span: 12,
minWidth: 140,
rules: [{ required: true, message: '请选择申请有效期至', trigger: 'change' }],
},
@@ -260,6 +271,6 @@ export const option = {
},
...auditColumns.map(column => ({ ...column, hide: true })),
]),
dialogWidth: 1480,
dialogWidth: 1010,
index: false,
};

View File

@@ -1,4 +1,3 @@
import website from '@/config/website';
import { getDeptLazyTree } from '@/api/system/dept';
export const userOption = safe => {
@@ -25,42 +24,41 @@ export const userOption = safe => {
searchShow: true,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchIndex: 4,
searchMenuPosition: 'right',
border: true,
index: true,
selection: true,
addBtn: false,
viewBtn: true,
dialogType: 'drawer',
dialogWidth: 1000,
dialogType: 'dialog',
dialogWidth: '90%',
labelPosition: 'right',
labelWidth: 120,
dialogClickModal: false,
column: [
{
label: '登录账号',
label: '账号',
prop: 'account',
search: true,
searchOrder: 2,
display: false,
},
{
label: '所属租户',
prop: 'tenantName',
slot: true,
display: false,
},
{
label: '用户姓名',
label: '姓名',
prop: 'realName',
search: true,
searchOrder: 4,
display: false,
},
{
label: '所属角色',
prop: 'roleName',
slot: true,
overHidden: true,
label: '手机号',
prop: 'phone',
search: true,
searchOrder: 3,
display: false,
},
{ label: '工号', prop: 'code', display: false },
{
label: '所属部门',
prop: 'deptName',
@@ -68,33 +66,22 @@ export const userOption = safe => {
overHidden: true,
display: false,
},
{ label: '岗位', prop: 'postName', overHidden: true, display: false },
{ label: '性别', prop: 'sexName', width: 80, display: false },
{ label: 'OA人员id', prop: 'oaPersonId', minWidth: 120, overHidden: true, display: false },
{ label: '备注', prop: 'remark', minWidth: 160, overHidden: true, display: false },
{
label: '用户平台',
prop: 'userTypeName',
width: 100,
slot: true,
display: false,
},
{
label: '用户平台',
label: '状态',
prop: 'status',
type: 'select',
dicUrl: '/blade-system/dict/dictionary?code=user_type',
props: {
label: 'dictValue',
value: 'dictKey',
},
dicData: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
],
dataType: 'number',
search: true,
hide: true,
searchOrder: 1,
display: false,
prop: 'userType',
rules: [
{
required: true,
message: '请选择用户平台',
trigger: 'blur',
},
],
},
],
group: [
@@ -104,53 +91,13 @@ export const userOption = safe => {
icon: 'el-icon-user-solid',
column: [
{
label: '所属租户',
prop: 'tenantId',
type: 'tree',
dicUrl: '/blade-system/tenant/select',
props: {
label: 'tenantName',
value: 'tenantId',
},
hide: !website.tenantMode,
addDisplay: website.tenantMode,
editDisplay: website.tenantMode,
viewDisplay: website.tenantMode,
rules: [
{
required: true,
message: '请输入所属租户',
trigger: 'click',
},
],
span: 24,
},
{
label: '登录账号',
label: '账号名',
prop: 'account',
span: 6,
rules: [
{
required: true,
message: '请输入登录账号',
trigger: 'blur',
},
],
},
{
label: '用户平台',
type: 'select',
dicUrl: '/blade-system/dict/dictionary?code=user_type',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
prop: 'userType',
rules: [
{
required: true,
message: '请选择用户平台',
message: '请输入账号',
trigger: 'blur',
},
],
@@ -161,6 +108,7 @@ export const userOption = safe => {
hide: true,
editDisplay: false,
viewDisplay: false,
addDisplay: false,
rules: [{ required: true, validator: validatePass, trigger: 'blur' }],
},
{
@@ -169,6 +117,7 @@ export const userOption = safe => {
hide: true,
editDisplay: false,
viewDisplay: false,
addDisplay: false,
rules: [{ required: true, validator: validatePass2, trigger: 'blur' }],
},
],
@@ -191,12 +140,13 @@ export const userOption = safe => {
],
},
{
label: '用户姓名',
label: '姓名',
prop: 'realName',
span: 6,
rules: [
{
required: true,
message: '请输入用户姓名',
message: '请输入姓名',
trigger: 'blur',
},
{
@@ -209,12 +159,14 @@ export const userOption = safe => {
{
label: '手机号码',
prop: 'phone',
span: 6,
rules: [{ required: true, message: '请输入手机号码', trigger: 'blur' }],
overHidden: true,
},
{
label: '电子邮箱',
prop: 'email',
hide: true,
span: 6,
overHidden: true,
},
{
@@ -235,7 +187,7 @@ export const userOption = safe => {
value: 3,
},
],
hide: true,
span: 6,
},
{
label: '用户生日',
@@ -261,6 +213,8 @@ export const userOption = safe => {
{
label: '用户编号',
prop: 'code',
label: '工号',
span: 6,
},
{
label: '所属角色',
@@ -273,13 +227,7 @@ export const userOption = safe => {
},
checkStrictly: true,
slot: true,
rules: [
{
required: true,
message: '请选择所属角色',
trigger: 'click',
},
],
display: false,
},
{
label: '所属部门',
@@ -292,6 +240,7 @@ export const userOption = safe => {
},
checkStrictly: true,
slot: true,
span: 6,
rules: [
{
required: true,
@@ -304,7 +253,7 @@ export const userOption = safe => {
label: '所属岗位',
prop: 'postId',
type: 'tree',
multiple: true,
multiple: false,
dicData: [],
props: {
label: 'postName',
@@ -317,6 +266,7 @@ export const userOption = safe => {
trigger: 'click',
},
],
span: 6,
},
{
label: '直属主管',
@@ -330,6 +280,7 @@ export const userOption = safe => {
value: 'id',
},
clearable: true,
display: false,
},
{
label: '是否主管',
@@ -346,6 +297,49 @@ export const userOption = safe => {
},
],
value: 0,
display: false,
},
{
label: '人员类别',
prop: 'personCategory',
type: 'select',
span: 6,
value: 1,
dicData: [
{ label: '内部员工', value: 1 },
{ label: '承运商', value: 2 },
],
},
{
label: '数据权限范围',
prop: 'dataScopeRange',
type: 'radio',
span: 24,
value: 1,
dicData: [
{ label: '仅所属组织', value: 1 },
{ label: '全部组织', value: 2 },
{ label: '自定义', value: 3 },
],
},
{
label: '数据层级范围',
prop: 'dataLevelRange',
type: 'radio',
span: 24,
value: 1,
dicData: [
{ label: '包含下级', value: 1 },
{ label: '仅本级', value: 2 },
],
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
span: 24,
maxlength: 200,
showWordLimit: true,
},
],
},

View File

@@ -16,62 +16,67 @@ export const option = {
editBtn: false,
delBtn: false,
viewBtn: false,
menuWidth: 220,
menuWidth: 180,
dialogClickModal: false,
column: [
{
label: '姓名',
label: '姓名身份证号',
prop: 'driverName',
search: true,
searchOrder: 7,
slot: true,
minWidth: 120,
minWidth: 220,
},
{
label: '手机号',
label: '手机号',
prop: 'mobile',
search: true,
searchOrder: 6,
minWidth: 130,
},
{
label: '准驾车型',
prop: 'drivingType',
label: '性别',
prop: 'gender',
width: 80,
align: 'center',
},
{
label: '司机类型',
prop: 'driverType',
search: true,
searchOrder: 5,
hide: true,
searchOrder: 3,
type: 'select',
dicData: [
{
label: 'A1',
value: 'A1',
label: '自有员工',
value: '自有',
},
{
label: 'A2',
value: 'A2',
label: '外协',
value: '外协',
},
{
label: 'A3',
value: 'A3',
},
{
label: 'B1',
value: 'B1',
},
{
label: 'B2',
value: 'B2',
},
{
label: 'C1',
value: 'C1',
},
{
label: 'C2',
value: 'C2',
label: '承运管理员',
value: '承运管理员',
},
],
minWidth: 110,
align: 'center',
},
{
label: '岗位',
prop: 'posts',
minWidth: 150,
},
{
label: '所属组织',
prop: 'organizationName',
search: true,
searchOrder: 1,
type: 'select',
filterable: true,
dicData: [],
minWidth: 160,
},
{
label: '状态',
@@ -94,65 +99,52 @@ export const option = {
align: 'center',
},
{
label: '司机类型',
prop: 'driverType',
search: true,
searchOrder: 3,
type: 'select',
dicData: [
{
label: '自有员工',
value: '自有',
},
{
label: '外协',
value: '外协',
},
],
minWidth: 110,
align: 'center',
},
{
label: '身份证号',
prop: 'idCardNo',
search: true,
searchOrder: 2,
minWidth: 190,
},
{
label: '所属组织',
prop: 'organizationName',
search: true,
searchOrder: 1,
type: 'select',
filterable: true,
dicData: [],
minWidth: 160,
},
{
label: '性别',
prop: 'gender',
width: 80,
align: 'center',
},
{
label: '岗位',
prop: 'posts',
minWidth: 150,
},
{
label: '驾驶认证',
label: '驾驶认证(日期)',
prop: 'drivingLicenseEndDate',
slot: true,
minWidth: 140,
align: 'center',
},
{
label: '从业资格认证',
label: '从业资格认证(日期)',
prop: 'qualificationEndDate',
slot: true,
minWidth: 150,
align: 'center',
minWidth: 160,
},
{
label: '身份证号',
prop: 'idCardNo',
search: true,
searchOrder: 2,
hide: true,
},
{
label: '准驾车型',
prop: 'drivingType',
search: true,
searchOrder: 5,
hide: true,
type: 'select',
dicData: [
{ label: 'A1', value: 'A1' },
{ label: 'A2', value: 'A2' },
{ label: 'A3', value: 'A3' },
{ label: 'B1', value: 'B1' },
{ label: 'B2', value: 'B2' },
{ label: 'C1', value: 'C1' },
{ label: 'C2', value: 'C2' },
],
},
{
label: '',
prop: 'expireStatus',
search: true,
searchOrder: 0,
searchSpan: 6,
hide: true,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
],
};

View File

@@ -14,23 +14,40 @@ export const option = {
editBtn: false,
delBtn: false,
viewBtn: false,
menuWidth: 160,
menuWidth: 320,
dialogClickModal: false,
column: [
{ label: '船舶名', prop: 'shipName', search: true, searchOrder: 5, slot: true, width: 130 },
{
label: '船舶名',
prop: 'shipName',
search: true,
searchOrder: 5,
slot: true,
minWidth: 130,
},
{
label: '船舶识别号',
prop: 'shipIdentifierNo',
search: true,
searchOrder: 4,
width: 140,
minWidth: 140,
},
{
label: '所属组织',
prop: 'organizationName',
search: true,
searchOrder: 1,
type: 'select',
filterable: true,
dicData: [],
minWidth: 160,
},
{
label: '船检登记号',
prop: 'shipInspectionNo',
search: true,
searchOrder: 3,
width: 130,
minWidth: 130,
},
{
label: '船舶类型',
@@ -44,52 +61,47 @@ export const option = {
{ label: '杂货船', value: '杂货船' },
{ label: '油船', value: '油船' },
],
width: 110,
align: 'center',
},
{
label: '所属组织',
prop: 'organizationName',
search: true,
searchOrder: 1,
type: 'select',
filterable: true,
dicData: [],
width: 160,
},
{
label: '状态',
prop: 'status',
type: 'select',
slot: true,
dicData: [
{ label: '启用', value: 1 },
{ label: '停用', value: 2 },
],
width: 90,
minWidth: 110,
align: 'center',
},
{
label: '国籍证有效期至',
prop: 'nationalityCertEndDate',
slot: true,
width: 150,
minWidth: 150,
align: 'center',
},
{
label: '最低安全配员证书有效期至',
prop: 'safeManningCertEndDate',
slot: true,
width: 210,
minWidth: 210,
align: 'center',
},
{
label: '营业运输证有效期至',
prop: 'businessTransportCertEndDate',
slot: true,
width: 170,
minWidth: 170,
align: 'center',
},
{ label: '承租有效期至', prop: 'leaseEndDate', slot: true, width: 140, align: 'center' },
{
label: '承租有效期至',
prop: 'leaseEndDate',
slot: true,
minWidth: 140,
align: 'center',
},
{
label: '',
prop: 'expireStatus',
search: true,
searchOrder: 0,
searchSpan: 6,
hide: true,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
],
};

View File

@@ -9,21 +9,37 @@ export const option = {
searchMenuPosition: 'right',
border: true,
index: true,
selection: true,
selection: false,
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: false,
menuWidth: 160,
menuWidth: 320,
dialogClickModal: false,
column: [
{
label: '车牌号',
prop: 'plateNo',
search: true,
searchOrder: 5,
searchOrder: 6,
slot: true,
width: 120,
minWidth: 120,
},
{
label: '业务关系',
prop: 'businessRelation',
search: true,
searchOrder: 5,
searchValue: '',
type: 'select',
dicData: [
{ label: '全部', value: '' },
{ label: '自有', value: '自有' },
{ label: '管理挂靠', value: '管理挂靠' },
{ label: '业务合作', value: '业务合作' },
],
minWidth: 120,
align: 'center',
},
{
label: '所属组织',
@@ -33,28 +49,13 @@ export const option = {
type: 'select',
filterable: true,
dicData: [],
width: 150,
},
{
label: '业务关系',
prop: 'businessRelation',
search: true,
searchOrder: 4,
type: 'select',
dicData: [
{ label: '全部', value: '' },
{ label: '自有', value: '自有' },
{ label: '管理挂靠', value: '管理挂靠' },
{ label: '业务合作', value: '业务合作' },
],
width: 120,
align: 'center',
minWidth: 150,
},
{
label: '车辆类型',
prop: 'vehicleType',
search: true,
searchOrder: 3,
searchOrder: 4,
type: 'select',
dicData: [
{ label: '全部', value: '' },
@@ -65,49 +66,95 @@ export const option = {
{ label: '拖挂车', value: '拖挂' },
{ label: '半挂车', value: '半挂' },
],
width: 110,
minWidth: 110,
align: 'center',
},
{
label: '核定载质量(KG)',
prop: 'approvedLoadKg',
width: 140,
minWidth: 140,
align: 'right',
},
{
label: '绑定司机',
prop: 'boundDriver',
slot: true,
minWidth: 120,
align: 'center',
},
{
label: '车辆类型',
prop: 'vehicleType',
minWidth: 110,
align: 'center',
},
{
label: '认证状态',
prop: 'certificationStatus',
slot: true,
search: true,
searchOrder: 2,
searchValue: '',
type: 'select',
dicData: [
{ label: '全部', value: '' },
{ label: '认证中', value: 0 },
{ label: '已认证', value: 1 },
{ label: '已驳回', value: 2 },
],
minWidth: 110,
align: 'center',
},
{
label: '车辆状态',
prop: 'status',
search: true,
searchOrder: 2,
searchOrder: 3,
type: 'select',
slot: true,
dicData: [
{ label: '启用', value: 1 },
{ label: '停用', value: 2 },
],
width: 90,
minWidth: 90,
align: 'center',
},
{
label: '行驶证有效期',
label: '道路运输证号',
prop: 'roadTransportCertNo',
minWidth: 160,
},
{
label: '行驶证认证',
prop: 'drivingLicenseEndDate',
slot: true,
width: 140,
minWidth: 140,
align: 'center',
},
{
label: '道路运输证有效期',
label: '道路运输证认证',
prop: 'roadTransportCertEndDate',
slot: true,
width: 160,
minWidth: 160,
align: 'center',
},
{
label: '年审有效期',
label: '道路运输证年审',
prop: 'annualReviewEndDate',
slot: true,
width: 130,
minWidth: 130,
align: 'center',
},
{
label: '',
prop: 'expireStatus',
search: true,
searchOrder: 0,
searchSpan: 6,
hide: true,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
],
};

View File

@@ -49,10 +49,11 @@ export const option = {
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
column: [
{
label: '车船类型',
@@ -124,9 +125,8 @@ export const option = {
{
label: '直接经济损失',
prop: 'directEconomicLoss',
type: 'number',
precision: 2,
append: '元',
type: 'input',
formslot: true,
minWidth: 140,
span: 12,
placeholder: '请输入',
@@ -135,9 +135,8 @@ export const option = {
{
label: '保险理赔金额',
prop: 'insuranceClaimAmount',
type: 'number',
precision: 2,
append: '元',
type: 'input',
formslot: true,
minWidth: 140,
span: 12,
placeholder: '请输入',

View File

@@ -48,10 +48,11 @@ export const option = {
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
column: [
{
label: '车船类型',
@@ -140,9 +141,8 @@ export const option = {
{
label: '费用',
prop: 'fee',
type: 'number',
precision: 2,
append: '元',
type: 'input',
formslot: true,
minWidth: 100,
span: 12,
placeholder: '请输入',
@@ -155,6 +155,7 @@ export const option = {
label: '评定(复核)单位',
prop: 'assessmentUnit',
minWidth: 160,
overHidden: true,
span: 12,
placeholder: '请输入',
rules: [{ max: 50, message: '最多 50 个字符', trigger: 'blur' }],

View File

@@ -0,0 +1,226 @@
import { formatUpdateUserName } from '@/utils/audit';
export const vehicleTypeDic = [
{ label: '车辆', value: '车辆' },
{ label: '船舶', value: '船舶' },
];
export const onlineStatusDic = [
{ label: '否', value: 0 },
{ label: '是', value: 1 },
];
export const option = {
height: 'auto',
calcHeight: 32,
dialogWidth: 960,
labelPosition: 'right',
labelWidth: 'auto',
searchLabelWidth: 160,
tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
saveBtnText: '提交',
updateBtnText: '提交',
searchShow: true,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 4,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuFixed: 'right',
column: [
{
label: '车船类型',
prop: 'vehicleType',
type: 'radio',
dicData: vehicleTypeDic,
value: '车辆',
search: true,
searchOrder: 3,
span: 24,
order: 1,
minWidth: 110,
rules: [{ required: true, message: '请选择车船类型', trigger: 'change' }],
},
{
label: '车牌号/船号',
prop: 'vehicleNo',
slot: true,
formslot: true,
search: true,
searchOrder: 4,
span: 12,
order: 2,
minWidth: 140,
rules: [{ required: true, message: '请输入车牌号/船号', trigger: 'blur' }],
},
{
label: '设备号',
prop: 'equipmentCode',
formslot: true,
search: true,
searchOrder: 2,
span: 12,
order: 3,
minWidth: 150,
rules: [{ required: true, message: '设备编号由系统生成', trigger: 'blur' }],
},
{
label: '设备名称',
prop: 'equipmentName',
span: 12,
order: 4,
minWidth: 150,
rules: [
{ required: true, message: '请输入设备名称', trigger: 'blur' },
{ max: 100, message: '最多 100 个字符', trigger: 'blur' },
],
},
{
label: '出厂日期',
prop: 'factoryDate',
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
span: 12,
order: 8,
minWidth: 130,
},
{
label: '设备品牌',
prop: 'equipmentBrand',
span: 12,
order: 5,
minWidth: 130,
rules: [{ max: 50, message: '最多 50 个字符', trigger: 'blur' }],
},
{
label: '设备类型',
prop: 'equipmentType',
type: 'select',
dicData: [],
span: 12,
order: 6,
minWidth: 130,
rules: [{ max: 50, message: '最多 50 个字符', trigger: 'change' }],
},
{
label: '规格型号',
prop: 'specificationModel',
span: 12,
order: 7,
minWidth: 150,
rules: [{ max: 100, message: '最多 100 个字符', trigger: 'blur' }],
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
minRows: 3,
span: 24,
order: 10,
minWidth: 180,
overHidden: true,
rules: [{ max: 200, message: '最多 200 个字符', trigger: 'blur' }],
},
{
label: '更新时间',
prop: 'updateTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
addDisplay: false,
editDisplay: false,
viewDisplay: false,
minWidth: 170,
},
{
label: '创建时间',
prop: 'createTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
addDisplay: false,
editDisplay: false,
viewDisplay: false,
minWidth: 170,
},
{
label: '是否在线',
prop: 'onlineStatus',
type: 'select',
dicData: onlineStatusDic,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
minWidth: 100,
},
{
label: '原厂设备号',
prop: 'originalEquipmentNo',
span: 12,
order: 9,
hide: true,
rules: [{ max: 100, message: '最多 100 个字符', trigger: 'blur' }],
},
{
label: '附件',
prop: 'attachments',
slot: true,
formslot: true,
span: 24,
order: 11,
hide: true,
},
{
label: '组织',
prop: 'createDept',
type: 'tree',
dicData: [],
props: { label: 'title', value: 'id' },
checkStrictly: true,
search: true,
searchOrder: 1,
hide: true,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '更新人',
prop: 'updateUserName',
formatter: formatUpdateUserName,
display: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
],
};
export const excelOption = {
labelPosition: 'right',
submitBtn: false,
emptyBtn: false,
column: [
{
label: '模板上传',
prop: 'excelFile',
type: 'upload',
drag: true,
loadText: '模板上传中,请稍等',
span: 24,
propsHttp: { res: 'data' },
tip: '请上传 .xls,.xlsx 标准格式文件',
action: '/blade-transport/equipment-ledger/import-equipment-ledger',
},
{ label: '模板下载', prop: 'excelTemplate', formslot: true, span: 24 },
],
};

View File

@@ -38,10 +38,11 @@ export const option = {
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
menuFixed: 'right',
column: [
{
@@ -103,9 +104,8 @@ export const option = {
{
label: '交易金额',
prop: 'transactionAmount',
type: 'number',
precision: 2,
append: '元',
type: 'input',
formslot: true,
minWidth: 130,
span: 12,
placeholder: '请输入',

View File

@@ -46,10 +46,11 @@ export const option = {
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
column: [
{
label: '车船类型',
@@ -128,8 +129,8 @@ export const option = {
{
label: '保额',
prop: 'insuredAmount',
type: 'number',
precision: 2,
type: 'input',
formslot: true,
append: '元',
minWidth: 120,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
@@ -137,8 +138,8 @@ export const option = {
{
label: '保费',
prop: 'premium',
type: 'number',
precision: 2,
type: 'input',
formslot: true,
append: '元',
minWidth: 120,
rules: [
@@ -181,19 +182,6 @@ export const option = {
display: false,
minWidth: 170,
},
{
label: '创建时间',
prop: 'createTimeRange',
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
searchRange: true,
search: true,
hide: true,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '组织',
prop: 'createDept',
@@ -210,6 +198,19 @@ export const option = {
editDisplay: false,
viewDisplay: false,
},
{
label: '创建时间',
prop: 'createTimeRange',
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
searchRange: true,
search: true,
hide: true,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '更新人',
prop: 'updateUserName',

View File

@@ -34,10 +34,11 @@ export const option = {
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
menuFixed: 'right',
column: [
{
@@ -67,9 +68,8 @@ export const option = {
{
label: '上月统计里程数',
prop: 'previousMonthMileage',
type: 'number',
precision: 2,
append: '公里',
type: 'input',
formslot: true,
minWidth: 210,
slot: true,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
@@ -77,9 +77,8 @@ export const option = {
{
label: '本月统计里程数',
prop: 'currentMonthMileage',
type: 'number',
precision: 2,
append: '公里',
type: 'input',
formslot: true,
minWidth: 210,
slot: true,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
@@ -87,9 +86,8 @@ export const option = {
{
label: '本月行驶里程数',
prop: 'monthlyMileage',
type: 'number',
precision: 2,
append: '公里',
type: 'input',
formslot: true,
minWidth: 170,
slot: true,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
@@ -97,9 +95,8 @@ export const option = {
{
label: '累计行驶里程数',
prop: 'totalMileage',
type: 'number',
precision: 2,
append: '公里',
type: 'input',
formslot: true,
minWidth: 170,
slot: true,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],

View File

@@ -35,10 +35,11 @@ export const option = {
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
menuFixed: 'right',
column: [
{
@@ -111,25 +112,24 @@ export const option = {
{
label: '数量',
prop: 'quantity',
type: 'number',
precision: 2,
type: 'input',
formslot: true,
minWidth: 110,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
label: '单价',
prop: 'unitPrice',
type: 'number',
precision: 2,
type: 'input',
formslot: true,
minWidth: 120,
append: '元/升',
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
label: '交易金额',
prop: 'transactionAmount',
type: 'number',
precision: 2,
type: 'input',
formslot: true,
minWidth: 130,
rules: [
{ required: true, message: '请输入交易金额', trigger: 'blur' },
@@ -139,8 +139,8 @@ export const option = {
{
label: '余额',
prop: 'balance',
type: 'number',
precision: 2,
type: 'input',
formslot: true,
minWidth: 110,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},

View File

@@ -46,10 +46,11 @@ export const option = {
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
menuFixed: 'right',
column: [
{
@@ -102,9 +103,8 @@ export const option = {
{
label: '金额',
prop: 'amount',
type: 'number',
precision: 2,
append: '元',
type: 'input',
formslot: true,
minWidth: 120,
span: 12,
placeholder: '请输入',

View File

@@ -39,10 +39,11 @@ export const option = {
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
column: [
{
label: '车牌号',
@@ -88,8 +89,8 @@ export const option = {
{
label: '换胎数量',
prop: 'tireQuantity',
type: 'number',
precision: 0,
type: 'input',
formslot: true,
minWidth: 110,
span: 12,
placeholder: '请输入',
@@ -98,9 +99,8 @@ export const option = {
{
label: '换胎费用',
prop: 'replacementCost',
type: 'number',
precision: 2,
append: '元',
type: 'input',
formslot: true,
minWidth: 120,
span: 12,
placeholder: '请输入',

View File

@@ -26,10 +26,11 @@ export const option = {
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
menuFixed: 'right',
column: [
{

View File

@@ -25,14 +25,6 @@ export const vehicleTypeDic = [
{ label: '船舶', value: '船舶' },
];
export const violationTypeDic = [
{ label: '闯红灯', value: '闯红灯' },
{ label: '超速', value: '超速' },
{ label: '违停', value: '违停' },
{ label: '压线', value: '压线' },
{ label: '其他', value: '其他' },
];
export const processStatusDic = [
{ label: '未处理', value: '未处理' },
{ label: '已处理', value: '已处理' },
@@ -57,10 +49,11 @@ export const option = {
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
column: [
{
label: '车船类型',
@@ -104,11 +97,11 @@ export const option = {
label: '类型',
prop: 'violationType',
type: 'select',
dicData: violationTypeDic,
dicData: [],
minWidth: 120,
span: 12,
placeholder: '请选择',
rules: [{ required: true, message: '请选择类型', trigger: 'change' }],
rules: [],
},
{
label: '事项',
@@ -118,10 +111,7 @@ export const option = {
display: false,
span: 12,
placeholder: '请输入',
rules: [
{ required: true, message: '请输入事项', trigger: 'blur' },
{ max: 100, message: '最多 100 个字符', trigger: 'blur' },
],
rules: [{ max: 100, message: '最多 100 个字符', trigger: 'blur' }],
},
{
label: '日期',
@@ -149,8 +139,8 @@ export const option = {
{
label: '罚款',
prop: 'fineAmount',
type: 'number',
precision: 2,
type: 'input',
formslot: true,
append: '元',
minWidth: 100,
span: 12,
@@ -160,8 +150,8 @@ export const option = {
{
label: '被扣分数',
prop: 'deductPoints',
type: 'number',
precision: 0,
type: 'input',
formslot: true,
minWidth: 110,
span: 12,
placeholder: '请输入',

View File

@@ -364,6 +364,19 @@
margin-top: 12px;
}
// 统一 CRUD 顶部新增按钮为蓝底白字,保留其他工具按钮的描边样式
.avue-crud__header .el-button--primary.is-plain:has(.el-icon-plus) {
--el-button-bg-color: #409eff;
--el-button-border-color: #409eff;
--el-button-hover-bg-color: #66b1ff;
--el-button-hover-border-color: #66b1ff;
--el-button-active-bg-color: #337ecc;
--el-button-active-border-color: #337ecc;
--el-button-text-color: #fff;
--el-button-hover-text-color: #fff;
--el-button-active-text-color: #fff;
}
// 统一分页为按钮样式并靠右展示
.avue-crud__pagination,
.empty-pagination {
@@ -536,16 +549,42 @@
display: none !important;
}
// 全站统一:操作列最多一行展示 4 个按钮,第 5 个开始自动换行
.avue-crud .el-table td .cell.avue-crud__menu {
display: inline-grid;
grid-template-columns: repeat(4, max-content);
gap: 4px;
// 全站统一:Avue 操作列文字链接保持间距并支持自动换行
.avue-crud .avue-crud__menu {
display: inline-flex !important;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: start;
text-align: left;
}
// 全站统一:手写表格操作列中的文字链接保持间距并支持自动换行
.el-table .el-table__body .cell:not(.avue-crud__menu):has(> .el-link) {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
// 全站统一:表格操作列文字链接统一为主色(蓝色)
// 覆盖 Avue #menu 槽位与手写表格操作列中 el-link 的 warning / success / danger 等类型,
// 同时统一 hover / active / disabled 态,保证全站操作列风格一致。
// 选择器同时覆盖 Avue 行菜单(.avue-crud__menu与任意含 el-link 的表格体单元格(手写操作列)。
.avue-crud__menu .el-link,
.el-table .el-table__body .cell:has(.el-link) .el-link {
--el-link-text-color: var(--el-color-primary);
--el-link-hover-text-color: var(--el-color-primary-light-3);
--el-link-active-text-color: var(--el-color-primary-dark-2);
--el-link-disabled-text-color: var(--el-color-primary-light-5);
}
// 全站统一:去除操作列 el-link 默认 margin避免与 8px gap 叠加导致文字间距过宽
.avue-crud .avue-crud__menu .el-link,
.el-table .el-table__body .cell:not(.avue-crud__menu):has(> .el-link) .el-link {
margin: 0;
}
.avue-crud__menu > .el-button,
.avue-crud__menu > .el-dropdown {
margin: 0;

View File

@@ -47,7 +47,12 @@ const decorateFailExcel = async (blob, businessName, failDetailDecorator) => {
*/
export const openImportDialog = (vm, businessName, onSuccess, options = {}) => {
const { failDetailDecorator } = options;
const column = vm.findColumn(vm.excelOption.column, 'excelFile');
const excelOption = vm.excelOption || vm.excelOptionConfig;
const column = vm.findColumn(excelOption?.column || [], 'excelFile');
if (!column) {
vm.$message.error('导入配置异常,请稍后重试');
return;
}
column.httpRequest = (option, uploadColumn) =>
handleImportExcel(vm, option, uploadColumn, businessName, onSuccess, { failDetailDecorator });
vm.excelBox = true;

View File

@@ -250,7 +250,7 @@ export default {
viewBtn: true,
labelWidth: 'auto',
searchLabelWidth: 160,
menuWidth: 160,
menuWidth: 320,
dialogWidth: 600,
dialogClickModal: false,
column: [
@@ -466,7 +466,7 @@ export default {
});
},
rowSave(row, done, loading) {
add(row).then(
add(this.withTenantId(row)).then(
() => {
this.onLoad(this.page);
this.$message({
@@ -482,7 +482,7 @@ export default {
);
},
rowUpdate(row, index, done, loading) {
update(row).then(
update(this.withTenantId(row)).then(
() => {
this.onLoad(this.page);
this.$message({
@@ -497,6 +497,12 @@ export default {
}
);
},
withTenantId(row) {
return {
...row,
tenantId: row.tenantId || this.userInfo.tenantId || website.tenantId,
};
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',

View File

@@ -366,15 +366,6 @@ export default {
addDisplay: false,
editDisplay: false,
},
{
label: '行政区划编号',
prop: 'regionCode',
minWidth: 160,
overHidden: false,
addDisplay: false,
editDisplay: false,
rules: [{ max: 32, message: '行政区划编码不能超过32字', trigger: 'blur' }],
},
{
label: '详细地址',
prop: 'detailAddress',
@@ -409,6 +400,15 @@ export default {
{ validator: validateLatitude, trigger: 'blur' },
],
},
{
label: '行政区划编号',
prop: 'regionCode',
minWidth: 160,
overHidden: false,
addDisplay: false,
editDisplay: false,
rules: [{ max: 32, message: '行政区划编码不能超过32字', trigger: 'blur' }],
},
{
label: '数据来源',
prop: 'dataSource',
@@ -425,23 +425,6 @@ export default {
{ label: '手动录入', value: '手动录入' },
],
},
{
label: '启停状态',
prop: 'status',
type: 'select',
search: true,
slot: true,
dataType: 'number',
minWidth: 110,
overHidden: false,
addDisplay: false,
editDisplay: false,
dicData: [
{ label: '全部', value: '' },
{ label: '启用', value: 1 },
{ label: '停用', value: 2 },
],
},
{
label: '备注',
prop: 'remark',
@@ -483,6 +466,23 @@ export default {
display: false,
minWidth: 160,
},
{
label: '启停状态',
prop: 'status',
type: 'select',
search: true,
slot: true,
dataType: 'number',
minWidth: 110,
overHidden: false,
addDisplay: false,
editDisplay: false,
dicData: [
{ label: '全部', value: '' },
{ label: '启用', value: 1 },
{ label: '停用', value: 2 },
],
},
],
},
excelOption: {
@@ -869,17 +869,12 @@ export default {
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob(
'/blade-system/airport-master/export-airport-master',
this.buildExportParams(),
{ feedback: true }
)
exportBlob('/blade-system/airport-master/export-airport-master', this.buildExportParams(), {
feedback: true,
})
.then(async res => {
const blob = await addExcelRequiredHeaderMarks(res.data);
downloadXls(
blob,
`空港机场主数据${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`
);
downloadXls(blob, `空港机场主数据${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
})
.finally(() => {
NProgress.done();

View File

@@ -5,7 +5,7 @@
:table-loading="loading"
:data="data"
v-model:page="page"
v-model:form="form"
v-model="form"
ref="crud"
:permission="permissionList"
:before-open="beforeOpen"
@@ -22,7 +22,7 @@
>
<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-icon class="el-icon--left"><Plus /></el-icon>建货物类型
</el-button>
<el-button type="primary" plain v-if="permission.cargo_type_import" @click="handleImport"
><el-icon class="el-icon--left"><Upload /></el-icon>批量导入</el-button
@@ -67,7 +67,7 @@
:remote-method="loadParentOptions"
:loading="parentLoading"
:disabled="dialogType === 'view' || form.typeLevel !== 2"
placeholder="请输入名称或编码搜索"
placeholder="请输入"
style="width: 100%"
@change="handleParentChange"
>
@@ -348,6 +348,11 @@ export default {
const parent = this.parentOptions.find(item => String(item.id) === String(value));
this.form.parentCargoName = parent ? parent.cargoName : '';
this.form.parentCargoCode = parent ? parent.cargoCode || parent.code || '' : '';
if (parent) {
this.$nextTick(() => {
this.$refs.crud?.clearValidate?.(['parentId', 'parentCargoCode']);
});
}
if (!parent) {
this.form.cargoCode = '';
return;
@@ -540,7 +545,10 @@ export default {
});
},
buildExportParams() {
return { ...this.query };
return {
...this.query,
...(this.ids ? { ids: this.ids } : {}),
};
},
handleExport() {
this.$confirm('是否导出货物类型数据?', '提示', {
@@ -564,16 +572,9 @@ export default {
},
handleTemplate() {
NProgress.start();
// 直接下载工程内置的标准导入模板public 目录静态资源),表头与可正常导入的模板一致,
// 含一级19 类)及全部二级示例,便于用户参照填写层级关系与编码规则
const templateUrl = `${import.meta.env.BASE_URL}cargo-type-import-template.xlsx`;
fetch(templateUrl)
exportBlob('/blade-system/cargo-type/export-template', {}, { feedback: true })
.then(res => {
if (!res.ok) throw new Error('模板文件不存在');
return res.blob();
})
.then(blob => {
downloadXls(blob, '货物类型导入模板.xlsx');
downloadXls(res.data, '货物类型导入模板.xlsx');
})
.catch(() => {
this.$message.error('模板下载失败,请稍后重试');

View File

@@ -24,10 +24,9 @@
<el-button
type="primary"
icon="el-icon-plus"
plain
v-if="hasPermission('common_address_add')"
@click="$refs.crud.rowAdd()"
>
>建常用地址
</el-button>
<el-button
type="primary"
@@ -188,7 +187,6 @@
/>
<el-input
v-model="form.detailAddress"
readonly
maxlength="255"
placeholder="点击后弹出地图组件"
suffix-icon="el-icon-location"
@@ -389,10 +387,96 @@ export default {
column.rules = rules;
}
},
setColumnSpan(prop, span) {
const column = this.findColumn(this.option.column, prop);
if (column) {
column.span = span;
}
},
setColumnRow(prop, row) {
const column = this.findColumn(this.option.column, prop);
if (column) {
column.row = row;
}
},
setColumnOrder(prop, order) {
const column = this.findColumn(this.option.column, prop);
if (column) {
column.order = order;
}
},
syncSourceFormLayout(addressType) {
const isPortTerminal = addressType === '港口/码头';
const isRailwayStation = addressType === '铁路车站';
const isAirport = addressType === '空港机场';
const isRegularAddress = addressType === '常规地址';
this.setColumnSpan(
'addressName',
isRegularAddress || isPortTerminal || isRailwayStation || isAirport ? 12 : 24
);
this.setColumnRow('addressName', true);
this.setColumnSpan('stationId', isRailwayStation ? 12 : 24);
this.setColumnSpan('airportId', isAirport ? 12 : 24);
this.setColumnRow('stationId', isRailwayStation);
this.setColumnRow('airportId', isAirport);
const formOrders = {
addressType: 100,
addressName: 70,
contactName: 60,
contactPhone: 50,
detailAddress: 40,
remark: 30,
};
if (isPortTerminal) {
Object.assign(formOrders, {
addressType: 80,
portId: 70,
terminalId: 60,
addressName: 50,
contactName: 40,
contactPhone: 30,
detailAddress: 20,
remark: 10,
});
} else if (isRailwayStation) {
Object.assign(formOrders, {
addressType: 80,
stationId: 70,
addressName: 60,
contactName: 50,
contactPhone: 40,
detailAddress: 30,
remark: 20,
});
} else if (isAirport) {
Object.assign(formOrders, {
addressType: 80,
airportId: 70,
addressName: 60,
contactName: 50,
contactPhone: 40,
detailAddress: 30,
remark: 20,
});
}
[
'addressType',
'portId',
'terminalId',
'stationId',
'airportId',
'addressName',
'contactName',
'contactPhone',
'detailAddress',
'remark',
].forEach(prop => this.setColumnOrder(prop, formOrders[prop]));
},
syncSourceColumnDisplay(addressType) {
const isPortTerminal = addressType === '港口/码头';
const isRailwayStation = addressType === '铁路车站';
const isAirport = addressType === '空港机场';
this.syncSourceFormLayout(addressType);
this.setColumnDisplay('portId', isPortTerminal);
this.setColumnDisplay('terminalId', isPortTerminal);
this.setColumnDisplay('stationId', isRailwayStation);
@@ -1183,6 +1267,17 @@ export default {
</script>
<style lang="scss" scoped>
.common-address-page :deep(.avue-crud__dialog .avue-form__row),
.common-address-page :deep(.avue-crud__dialog .el-row) {
margin-right: -16px;
margin-left: -16px;
}
.common-address-page :deep(.avue-crud__dialog .el-col) {
padding-right: 16px;
padding-left: 16px;
}
.common-address-page {
&__map-address {
display: flex;

View File

@@ -229,7 +229,15 @@ import 'nprogress/nprogress.css';
const DEFAULT_COUNTRY_CODE = '+86';
const EXCLUDED_EXCEL_HEADERS = ['数据来源', '启停状态'];
const REQUIRED_EXPORT_HEADERS = ['港口码头名称', '港口/码头名称', '类型', '国家', '城市', '经度', '纬度'];
const REQUIRED_EXPORT_HEADERS = [
'港口码头名称',
'港口/码头名称',
'类型',
'国家',
'城市',
'经度',
'纬度',
];
const getExcelCellText = value => {
if (value === undefined || value === null) return '';
@@ -256,9 +264,7 @@ const removeWorkbookColumnsByHeaders = workbook => {
}
});
if (indexes.length) {
indexes
.sort((a, b) => b - a)
.forEach(colNumber => worksheet.spliceColumns(colNumber, 1));
indexes.sort((a, b) => b - a).forEach(colNumber => worksheet.spliceColumns(colNumber, 1));
break;
}
}
@@ -383,13 +389,42 @@ export default {
dialogClickModal: false,
menuWidth: 160,
column: [
{
label: '编码',
prop: 'code',
search: true,
formslot: true,
width: 140,
span: 12,
order: 70,
placeholder: '请输入编码',
rules: [
{ required: true, message: '请输入编码', trigger: 'blur' },
{ validator: validateCode, trigger: 'blur' },
],
change: ({ value }) => this.handleCodeChange(value),
blur: ({ value }) => this.handleCodeChange(value),
},
{
label: '港口/码头名称',
prop: 'name',
minWidth: 140,
search: true,
searchOrder: 7,
span: 12,
order: 80,
placeholder: '请输入',
rules: [{ required: true, message: '请输入港口/码头名称', trigger: 'blur' }],
},
{
label: '类型',
prop: 'category',
type: 'select',
search: true,
searchOrder: 6,
formslot: true,
span: 24,
order: 100,
dicData: [
{ label: '全部', value: '' },
{ label: '港口', value: '港口' },
@@ -409,6 +444,7 @@ export default {
dicData: [],
hide: true,
span: 12,
order: 90,
placeholder: '请选择',
rules: [{ required: false, message: '请选择上级港口', trigger: 'change' }],
change: ({ value }) => this.handleParentChange(value),
@@ -416,7 +452,7 @@ export default {
{
label: '上级港口',
prop: 'parentName',
minWidth: 120,
minWidth: 140,
slot: true,
addDisplay: false,
editDisplay: false,
@@ -432,39 +468,17 @@ export default {
placeholder: '自动带出',
rules: [{ required: false, message: '请选择上级港口', trigger: 'change' }],
},
{
label: '港口/码头名称',
prop: 'name',
minWidth: 140,
search: true,
span: 12,
placeholder: '请输入',
rules: [{ required: true, message: '请输入港口/码头名称', trigger: 'blur' }],
},
{
label: '编码',
prop: 'code',
search: true,
formslot: true,
width: 140,
span: 12,
placeholder: '请输入编码',
rules: [
{ required: true, message: '请输入编码', trigger: 'blur' },
{ validator: validateCode, trigger: 'blur' },
],
change: ({ value }) => this.handleCodeChange(value),
blur: ({ value }) => this.handleCodeChange(value),
},
{
label: '国家',
prop: 'country',
type: 'select',
search: true,
searchOrder: 5,
formslot: true,
filterable: true,
dicData: [],
span: 12,
order: 60,
minWidth: 140,
overHidden: false,
placeholder: '自动带出',
@@ -478,6 +492,7 @@ export default {
formslot: true,
hide: true,
span: 12,
order: 50,
placeholder: '自动带出',
rules: [{ required: true, message: '请选择所属城市', trigger: 'change' }],
},
@@ -486,6 +501,7 @@ export default {
prop: 'city',
type: 'select',
search: true,
searchOrder: 4,
filterable: true,
dicData: [],
addDisplay: false,
@@ -513,14 +529,44 @@ export default {
prop: 'districtName',
type: 'select',
search: true,
searchOrder: 3,
filterable: true,
dicData: [],
minWidth: 100,
hide: true,
addDisplay: false,
editDisplay: false,
},
{
label: '行政区划编号',
label: '经度',
prop: 'longitude',
formslot: true,
minWidth: 130,
span: 12,
order: 20,
placeholder: '请输入',
overHidden: false,
rules: [
{ required: true, message: '请输入经度', trigger: 'blur' },
{ validator: validateLongitude, trigger: 'blur' },
],
},
{
label: '纬度',
prop: 'latitude',
formslot: true,
minWidth: 130,
span: 12,
order: 10,
placeholder: '请输入',
overHidden: false,
rules: [
{ required: true, message: '请输入纬度', trigger: 'blur' },
{ validator: validateLatitude, trigger: 'blur' },
],
},
{
label: '行政区划编码',
prop: 'regionCode',
minWidth: 140,
span: 12,
@@ -538,42 +584,18 @@ export default {
span: 24,
minWidth: 220,
overHidden: false,
order: 30,
placeholder: '请输入',
maxlength: 255,
showWordLimit: true,
rules: [{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' }],
},
{
label: '经度',
prop: 'longitude',
formslot: true,
minWidth: 130,
span: 12,
placeholder: '请输入',
overHidden: false,
rules: [
{ required: true, message: '请输入经度', trigger: 'blur' },
{ validator: validateLongitude, trigger: 'blur' },
],
},
{
label: '纬度',
prop: 'latitude',
formslot: true,
minWidth: 130,
span: 12,
placeholder: '请输入',
overHidden: false,
rules: [
{ required: true, message: '请输入纬度', trigger: 'blur' },
{ validator: validateLatitude, trigger: 'blur' },
],
},
{
label: '数据来源',
prop: 'dataSource',
type: 'select',
search: true,
searchOrder: 2,
minWidth: 120,
overHidden: false,
addDisplay: false,
@@ -585,23 +607,6 @@ export default {
{ label: '手工导入', value: '手工导入' },
],
},
{
label: '启停状态',
prop: 'status',
type: 'select',
search: true,
slot: true,
dataType: 'number',
minWidth: 110,
overHidden: false,
addDisplay: false,
editDisplay: false,
dicData: [
{ label: '全部', value: '' },
{ label: '启用', value: 1 },
{ label: '停用', value: 2 },
],
},
{
label: '备注',
prop: 'remark',
@@ -639,9 +644,26 @@ export default {
valueFormat: 'YYYY-MM-DD HH:mm:ss',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 160,
},
{
label: '状态',
prop: 'status',
type: 'select',
search: true,
searchOrder: 1,
slot: true,
dataType: 'number',
minWidth: 110,
overHidden: false,
addDisplay: false,
editDisplay: false,
dicData: [
{ label: '全部', value: '' },
{ label: '启用', value: 1 },
{ label: '停用', value: 2 },
],
},
],
},
excelOption: {
@@ -1270,19 +1292,14 @@ export default {
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob(
'/blade-system/port-terminal/export-port-terminal',
this.buildExportParams(),
{ feedback: true }
)
exportBlob('/blade-system/port-terminal/export-port-terminal', this.buildExportParams(), {
feedback: true,
})
.then(async res => {
const blob = await removeExcelColumnsByHeaders(res.data, {
markRequiredHeaders: true,
});
downloadXls(
blob,
`港口码头主数据${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`
);
downloadXls(blob, `港口码头主数据${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
})
.finally(() => {
NProgress.done();

View File

@@ -335,15 +335,6 @@ export default {
addDisplay: false,
editDisplay: false,
},
{
label: '行政区划编号',
prop: 'regionCode',
minWidth: 160,
overHidden: false,
addDisplay: false,
editDisplay: false,
rules: [{ max: 32, message: '行政区划编码不能超过32字', trigger: 'blur' }],
},
{
label: '详细地址',
prop: 'detailAddress',
@@ -373,20 +364,13 @@ export default {
rules: [{ validator: validateLatitude, trigger: 'blur' }],
},
{
label: '数据来源',
prop: 'dataSource',
type: 'select',
search: true,
minWidth: 120,
label: '行政区划编号',
prop: 'regionCode',
minWidth: 160,
overHidden: false,
addDisplay: false,
editDisplay: false,
dicData: [
{ label: '全部', value: '' },
{ label: '初始化导入', value: '初始化导入' },
{ label: '批量', value: '批量' },
{ label: '手动', value: '手动' },
],
rules: [{ max: 32, message: '行政区划编码不能超过32字', trigger: 'blur' }],
},
{
label: '启停状态',
@@ -446,6 +430,22 @@ export default {
display: false,
minWidth: 160,
},
{
label: '数据来源',
prop: 'dataSource',
type: 'select',
search: true,
minWidth: 120,
overHidden: false,
addDisplay: false,
editDisplay: false,
dicData: [
{ label: '全部', value: '' },
{ label: '初始化导入', value: '初始化导入' },
{ label: '批量', value: '批量' },
{ label: '手动', value: '手动' },
],
},
],
},
excelOption: {

View File

@@ -11,6 +11,37 @@
</el-col>
<el-col class="region-page__form-panel" :span="18">
<basic-container>
<el-form class="region-page__search" :model="searchForm" inline>
<el-form-item label="区域编码">
<el-input v-model="searchForm.code" clearable placeholder="请输入区域编码" />
</el-form-item>
<el-form-item label="区域名称">
<el-input v-model="searchForm.name" clearable placeholder="请输入区域名称" />
</el-form-item>
<el-form-item label="区域层级">
<el-select v-model="searchForm.regionLevel" clearable placeholder="请选择区域层级">
<el-option label="省份" :value="1" />
<el-option label="省市" :value="2" />
<el-option label="区县" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="数据来源">
<el-select v-model="searchForm.dataSource" clearable placeholder="请选择数据来源">
<el-option label="手动录入" value="手动录入" />
<el-option label="初始化导入" value="初始化导入" />
</el-select>
</el-form-item>
<el-form-item label="启停状态">
<el-select v-model="searchForm.status" clearable placeholder="请选择启停状态">
<el-option label="启用" :value="1" />
<el-option label="停用" :value="2" />
</el-select>
</el-form-item>
<el-form-item class="region-page__search-actions">
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleSearchReset">清空</el-button>
</el-form-item>
</el-form>
<el-button-group>
<el-button
v-if="permission.region_add"
@@ -136,14 +167,21 @@ export default {
treeCode: '',
treeParentCode: '',
treeData: [],
searchForm: {
code: '',
name: '',
regionLevel: undefined,
dataSource: '',
status: undefined,
},
parentOptions: [],
syncLoading: false,
treeOption: {
nodeKey: 'id',
lazy: true,
treeLoad: function (node, resolve) {
treeLoad: (node, resolve) => {
const parentCode = node.level === 0 ? ROOT_PARENT_CODE : node.data.id;
getLazyTree(parentCode).then(res => {
getLazyTree(parentCode, this.searchForm).then(res => {
resolve(
res.data.data.map(item => {
return {
@@ -402,7 +440,7 @@ export default {
methods: {
initTree() {
this.treeData = [];
getLazyTree(this.topCode).then(res => {
getLazyTree(this.topCode, this.searchForm).then(res => {
this.treeData = res.data.data.map(item => {
return {
...item,
@@ -411,6 +449,20 @@ export default {
});
});
},
handleSearch() {
this.initTree();
this.regionForm = {};
},
handleSearchReset() {
this.searchForm = {
code: '',
name: '',
regionLevel: undefined,
dataSource: '',
status: undefined,
};
this.handleSearch();
},
nodeClick(data) {
this.setRegionColumnDisplay('code', true);
this.treeCode = data.id;
@@ -624,20 +676,18 @@ export default {
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob(
`/blade-system/region/export-region?${this.website.tokenHeader}=${getToken()}`,
{ feedback: true }
).then(res => {
exportBlob(`/blade-system/region/export-region?${this.website.tokenHeader}=${getToken()}`, {
feedback: true,
}).then(res => {
downloadXls(res.data, `行政区划数据${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
NProgress.done();
});
});
},
handleTemplate() {
exportBlob(
`/blade-system/region/export-template?${this.website.tokenHeader}=${getToken()}`,
{ feedback: true }
).then(res => {
exportBlob(`/blade-system/region/export-template?${this.website.tokenHeader}=${getToken()}`, {
feedback: true,
}).then(res => {
downloadXls(res.data, '行政区划模板.xlsx');
});
},

View File

@@ -24,10 +24,9 @@
<el-button
type="primary"
icon="el-icon-plus"
plain
v-if="hasPermission('common_cargo_add')"
@click="$refs.crud.rowAdd()"
>
>建常用货物
</el-button>
<el-button
type="primary"
@@ -113,7 +112,7 @@
clearable
maxlength="20"
show-word-limit
placeholder="请输入货物编号"
placeholder="请输入"
:disabled="dialogReadonly || dialogType === 'edit'"
@input="handleCargoCodeInput"
/>
@@ -144,6 +143,10 @@
</div>
</template>
<template #packageBrand="{ row }">
{{ [row.packageType, row.brand].filter(Boolean).join(' / ') || '-' }}
</template>
<template #menu="{ row, index }">
<el-link
type="primary"
@@ -192,6 +195,7 @@
import * as api from '@/api/business/common-cargo';
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
import { exportBlob } from '@/api/common';
import { getDeptTree } from '@/api/system/dept';
import { config, excelOption, option } from '@/option/business/common-cargo';
import { priceUnitOptions } from '@/option/business/common';
import { getToken } from '@/utils/auth';
@@ -221,6 +225,7 @@ export default {
secondCargoTypeLoading: false,
firstCargoTypeOptions: [],
secondCargoTypeOptions: [],
deptOptions: [],
page: {
pageSize: 10,
pageSizes: [10, 20, 50, 100],
@@ -247,6 +252,9 @@ export default {
return this.selectionList.map(item => item.id).join(',');
},
},
created() {
this.initDeptTree();
},
methods: {
hasPermission(code) {
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
@@ -256,6 +264,28 @@ export default {
if (Array.isArray(data)) return data;
return (data && data.records) || [];
},
initDeptTree() {
getDeptTree(this.userInfo.tenantId).then(res => {
this.deptOptions = this.flattenDept(res.data.data || []);
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
if (deptColumn) {
deptColumn.dicData = this.deptOptions;
}
});
},
flattenDept(tree, level = 0) {
const result = [];
tree.forEach(item => {
result.push({
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
value: item.id,
});
if (item.children && item.children.length) {
result.push(...this.flattenDept(item.children, level + 1));
}
});
return result;
},
normalizeCargoTypeOptions(list = []) {
return list
.map(item => ({
@@ -319,11 +349,12 @@ export default {
const second = this.secondCargoTypeOptions.find(item => item.cargoCode === value);
this.form.secondCargoTypeName = second ? second.cargoName : '';
this.form.secondCargoTypeCode = second ? second.cargoCode : '';
this.form.cargoCode = second ? String(second.cargoCode).slice(0, 4) : '';
this.syncCargoCodePrefix();
},
getCargoCodePrefix() {
return this.form.secondCargoTypeCode && this.form.firstCargoTypeCode
? this.form.firstCargoTypeCode
return this.form.secondCargoTypeCode
? String(this.form.secondCargoTypeCode).slice(0, 4)
: '';
},
syncCargoCodePrefix() {
@@ -383,13 +414,13 @@ export default {
return false;
}
const cargoCode = String(row.cargoCode || '');
const prefix = row.firstCargoTypeCode;
const prefix = String(row.secondCargoTypeCode || '').slice(0, 4);
if (!cargoCode || cargoCode === prefix) {
this.$message.warning('请输入货物编号');
return false;
}
if (!cargoCode.startsWith(prefix)) {
this.$message.warning('货物编号需拼接级货物类型编号在前面');
this.$message.warning('货物编号需拼接级货物类型编号在前面');
return false;
}
if (cargoCode.length > 20) {

View File

@@ -24,10 +24,25 @@
<el-button
type="primary"
icon="el-icon-plus"
plain
v-if="hasPermission('common_route_add')"
@click="$refs.crud.rowAdd()"
>
>建路线
</el-button>
<el-button
type="primary"
icon="el-icon-upload"
plain
v-if="config.importUrl && hasPermission('common_route_import')"
@click="handleImport"
>批量导入
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="config.templateUrl && hasPermission('common_route_template')"
@click="handleTemplate"
>下载模板
</el-button>
<el-button
type="primary"
@@ -57,10 +72,10 @@
/>
<el-input
v-model="form.departureAddress"
readonly
placeholder="请输入并选择地址"
suffix-icon="el-icon-location"
:disabled="dialogReadonly"
@click="openRouteMapDialog('departure')"
/>
<el-tooltip content="选择常用地址" placement="top">
<el-button
@@ -84,10 +99,10 @@
/>
<el-input
v-model="form.arrivalAddress"
readonly
placeholder="请输入并选择地址"
suffix-icon="el-icon-location"
:disabled="dialogReadonly"
@click="openRouteMapDialog('arrival')"
/>
<el-tooltip content="选择常用地址" placement="top">
<el-button
@@ -133,6 +148,16 @@
@load="onLoad(page, query)"
/>
<el-dialog :title="`${config.title}导入`" append-to-body v-model="excelBox" width="555px">
<avue-form :option="excelOptionConfig" v-model="excelForm">
<template #excelTemplate>
<el-button type="primary" @click="handleTemplate">
点击下载<i class="el-icon-download el-icon--right"></i>
</el-button>
</template>
</avue-form>
</el-dialog>
<el-dialog
title="选择常用地址"
append-to-body
@@ -242,6 +267,48 @@
<el-button @click="addressDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog
title="地图选择地址"
append-to-body
v-model="routeMapVisible"
width="920px"
@opened="initRouteAmap"
>
<div class="common-route-page__map-toolbar">
<el-input
v-model="routeMapKeyword"
clearable
placeholder="输入地址关键词"
@keyup.enter="searchRouteMapKeyword"
/>
<el-button
type="primary"
:icon="Search"
:loading="routeMapLoading"
@click="searchRouteMapKeyword"
>
搜索
</el-button>
</div>
<div ref="routeAmap" class="common-route-page__map"></div>
<div class="common-route-page__map-info">
<span>{{ routeMapStatus }}</span>
<span v-if="routeMapSelected.regionName"
>发货/收货地{{ routeMapSelected.regionName }}</span
>
</div>
<template #footer>
<el-button @click="routeMapVisible = false">取消</el-button>
<el-button
type="primary"
:disabled="!routeMapSelected.longitude"
@click="confirmRouteMapPick"
>
确定
</el-button>
</template>
</el-dialog>
</basic-container>
</template>
@@ -251,18 +318,24 @@ import { getList as getCommonAddressList } from '@/api/base/common-address';
import { exportBlob } from '@/api/common';
import { getDeptTree } from '@/api/system/dept';
import { addressTypeOptions } from '@/option/base/common-address';
import { config, option } from '@/option/business/common-route';
import { config, excelOption, option } from '@/option/business/common-route';
import { getToken } from '@/utils/auth';
import { openImportDialog } from '@/utils/import-excel';
import { downloadXls } from '@/utils/util';
import { List } from '@element-plus/icons-vue';
import { List, Search } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
let amapLoader;
export default {
data() {
return {
List,
Search,
config,
option,
form: {},
@@ -270,12 +343,24 @@ export default {
loading: true,
data: [],
dialogReadonly: false,
excelBox: false,
excelForm: {},
excelOptionConfig: excelOption,
addressTypeOptions,
addressDialogVisible: false,
addressTarget: '',
addressQuery: {},
addressData: [],
addressLoading: false,
routeMapVisible: false,
routeMapTarget: '',
routeMapLoading: false,
routeMapKeyword: '',
routeMapStatus: '可搜索地址或点击地图选点',
routeMapSelected: {},
routeAmap: null,
routeAmapMarker: null,
routeAmapGeocoder: null,
deptOptions: [],
page: {
pageSize: 10,
@@ -317,9 +402,22 @@ export default {
},
initDeptTree() {
getDeptTree(this.userInfo.tenantId).then(res => {
this.deptOptions = this.flattenDept(res.data.data || []);
const deptTree = res.data.data || [];
this.deptOptions = this.flattenDept(deptTree);
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
if (deptColumn) {
deptColumn.dicData = this.buildDeptTree(deptTree);
}
});
},
buildDeptTree(tree) {
return tree.map(item => ({
label: item.title || item.deptName || item.name,
value: item.id,
children:
item.children && item.children.length ? this.buildDeptTree(item.children) : undefined,
}));
},
flattenDept(tree, level = 0) {
const result = [];
tree.forEach(item => {
@@ -404,6 +502,246 @@ export default {
this.form[`${prefix}Contact`] = address.contactName || this.form[`${prefix}Contact`] || '';
this.form[`${prefix}Phone`] = address.contactPhone || this.form[`${prefix}Phone`] || '';
},
openRouteMapDialog(target) {
if (this.dialogReadonly) return;
const prefix = target === 'departure' ? 'departure' : 'arrival';
this.routeMapTarget = prefix;
this.routeMapKeyword = this.form[`${prefix}Address`] || this.form[`${prefix}Name`] || '';
this.routeMapSelected = this.buildRouteMapSelection({
lng: this.form[`${prefix}Longitude`],
lat: this.form[`${prefix}Latitude`],
address: this.form[`${prefix}Address`],
regionName: this.form[`${prefix}Name`],
});
this.routeMapStatus = this.routeMapSelected.longitude
? '已加载当前选点,可重新选点'
: '可搜索地址或点击地图选点';
this.routeMapVisible = true;
},
loadAmap() {
if (window.AMap && window.AMap.Map) {
return Promise.resolve();
}
if (!amapLoader) {
amapLoader = new Promise((resolve, reject) => {
window._AMapSecurityConfig = {
securityJsCode: AMAP_SECURITY_CODE,
};
const script = document.createElement('script');
script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}`;
script.async = true;
script.onload = resolve;
script.onerror = () => reject(new Error('高德地图组件加载失败'));
document.body.appendChild(script);
});
}
return amapLoader;
},
initRouteAmap() {
this.loadAmap()
.then(() => {
this.$nextTick(() => {
if (!this.routeAmap) {
const center = this.toLngLat(
this.routeMapSelected.longitude || 116.40769,
this.routeMapSelected.latitude || 39.89945
);
this.routeAmap = new window.AMap.Map(this.$refs.routeAmap, {
center,
zoom: this.routeMapSelected.longitude ? 14 : 11,
});
window.AMap.plugin(['AMap.ToolBar', 'AMap.Geocoder'], () => {
this.routeAmap.addControl(new window.AMap.ToolBar());
if (!this.routeAmapGeocoder) {
this.routeAmapGeocoder = new window.AMap.Geocoder();
}
});
this.routeAmap.on('click', event => this.pickRouteMapPoint(event.lnglat));
} else if (typeof this.routeAmap.resize === 'function') {
this.routeAmap.resize();
}
if (this.routeMapSelected.longitude) {
this.renderRouteMapMarker(
this.toLngLat(this.routeMapSelected.longitude, this.routeMapSelected.latitude)
);
}
});
})
.catch(() => {
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
this.routeMapVisible = false;
});
},
searchRouteMapKeyword() {
const keyword = String(this.routeMapKeyword || '').trim();
if (!keyword) {
this.$message.warning('请输入地址关键词');
return;
}
this.loadAmap()
.then(() => {
this.routeMapLoading = true;
return this.ensureRouteAmapGeocoder();
})
.then(() => this.runAmapGeocode('location', keyword))
.then(result => {
const point = this.resolveMapPoint(result);
if (!point) {
this.routeMapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
return;
}
this.pickRouteMapPoint(point, keyword);
})
.catch(() => {
this.routeMapStatus = '地图搜索失败';
this.$message.error('地图搜索失败,请稍后重试');
})
.finally(() => {
this.routeMapLoading = false;
});
},
pickRouteMapPoint(lnglat, keyword) {
const longitude = this.getPointLng(lnglat);
const latitude = this.getPointLat(lnglat);
if (longitude === undefined || latitude === undefined) {
this.$message.warning('选点坐标无效');
return;
}
const point = this.toLngLat(longitude, latitude);
this.renderRouteMapMarker(point);
this.routeMapSelected = this.buildRouteMapSelection({
lng: longitude,
lat: latitude,
address: keyword,
});
this.routeMapStatus = '正在反查地址...';
this.ensureRouteAmapGeocoder()
.then(() => this.runAmapGeocode('address', point))
.then(result => {
const address = this.resolveMapAddress(result);
this.routeMapSelected = {
...this.routeMapSelected,
detailAddress: address.detailAddress || this.routeMapSelected.detailAddress,
regionName: address.regionName || this.routeMapSelected.regionName,
};
this.routeMapKeyword = this.routeMapSelected.detailAddress || this.routeMapKeyword;
this.routeMapStatus = this.routeMapSelected.detailAddress || '已选点,可确认回填';
})
.catch(() => {
this.routeMapStatus = '反查地址失败';
this.$message.error('反查地址失败,请重新选点');
});
},
renderRouteMapMarker(point) {
if (!this.routeAmap || !window.AMap) return;
if (this.routeAmapMarker) {
this.routeAmapMarker.setMap(null);
}
this.routeAmapMarker = new window.AMap.Marker({ position: point });
this.routeAmapMarker.setMap(this.routeAmap);
this.routeAmap.setCenter(point);
},
confirmRouteMapPick() {
if (!this.routeMapSelected.longitude) {
this.$message.warning('请先搜索或点击地图完成选点');
return;
}
const prefix = this.routeMapTarget;
this.form[`${prefix}Address`] =
this.routeMapSelected.detailAddress || this.form[`${prefix}Address`];
this.form[`${prefix}Name`] = this.routeMapSelected.regionName || this.form[`${prefix}Name`];
this.form[`${prefix}Longitude`] = this.routeMapSelected.longitude;
this.form[`${prefix}Latitude`] = this.routeMapSelected.latitude;
this.routeMapVisible = false;
this.$refs.crud?.validateField?.(`${prefix}Address`);
},
buildRouteMapSelection({ lng, lat, address, regionName }) {
return {
longitude: this.formatCoordinate(lng),
latitude: this.formatCoordinate(lat),
detailAddress: address || '',
regionName: regionName || '',
};
},
formatCoordinate(value) {
if (value === undefined || value === null || value === '') return '';
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue.toFixed(6) : '';
},
toLngLat(lng, lat) {
return new window.AMap.LngLat(Number(lng), Number(lat));
},
getPointLng(point) {
if (!point) return undefined;
if (typeof point.getLng === 'function') return point.getLng();
return point.lng ?? point.lon;
},
getPointLat(point) {
if (!point) return undefined;
if (typeof point.getLat === 'function') return point.getLat();
return point.lat;
},
ensureRouteAmapGeocoder() {
if (this.routeAmapGeocoder) return Promise.resolve(this.routeAmapGeocoder);
return new Promise((resolve, reject) => {
if (!window.AMap || !window.AMap.plugin) {
reject(new Error('高德地图组件未就绪'));
return;
}
window.AMap.plugin(['AMap.Geocoder'], () => {
try {
this.routeAmapGeocoder = new window.AMap.Geocoder();
resolve(this.routeAmapGeocoder);
} catch (error) {
reject(error);
}
});
});
},
runAmapGeocode(action, input) {
return new Promise((resolve, reject) => {
const timer = window.setTimeout(() => {
reject(new Error('高德地图请求超时'));
}, 10000);
const done = (status, result) => {
window.clearTimeout(timer);
if (status === 'complete' && result) {
resolve(result);
return;
}
reject(new Error('高德地图请求失败'));
};
try {
if (action === 'location') {
this.routeAmapGeocoder.getLocation(input, done);
} else {
this.routeAmapGeocoder.getAddress(input, done);
}
} catch (error) {
window.clearTimeout(timer);
reject(error);
}
});
},
resolveMapPoint(result) {
if (!result) return null;
const geocode = Array.isArray(result.geocodes) ? result.geocodes[0] : null;
if (geocode && geocode.location) return geocode.location;
if (result.location) return result.location;
if (result.lnglat) return result.lnglat;
if (result.getLng || result.lng || result.lon) return result;
return null;
},
resolveMapAddress(result = {}) {
const regeocode = result.regeocode || {};
const component = regeocode.addressComponent || {};
const city = Array.isArray(component.city) ? '' : component.city;
return {
detailAddress: regeocode.formattedAddress || '',
regionName: [component.province, city, component.district].filter(Boolean).join(''),
};
},
normalizeRow(row) {
const submitRow = { ...row };
Object.keys(submitRow).forEach(key => {
@@ -613,6 +951,27 @@ export default {
});
});
},
handleTemplate() {
NProgress.start();
exportBlob(
config.templateUrl,
{
[this.website.tokenHeader]: getToken(),
},
{ feedback: true }
)
.then(res => {
downloadXls(res.data, `${config.title}模板.xlsx`);
})
.finally(() => {
NProgress.done();
});
},
handleImport() {
const column = this.excelOptionConfig.column.find(item => item.prop === 'excelFile');
column.action = config.importUrl;
openImportDialog(this, config.title, () => this.onLoad(this.page, this.query));
},
},
};
</script>
@@ -654,5 +1013,31 @@ export default {
justify-content: flex-end;
margin-top: 14px;
}
&__map-toolbar {
display: flex;
gap: 8px;
margin-bottom: 12px;
.el-input {
flex: 1;
min-width: 0;
}
}
&__map {
width: 100%;
height: 420px;
border: 1px solid #eff1f7;
}
&__map-info {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 10px;
color: #606266;
line-height: 1.6;
}
}
</style>

View File

@@ -18,13 +18,13 @@
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@search-icon-change="handleSearchIconChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button
type="primary"
:icon="actionButtonLikeSearch ? undefined : 'el-icon-plus'"
:plain="!actionButtonLikeSearch"
v-if="canCreate"
@click="$refs.crud.rowAdd()"
>{{ config.createText || '新增' }}
@@ -114,11 +114,35 @@
<template #contractCategory-label>合同类型</template>
<template v-if="config.enableContractExpiryTags" #search-menu>
<div v-show="searchExpanded" class="business-crud-page__contract-expiry-search">
<span class="business-crud-page__contract-expiry-label">签约类型:</span>
<el-select v-model="contractSignType" clearable placeholder="请选择">
<el-option
v-for="item in contractSignTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<div class="business-crud-page__contract-expiry-tags">
<el-check-tag
v-for="item in contractExpiryTagOptions"
:key="item.value"
:checked="contractExpiryScope === item.value"
@change="handleContractExpiryScopeChange(item.value)"
>
{{ item.label }}{{ contractExpiryStats[item.statKey] || 0 }}
</el-check-tag>
</div>
</div>
</template>
<template #projectName-form>
<el-select
v-model="selectedProjectId"
class="business-crud-page__field"
placeholder="请选择项目名称"
:placeholder="config.projectPlaceholder || '请选择项目名称'"
filterable
clearable
:loading="projectLoading"
@@ -1221,7 +1245,7 @@
<el-cascader
v-model="selectedOrganizationId"
class="business-crud-page__field"
placeholder="请选择所属组织"
:placeholder="config.organizationPlaceholder || '请选择所属组织'"
:options="organizationOptions"
:props="organizationCascaderProps"
clearable
@@ -1402,12 +1426,17 @@
</el-radio-group>
</div>
<el-form
v-if="settlementRuleEnabled"
:model="settlementRuleForm"
label-position="right"
label-width="auto"
class="business-crud-page__module-form business-crud-page__module-form--three"
>
<el-form-item label="账单起始日期" required>
<el-form-item
label="账单起始日期"
required
:error="settlementRuleErrors.billStartDate"
>
<el-date-picker
v-model="settlementRuleForm.billStartDate"
type="date"
@@ -1415,9 +1444,10 @@
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled="dialogReadonly"
@change="clearSettlementRuleError('billStartDate')"
/>
</el-form-item>
<el-form-item label="结算类型" required>
<el-form-item label="结算类型" required :error="settlementRuleErrors.settlementType">
<el-select
v-model="settlementRuleForm.settlementType"
placeholder="请选择结算类型"
@@ -1432,7 +1462,12 @@
/>
</el-select>
</el-form-item>
<el-form-item v-if="showSettlementBillCycleType" label="账单周期类型" required>
<el-form-item
v-if="showSettlementBillCycleType"
label="账单周期类型"
required
:error="settlementRuleErrors.billCycleType"
>
<el-select
v-model="settlementRuleForm.billCycleType"
placeholder="请选择账单周期类型"
@@ -1447,11 +1482,17 @@
/>
</el-select>
</el-form-item>
<el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日">
<el-form-item
v-if="showSettlementBillCutoffDay"
label="账单截单日"
required
:error="settlementRuleErrors.billCutoffDay"
>
<el-select
v-model="settlementRuleForm.billCutoffDay"
placeholder="请选择账单截单日"
:disabled="dialogReadonly"
@change="clearSettlementRuleError('billCutoffDay')"
>
<el-option
v-for="item in billCutoffDayOptions"
@@ -1461,11 +1502,17 @@
/>
</el-select>
</el-form-item>
<el-form-item v-if="showSettlementCycleDays" label="周期天数" required>
<el-form-item
v-if="showSettlementCycleDays"
label="周期天数"
required
:error="settlementRuleErrors.cycleDays"
>
<el-select
v-model="settlementRuleForm.cycleDays"
placeholder="请选择周期天数"
:disabled="dialogReadonly"
@change="clearSettlementRuleError('cycleDays')"
>
<el-option
v-for="item in cycleDayOptions"
@@ -2055,7 +2102,6 @@
<span class="business-crud-page__dispatch-heading-name">
{{ dispatchDialogTitle }}
</span>
<span class="business-crud-page__dispatch-heading-split">|</span>
<span class="business-crud-page__dispatch-heading-no">
{{ dispatchDialogNo || '-' }}
</span>
@@ -2067,8 +2113,9 @@
</el-tag>
</div>
</div>
<div class="business-crud-page__dispatch-summary">
<div class="business-crud-page__dispatch-summary-left">
<section-card title="基本信息" class="business-crud-page__dispatch-summary-card">
<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>
@@ -2088,22 +2135,22 @@
{{ dispatchRow.contractName || dispatchRow.contractNo || '-' }}
</div>
</div>
<div class="business-crud-page__dispatch-summary-item">
<div class="business-crud-page__dispatch-summary-label">货物信息</div>
<div class="business-crud-page__dispatch-summary-value">
{{ dispatchPlanCargoInfo || '-' }}
</div>
</div>
<div class="business-crud-page__dispatch-summary-item">
<div class="business-crud-page__dispatch-summary-label">计划执行时间</div>
<div class="business-crud-page__dispatch-summary-value">
{{
[dispatchRow.planStartDate, dispatchRow.planEndDate]
.filter(Boolean)
.join('-') || '-'
.join(' ~ ') || '-'
}}
</div>
</div>
<div class="business-crud-page__dispatch-summary-item is-wide">
<div class="business-crud-page__dispatch-summary-label">货物信息</div>
<div class="business-crud-page__dispatch-summary-value">
{{ dispatchPlanCargoInfo || '-' }}
</div>
</div>
<div
class="business-crud-page__dispatch-summary-item business-crud-page__dispatch-summary-item--attachments"
>
@@ -2124,44 +2171,48 @@
</div>
</div>
</div>
</div>
<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>
<div class="business-crud-page__dispatch-route-body">
<div class="business-crud-page__dispatch-route-title">
{{ parseDispatchRoutePoint(dispatchRow, 'start').name || '-' }}
</section-card>
<section-card title="收发货路线" class="business-crud-page__dispatch-route-card">
<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>
<div class="business-crud-page__dispatch-route-body">
<div class="business-crud-page__dispatch-route-title">
{{ parseDispatchRoutePoint(dispatchRow, 'start').name || '-' }}
</div>
<div class="business-crud-page__dispatch-route-address">
{{ parseDispatchRoutePoint(dispatchRow, 'start').address || '-' }}
</div>
<div class="business-crud-page__dispatch-route-contact">
{{ parseDispatchRoutePoint(dispatchRow, 'start').contact || '-' }}
</div>
</div>
<div class="business-crud-page__dispatch-route-address">
{{ parseDispatchRoutePoint(dispatchRow, 'start').address || '-' }}
</div>
<div class="business-crud-page__dispatch-route-contact">
{{ parseDispatchRoutePoint(dispatchRow, 'start').contact || '-' }}
</div>
<div class="business-crud-page__dispatch-route-vehicle">
<el-icon><Van /></el-icon>
</div>
<div class="business-crud-page__dispatch-route-item">
<span class="business-crud-page__dispatch-route-badge is-end">终</span>
<div class="business-crud-page__dispatch-route-body">
<div class="business-crud-page__dispatch-route-title">
{{ parseDispatchRoutePoint(dispatchRow, 'end').name || '-' }}
</div>
<div class="business-crud-page__dispatch-route-address">
{{ parseDispatchRoutePoint(dispatchRow, 'end').address || '-' }}
</div>
<div class="business-crud-page__dispatch-route-contact">
{{ parseDispatchRoutePoint(dispatchRow, 'end').contact || '-' }}
</div>
</div>
</div>
</div>
<div class="business-crud-page__dispatch-route-line" />
<div class="business-crud-page__dispatch-route-item">
<span class="business-crud-page__dispatch-route-badge is-end">终</span>
<div class="business-crud-page__dispatch-route-body">
<div class="business-crud-page__dispatch-route-title">
{{ parseDispatchRoutePoint(dispatchRow, 'end').name || '-' }}
</div>
<div class="business-crud-page__dispatch-route-address">
{{ parseDispatchRoutePoint(dispatchRow, 'end').address || '-' }}
</div>
<div class="business-crud-page__dispatch-route-contact">
{{ parseDispatchRoutePoint(dispatchRow, 'end').contact || '-' }}
</div>
</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"
@@ -2169,10 +2220,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
@@ -2265,7 +2316,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>
@@ -2274,7 +2325,7 @@
</template>
</el-table-column>
</el-table>
</div>
</section-card>
</div>
<template #footer>
<div class="business-crud-page__dispatch-footer">
@@ -2799,7 +2850,7 @@
:title="billingPlanMode === 'edit' ? '编辑计费方案' : '添加计费方案'"
append-to-body
v-model="billingPlanBox"
width="96%"
width="90%"
class="business-crud-page__billing-dialog"
>
<el-form
@@ -2826,6 +2877,7 @@
<el-form-item label="备注">
<el-input
v-model="billingPlanForm.remark"
class="business-crud-page__billing-plan-remark"
maxlength="100"
show-word-limit
:disabled="dialogReadonly"
@@ -3817,6 +3869,11 @@ export default {
type: Object,
default: () => ({ column: [] }),
},
// 可选:固定操作列宽度;不传则按按钮数量自动估算(沿用既有逻辑)
menuWidth: {
type: Number,
default: null,
},
},
data() {
return {
@@ -3864,7 +3921,11 @@ export default {
defaultDialogButtons: null,
contractCreateActionLoading: '',
transportPlanCreateActionLoading: '',
option: this.cloneOption(this.crudOption, estimateMenuButtonCount(this.config)),
option: (() => {
const opt = this.cloneOption(this.crudOption, estimateMenuButtonCount(this.config));
if (this.menuWidth != null) opt.menuWidth = this.menuWidth;
return opt;
})(),
selectedProjectId: '',
projectOptions: [],
projectLoading: false,
@@ -3957,6 +4018,7 @@ export default {
cargoImportLoading: false,
billingPlanRows: [],
settlementRuleForm: defaultSettlementRule(),
settlementRuleErrors: {},
reconciliationForm: defaultReconciliation(),
changeRecordRows: [],
billingPlanBox: false,
@@ -4034,9 +4096,23 @@ export default {
total: 0,
},
selectionList: [],
contractExpiryScope: '',
contractSignType: '',
searchExpanded: false,
attachmentUploadMode: false,
contractExpiryStats: {
all: 0,
within30: 0,
within90: 0,
over90: 0,
expired: 0,
},
};
},
created() {
if (this.config.enableAllDept && this.isAdmin) {
this.allDept = 1;
}
if (this.config.enableProjectSelect) {
this.loadProjectOptions();
}
@@ -4075,6 +4151,18 @@ export default {
actionButtonLikeSearch() {
return this.config.actionButtonLikeSearch === true;
},
contractSignTypeOptions() {
return this.option.column?.find(column => column.prop === 'signType')?.dicData || [];
},
contractExpiryTagOptions() {
return [
{ label: '全部', value: '', statKey: 'all' },
{ label: '30天内到期', value: 'within30', statKey: 'within30' },
{ label: '90天内到期', value: 'within90', statKey: 'within90' },
{ label: '90天以上', value: 'over90', statKey: 'over90' },
{ label: '已到期', value: 'expired', statKey: 'expired' },
];
},
contractSelectEnabled() {
return this.config.enableContractSelect || this.config.enableTransportPlanForm;
},
@@ -4241,7 +4329,9 @@ export default {
return this.isAdmin;
},
readonlyScope() {
return this.config.enableAllDept && this.allDept === 1;
return (
this.config.enableAllDept && this.config.allDeptReadonly !== false && this.allDept === 1
);
},
canCreate() {
return this.hasPermission(`${this.config.permission}_add`) && !this.readonlyScope;
@@ -4300,6 +4390,9 @@ export default {
emitPath: true,
};
},
settlementRuleEnabled() {
return Number(this.settlementRuleForm.autoGenerate) === 1;
},
showSettlementBillCycleType() {
return this.settlementRuleForm.settlementType === '月结';
},
@@ -4536,6 +4629,7 @@ export default {
return;
}
if (action.action === 'edit' || action.action === 'attachmentUpload') {
this.attachmentUploadMode = action.action === 'attachmentUpload';
this.$refs.crud.rowEdit(row, index);
return;
}
@@ -4713,6 +4807,7 @@ export default {
return rule.settlementType === '固定天数周期结算';
},
handleSettlementTypeChange(value) {
this.clearSettlementRuleError('settlementType');
if (value === '月结') {
if (!this.settlementRuleForm.billCycleType) {
this.settlementRuleForm.billCycleType = '固定截单日';
@@ -4730,6 +4825,7 @@ export default {
}
},
handleSettlementBillCycleTypeChange(value) {
this.clearSettlementRuleError('billCycleType');
if (value === '固定截单日') {
this.settlementRuleForm.billCutoffDay = this.settlementRuleForm.billCutoffDay || 25;
return;
@@ -4752,23 +4848,40 @@ export default {
}
return nextRule;
},
clearSettlementRuleError(field) {
if (!this.settlementRuleErrors[field]) return;
this.settlementRuleErrors = {
...this.settlementRuleErrors,
[field]: '',
};
},
validateSettlementRule() {
const rule = this.normalizeSettlementRule(this.settlementRuleForm);
this.settlementRuleErrors = {};
if (this.isBillingFieldEmpty(rule.billStartDate)) {
this.settlementRuleErrors = { billStartDate: '请选择账单起始日期' };
this.$message.warning('请选择账单起始日期');
return false;
}
if (this.isBillingFieldEmpty(rule.settlementType)) {
this.settlementRuleErrors = { settlementType: '请选择结算类型' };
this.$message.warning('请选择结算类型');
return false;
}
if (this.isMonthlySettlement(rule) && this.isBillingFieldEmpty(rule.billCycleType)) {
this.settlementRuleErrors = { billCycleType: '请选择账单周期类型' };
this.$message.warning('请选择账单周期类型');
return false;
}
if (this.isFixedBillCutoffSettlement(rule)) {
if (this.isBillingFieldEmpty(rule.billCutoffDay)) {
this.settlementRuleErrors = { billCutoffDay: '请选择账单截单日' };
this.$message.warning('请选择账单截单日');
return false;
}
const billCutoffDay = Number(rule.billCutoffDay);
if (!Number.isInteger(billCutoffDay) || billCutoffDay < 1 || billCutoffDay > 31) {
this.settlementRuleErrors = { billCutoffDay: '账单截单日必须为1~31的整数' };
this.$message.warning('账单截单日必须为1~31的整数');
return false;
}
@@ -4776,13 +4889,14 @@ export default {
if (this.isFixedCycleSettlement(rule)) {
const cycleDays = Number(rule.cycleDays);
if (!Number.isInteger(cycleDays) || cycleDays < 1 || cycleDays > 365) {
this.settlementRuleErrors = { cycleDays: '周期天数必须为1~365的整数' };
this.$message.warning('周期天数必须为1~365的整数');
return false;
}
}
return true;
},
normalizeRow(row) {
normalizeRow(row, saveAsDraft = false) {
const submitRow = { ...row };
if (this.config.enableTransportPlanForm) {
if (!this.validateTransportPlanForm(submitRow)) return false;
@@ -4802,7 +4916,7 @@ export default {
submitRow.billingPlanJson = JSON.stringify(this.billingPlanRows);
}
if (this.config.enableSettlementRule) {
if (!this.validateSettlementRule()) return false;
if (this.settlementRuleEnabled && !this.validateSettlementRule()) return false;
submitRow.settlementRuleJson = JSON.stringify(this.normalizeSettlementRule());
}
if (this.config.enableReconciliation) {
@@ -4811,7 +4925,7 @@ export default {
if (this.config.enableChangeRecord) {
submitRow.changeRecordJson = JSON.stringify(this.changeRecordRows);
}
if (this.config.saveAsDraft) {
if (this.config.saveAsDraft && saveAsDraft) {
submitRow.approvalStatus = 'draft';
}
delete submitRow.basicInfoTitle;
@@ -4895,22 +5009,25 @@ export default {
this.stopSubmitLoading(loading);
return;
}
this.getSaveRequest()(submitRow).then(
const request =
this.attachmentUploadMode && typeof this.api.updateAttachments === 'function'
? this.api.updateAttachments
: this.getSaveRequest();
request(submitRow).then(
() => {
this.attachmentUploadMode = false;
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
this.attachmentUploadMode = false;
window.console.log(error);
this.stopSubmitLoading(loading);
}
);
},
getSaveRequest() {
if (this.config.saveAsDraft && typeof this.api.saveDraft === 'function') {
return this.api.saveDraft;
}
return this.api.submit;
},
buildContractCreateSubmitRow(action) {
@@ -5032,7 +5149,7 @@ export default {
handleDraftSave() {
const request =
typeof this.api.saveDraft === 'function' ? this.api.saveDraft : this.api.submit;
const submitRow = this.normalizeRow(this.form);
const submitRow = this.normalizeRow(this.form, true);
if (!submitRow) return;
this.draftSaveLoading = true;
request(submitRow)
@@ -5095,6 +5212,7 @@ export default {
},
beforeOpen(done, type) {
this.crudDialogType = type;
this.settlementRuleErrors = {};
this.syncCustomCreateDialogButtons(type);
this.dialogReadonly = type === 'view';
this.syncContractNoDisabled(type);
@@ -5176,8 +5294,7 @@ export default {
this.syncCurrentOrganizationOption();
}
if (this.config.enableProjectSelect) {
this.form.usedFundLimit = 0;
this.form.remainingFundLimit = 0;
this.form.remainingFundLimit = this.calculateRemainingFundLimit();
this.syncCurrentProjectOption();
}
if (this.contractSelectEnabled) {
@@ -5224,7 +5341,7 @@ export default {
loadProjectOptions() {
if (!this.config.enableProjectSelect || this.projectLoading) return;
this.projectLoading = true;
getProjectList(1, 9999, {})
getProjectList(1, 9999, this.config.projectQueryParams || {})
.then(res => {
this.projectOptions = extractRecords(res);
this.syncCurrentProjectOption();
@@ -5248,6 +5365,12 @@ export default {
});
}
},
calculateRemainingFundLimit() {
const projectFundLimit = Number(this.form.projectFundLimit);
const usedFundLimit = Number(this.form.usedFundLimit);
if (!Number.isFinite(projectFundLimit) || !Number.isFinite(usedFundLimit)) return '';
return Math.round((projectFundLimit - usedFundLimit + Number.EPSILON) * 100) / 100;
},
handleProjectChange(projectId) {
const project = this.projectOptions.find(item => String(item.id) === String(projectId));
if (!project) {
@@ -5275,7 +5398,7 @@ export default {
this.form.undertakeDeptName = project.undertakeDeptName || '';
this.form.projectFundLimit = project.fundLimit || project.projectFundLimit || 0;
this.form.usedFundLimit = 0;
this.form.remainingFundLimit = 0;
this.form.remainingFundLimit = this.calculateRemainingFundLimit();
if (this.contractSelectEnabled || this.config.enableTransportPlanForm) {
this.form.customerName =
project.customerName ||
@@ -7617,11 +7740,16 @@ export default {
},
searchReset() {
this.query = {};
this.contractExpiryScope = '';
this.contractSignType = '';
this.page.currentPage = 1;
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.query = {
...params,
...(this.contractSignType ? { signType: this.contractSignType } : {}),
};
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
@@ -7644,6 +7772,25 @@ export default {
refreshChange() {
this.onLoad(this.page, this.query);
},
handleSearchIconChange(expanded) {
this.searchExpanded = expanded;
},
handleContractExpiryScopeChange(expireScope) {
this.contractExpiryScope = expireScope;
this.page.currentPage = 1;
this.onLoad(this.page, this.query);
},
refreshContractExpiryStats(query) {
if (!this.config.enableContractExpiryTags || typeof this.api.expireStats !== 'function') return;
const statsQuery = { ...query };
delete statsQuery.expireScope;
this.api.expireStats(statsQuery).then(res => {
this.contractExpiryStats = {
...this.contractExpiryStats,
...(res.data.data || {}),
};
});
},
handleScopeChange() {
if (!this.canViewAllDept) {
this.allDept = 0;
@@ -7700,7 +7847,13 @@ export default {
},
onLoad(page, params = {}) {
this.loading = true;
const query = this.normalizeQueryParams({ ...params, ...this.query, allDept: this.allDept });
const query = this.normalizeQueryParams({
...params,
...this.query,
allDept: this.allDept,
...(this.contractSignType ? { signType: this.contractSignType } : {}),
...(this.contractExpiryScope ? { expireScope: this.contractExpiryScope } : {}),
});
this.api
.getList(page.currentPage, page.pageSize, query)
.then(res => {
@@ -7708,6 +7861,7 @@ export default {
this.page.total = result.total;
this.data = result.records;
this.selectionClear();
this.refreshContractExpiryStats(query);
})
.finally(() => {
this.loading = false;
@@ -8611,6 +8765,38 @@ export default {
width: 100%;
}
&__contract-expiry-search {
display: flex;
order: -1;
align-items: center;
gap: 8px;
margin-right: auto;
min-width: 0;
:deep(.el-select) {
width: 160px;
}
}
&__contract-expiry-label {
flex: 0 0 auto;
min-width: 140px;
color: #606266;
text-align: right;
white-space: nowrap;
}
&__contract-expiry-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
:deep(.el-check-tag) {
margin: 0;
white-space: nowrap;
}
}
.dialog-section-title {
display: flex;
align-items: center;
@@ -9131,7 +9317,10 @@ export default {
}
:deep(.el-dialog__body) {
padding: 0;
padding: 16px;
background: #f5f6fa;
max-height: calc(100vh - 140px);
overflow-y: auto;
}
}
@@ -9142,12 +9331,11 @@ export default {
}
&__dispatch-top {
padding: 22px 32px 14px;
border-bottom: 1px solid #e9edf5;
padding: 0;
}
&__dispatch-heading {
margin-bottom: 20px;
margin-bottom: 14px;
}
&__dispatch-heading-title {
@@ -9158,51 +9346,60 @@ export default {
}
&__dispatch-heading-name {
font-size: 22px;
font-size: 18px;
font-weight: 600;
color: #303133;
}
&__dispatch-heading-split {
font-size: 20px;
color: #606266;
}
&__dispatch-heading-no {
font-size: 15px;
color: #303133;
font-size: 14px;
color: #606266;
}
&__dispatch-summary {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(520px, 1fr);
gap: 32px;
align-items: start;
grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
gap: 16px;
align-items: stretch;
margin-bottom: 12px;
}
&__dispatch-summary-card,
&__dispatch-route-card {
display: flex;
flex-direction: column;
:deep(.section-card__body) {
flex: 1;
}
}
&__dispatch-summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
column-gap: 48px;
row-gap: 18px;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 16px;
}
&__dispatch-summary-item {
min-width: 0;
&.is-wide {
grid-column: span 2;
}
}
&__dispatch-summary-label {
margin-bottom: 8px;
font-size: 14px;
color: #666;
font-weight: 500;
margin-bottom: 4px;
font-size: 12px;
color: #909399;
line-height: 1.4;
}
&__dispatch-summary-value {
min-height: 24px;
font-size: 15px;
min-height: 20px;
font-size: 14px;
line-height: 1.45;
color: #333;
color: #303133;
word-break: break-word;
}
@@ -9212,24 +9409,30 @@ export default {
&__dispatch-attachment-link {
display: inline-block;
margin-right: 22px;
margin-bottom: 6px;
margin-right: 16px;
margin-bottom: 4px;
font-size: 14px;
}
&__dispatch-route {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) 72px minmax(0, 1fr);
gap: 24px;
grid-template-columns: minmax(0, 1fr) 56px minmax(0, 1fr);
gap: 40px;
align-items: center;
justify-items: center;
min-height: 200px;
//height: 100%;
margin: 20px 0;
}
&__dispatch-route-item {
display: flex;
align-items: flex-start;
gap: 16px;
max-width: 100%;
align-items: center;
gap: 12px;
max-width: 90%;
padding: 16px;
min-height: 80px;
border: 1px dashed #e3e4ec;
border-radius: 12px;
}
&__dispatch-route-badge {
@@ -9237,12 +9440,13 @@ export default {
flex: 0 0 auto;
align-items: center;
justify-content: center;
width: 46px;
height: 46px;
border-radius: 6px;
width: 40px;
height: 40px;
border-radius: 50%;
color: #fff;
font-size: 16px;
font-weight: 600;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
&.is-start {
background: #2a92ff;
@@ -9254,39 +9458,68 @@ export default {
}
&__dispatch-route-body {
max-width: 340px;
text-align: center;
min-width: 0;
text-align: left;
}
&__dispatch-route-title {
font-size: 22px;
line-height: 1.25;
font-size: 18px;
line-height: 1.35;
color: #303133;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__dispatch-route-address {
margin-top: 12px;
font-size: 15px;
line-height: 1.45;
color: #333;
margin-top: 4px;
font-size: 13px;
line-height: 1.5;
color: #606266;
}
&__dispatch-route-contact {
margin-top: 10px;
font-size: 15px;
color: #333;
margin-top: 2px;
font-size: 13px;
color: #909399;
}
&__dispatch-route-line {
width: 72px;
height: 4px;
background: #3b3b3b;
border-radius: 999px;
}
&__dispatch-route-vehicle {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
margin: 0 auto;
border-radius: 50%;
background: #fff;
border: 2px solid var(--el-color-primary);
color: var(--el-color-primary);
font-size: 22px;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.18);
&__dispatch-list {
padding: 12px 18px 18px;
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
width: 20px;
height: 0;
border-top: 2px dashed var(--el-color-primary-light-5);
transform: translateY(-50%);
}
&::before {
right: 100%;
margin-right: 6px;
}
&::after {
left: 100%;
margin-left: 6px;
}
}
&__dispatch-list-head {
@@ -9338,8 +9571,11 @@ export default {
&__dispatch-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
gap: 10px;
padding-right: 0;
padding-top: 16px;
margin-top: 16px;
border-top: 1px solid #e9edf5;
}
&__dispatch-item-dialog {
@@ -9405,6 +9641,10 @@ export default {
margin-bottom: 18px;
}
&__billing-plan-remark {
width: 80%;
}
&__billing-rule-head {
display: flex;
align-items: center;
@@ -9656,4 +9896,7 @@ export default {
}
}
}
.section-card{
margin-bottom: 0 !important;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<business-crud-page :api="api" :config="config" :crud-option="option" />
<business-crud-page :api="api" :config="config" :crud-option="option" :menu-width="245" />
</template>
<script>

File diff suppressed because it is too large Load Diff

View File

@@ -20,7 +20,6 @@
<el-button
type="primary"
icon="el-icon-plus"
plain
v-if="canMajorProjectSupplement"
@click="openProjectDialog('majorSupplement')"
>
@@ -29,7 +28,6 @@
<el-button
type="primary"
icon="el-icon-plus"
plain
v-if="canCreate"
@click="openProjectDialog('add')"
>
@@ -176,21 +174,16 @@
</el-select>
</el-form-item>
<el-form-item label="承办部门" prop="undertakeDeptId">
<el-select
<el-cascader
v-model="form.undertakeDeptId"
placeholder="请选择承办部门"
filterable
clearable
filterable
:options="deptTreeOptions"
:props="deptCascaderProps"
:disabled="isBasicInfoReadonly"
@change="handleUndertakeDeptChange"
>
<el-option
v-for="item in deptOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
/>
</el-form-item>
<el-form-item label="项目由来" prop="projectSource">
<el-select
@@ -217,9 +210,11 @@
<el-form-item label="客户名称" prop="customerNames">
<el-select
v-model="selectedCustomerId"
placeholder="搜索选择"
placeholder="支持多选,选择后下方列表展示"
multiple
filterable
clearable
collapse-tags
:loading="customerLoading"
@visible-change="visible => visible && loadCustomerOptions('客户')"
@change="handleCustomerChange"
@@ -257,6 +252,7 @@
v-model="form.fundLimit"
placeholder="实际业务回款周期内付款额度"
@input="value => handleNumberInput('fundLimit', value)"
@blur="formatAmountField('fundLimit')"
/>
</el-form-item>
<el-form-item label="项目应收账款额度万元" prop="receivableLimit">
@@ -264,6 +260,7 @@
v-model="form.receivableLimit"
placeholder="回款期内最大应收账款额度"
@input="value => handleNumberInput('receivableLimit', value)"
@blur="formatAmountField('receivableLimit')"
/>
</el-form-item>
<el-form-item label="应收账款回款期限" prop="receivableDays">
@@ -466,65 +463,75 @@
</el-form-item>
</div>
<div class="project-apply-form__material-head">
<div class="dialog-section-title project-apply-form__material-title">项目材料</div>
<el-button type="primary" :disabled="!attachmentRows.length" @click="handleBatchDownload"
>批量下载</el-button
>
</div>
<el-table :data="attachmentRows" border class="project-apply-form__table">
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column label="附件类型" min-width="160" align="center">
<template #default="{ row }">
<el-select v-model="row.fileType" placeholder="请选择" :disabled="dialogReadonly">
<el-option
v-for="item in projectAttachmentTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
<div class="project-apply-form__material-card">
<div class="project-apply-form__material-head">
<div class="dialog-section-title project-apply-form__material-title">项目材料</div>
<el-button
type="primary"
:disabled="!attachmentRows.length"
@click="handleBatchDownload"
>批量下载</el-button
>
</div>
<el-alert
v-if="dialogReadonly && missingRequiredAttachmentTypes.length"
class="project-apply-form__material-warning"
type="warning"
:closable="false"
show-icon
:title="`缺少必需材料:${missingRequiredAttachmentTypes.join('、')}`"
/>
<el-table :data="attachmentRows" border class="project-apply-form__material-table">
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column label="附件类型" min-width="160" align="center">
<template #default="{ row }">
<el-select v-model="row.fileType" placeholder="请选择" :disabled="dialogReadonly">
<el-option
v-for="item in projectAttachmentTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="文件名" min-width="220" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.originalName || row.name }}</template>
</el-table-column>
<el-table-column label="附件描述" min-width="240" align="center">
<template #default="{ row }">
<el-input
v-model="row.description"
placeholder="请输入附件描述"
:disabled="dialogReadonly"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="文件名" min-width="220" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.originalName || row.name }}</template>
</el-table-column>
<el-table-column label="附件描述" min-width="240" align="center">
<template #default="{ row }">
<el-input
v-model="row.description"
placeholder="请输入附件描述"
:disabled="dialogReadonly"
/>
</template>
</el-table-column>
<el-table-column label="文件大小" min-width="120" align="center">
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
</el-table-column>
<el-table-column prop="uploadUserName" label="上传人" min-width="140" align="center" />
<el-table-column prop="uploadTime" label="上传时间" min-width="170" align="center" />
<el-table-column label="操作" width="110" align="center">
<template #default="{ row, $index }">
<el-link type="primary" @click="downloadAttachment(row)">下载</el-link>
<el-link
v-if="!dialogReadonly"
type="primary"
@click="removeAttachment($index)"
>删除</el-link
>
</template>
</el-table-column>
</el-table>
<vehicle-attachment-upload
v-model="attachmentRows"
class="project-apply-form__upload"
:readonly="dialogReadonly"
:file-types="attachmentFileTypes"
:max-size="500"
button-text="上传附件"
tip="支持pdfbmpjpegpngjpgdocdocxpptpptxxlsxxlsemlmsgzip的文件格式单个文件大小限制500M"
@change="handleAttachmentChange"
/>
</template>
</el-table-column>
<el-table-column label="文件大小" min-width="120" align="center">
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
</el-table-column>
<el-table-column prop="uploadUserName" label="上传人" min-width="140" align="center" />
<el-table-column prop="uploadTime" label="上传时间" min-width="170" align="center" />
<el-table-column label="操作" width="110" align="center">
<template #default="{ row, $index }">
<el-link type="primary" @click="downloadAttachment(row)">下载</el-link>
<el-link v-if="!dialogReadonly" type="primary" @click="removeAttachment($index)"
>删除</el-link
>
</template>
</el-table-column>
</el-table>
<vehicle-attachment-upload
v-model="attachmentRows"
class="project-apply-form__upload"
:readonly="dialogReadonly"
:file-types="attachmentFileTypes"
:max-size="50"
button-text="上传附件"
tip="支持pdfbmpjpegpngjpgdocdocxpptpptxxlsxxlsemlmsgzip的文件格式单个文件大小限制50M"
@change="handleAttachmentChange"
/>
</div>
<div v-if="!isChangeDialog" class="dialog-section-title project-apply-form__table-title">
变更记录
@@ -595,16 +602,24 @@
:class="{ 'project-apply-dialog__footer--change': isChangeDialog }"
>
<template v-if="isChangeDialog">
<el-button type="primary" :loading="submitLoading" @click="submitChangeProject">
提交
</el-button>
<el-button type="primary" :loading="submitLoading" @click="saveChangeProject">
保存
</el-button>
<el-button @click="projectBox = false">关闭</el-button>
<el-button type="primary" :loading="submitLoading" @click="submitChangeProject">
提交
</el-button>
<el-button @click="handleCancelProject">取消</el-button>
</template>
<template v-else>
<el-button @click="projectBox = false">取消</el-button>
<el-button @click="handleCancelProject">取消</el-button>
<el-button
v-if="!dialogReadonly"
type="primary"
:loading="submitLoading"
@click="saveProject"
>
保存
</el-button>
<el-button
v-if="!dialogReadonly"
type="primary"
@@ -680,7 +695,6 @@ import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
import { getList as getUserList } from '@/api/system/user';
import { getToken } from '@/utils/auth';
import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
@@ -745,6 +759,7 @@ const changeTypeOptions = [
];
const normalProjectAttachmentTypeOptions = [
'项目立项报告',
'利润测算表',
'单一来源供应商申请表/三方比价表',
'合同模板',
@@ -754,6 +769,7 @@ const normalProjectAttachmentTypeOptions = [
].map(item => ({ label: item, value: item }));
const majorProjectAttachmentTypeOptions = [
'项目立项报告',
'会议纪要',
'评审表决票',
'利润测算表',
@@ -875,6 +891,8 @@ export default {
fundDemand: [{ label: '资金需求', validator: validateAmount, trigger: 'blur' }],
handlerUserId: [
{
required: true,
message: '请选择项目经办人',
label: '项目经办人',
nameProp: 'handlerUserName',
validator: validateUser,
@@ -883,6 +901,8 @@ export default {
],
principalUserId: [
{
required: true,
message: '请选择项目负责人',
label: '项目负责人',
nameProp: 'principalUserName',
validator: validateUser,
@@ -899,8 +919,16 @@ export default {
],
},
deptOptions: [],
deptTreeOptions: [],
deptCascaderProps: {
label: 'label',
value: 'value',
children: 'children',
emitPath: false,
checkStrictly: true,
},
cargoTypeOptions: [],
selectedCustomerId: '',
selectedCustomerId: [],
selectedCarrierIds: [],
customerOptions: [],
carrierOptions: [],
@@ -993,6 +1021,12 @@ export default {
? majorProjectAttachmentTypeOptions
: normalProjectAttachmentTypeOptions;
},
missingRequiredAttachmentTypes() {
const uploadedTypes = new Set(this.attachmentRows.map(item => item.fileType).filter(Boolean));
return this.projectAttachmentTypeOptions
.map(item => item.value)
.filter(fileType => !uploadedTypes.has(fileType));
},
},
created() {
this.loadDeptOptions();
@@ -1001,18 +1035,17 @@ export default {
},
methods: {
buildTableOption() {
return applyTableMenuWidth(
{
...option,
dialogType: 'dialog',
addBtn: false,
viewBtn: false,
editBtn: false,
delBtn: false,
column: (option.column || []).map(column => ({ ...column, display: false })),
},
4
);
return {
...option,
dialogType: 'dialog',
addBtn: false,
viewBtn: false,
editBtn: false,
delBtn: false,
// 操作列收窄本项目操作按钮较少320px 过宽,调整为 220px链接会自动换行兜底
menuWidth: 180,
column: (option.column || []).map(column => ({ ...column, display: false })),
};
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
@@ -1233,7 +1266,7 @@ export default {
if (type === 'add' || type === 'majorSupplement') {
this.applyProjectDetail({
...emptyForm(),
projectType: type === 'majorSupplement' ? '重大项目' : '',
projectType: type === 'majorSupplement' ? '重大项目' : '普通项目',
});
this.fillDefaultUsers();
return;
@@ -1253,21 +1286,35 @@ export default {
this.carrierRows = [];
this.attachmentRows = [];
this.changeRows = [];
this.selectedCustomerId = '';
this.selectedCustomerId = [];
this.selectedCarrierIds = [];
},
applyProjectDetail(row) {
const displayRow = {
...row,
fundLimit: this.formatAmount(row.fundLimit),
receivableLimit: this.formatAmount(row.receivableLimit),
...(this.dialogReadonly
? {
projectScale: this.normalizeReadonlyAmount(row.projectScale),
estimatedProfit: this.normalizeReadonlyAmount(row.estimatedProfit),
fundDemand: this.normalizeReadonlyAmount(row.fundDemand),
}
: {}),
};
const form = {
...emptyForm(),
...row,
...displayRow,
handlerUserName: this.resolveUserRealName(displayRow, 'handler'),
principalUserName: this.resolveUserRealName(displayRow, 'principal'),
businessDateRange:
row.businessStartDate && row.businessEndDate
? [row.businessStartDate, row.businessEndDate]
displayRow.businessStartDate && displayRow.businessEndDate
? [displayRow.businessStartDate, displayRow.businessEndDate]
: [],
};
const situation = this.parseSituation(row.situationRemark);
const customerRows = this.parseJsonArray(row.customerJson);
const carrierRows = this.parseJsonArray(row.carrierJson);
const situation = this.parseSituation(displayRow.situationRemark);
const customerRows = this.parseJsonArray(displayRow.customerJson);
const carrierRows = this.parseJsonArray(displayRow.carrierJson);
this.form = {
...form,
...situation,
@@ -1276,9 +1323,11 @@ export default {
};
this.customerRows = customerRows;
this.carrierRows = carrierRows;
this.attachmentRows = this.parseJsonArray(row.attachmentsJson);
this.changeRows = this.buildChangeRows(row);
this.selectedCustomerId = this.getCustomerRowId(this.customerRows[0]);
this.attachmentRows = this.parseJsonArray(displayRow.attachmentsJson);
this.changeRows = this.buildChangeRows(displayRow);
this.selectedCustomerId = this.customerRows
.map(item => this.getCustomerRowId(item))
.filter(Boolean);
this.selectedCarrierIds = this.carrierRows
.map(item => this.getCustomerRowId(item))
.filter(Boolean);
@@ -1287,7 +1336,13 @@ export default {
fillDefaultUsers() {
const userId = this.userInfo?.userId || this.userInfo?.id || '';
const userName =
this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || '';
this.userInfo?.realName ||
this.userInfo?.real_name ||
this.userInfo?.name ||
this.userInfo?.userName ||
this.userInfo?.user_name ||
this.userInfo?.account ||
'';
if (userId && userName) {
this.form.handlerUserId = userId;
this.form.handlerUserName = userName;
@@ -1295,6 +1350,16 @@ export default {
this.form.principalUserName = userName;
}
},
resolveUserRealName(row = {}, role) {
return (
row[`${role}RealName`] ||
row[`${role}UserRealName`] ||
row[`${role}User`]?.realName ||
row[role]?.realName ||
row[`${role}UserName`] ||
''
);
},
resolveChangeType(row = {}) {
if (row.changeType) return row.changeType;
const node = row.currentNode || '';
@@ -1330,6 +1395,36 @@ export default {
});
});
},
saveProject() {
this.submitLoading = true;
this.api
.saveDraft(this.normalizeSubmitForm())
.then(res => {
if (res.data?.success === false || res.data?.data === false) {
this.$message.error(res.data?.msg || '保存失败,请联系管理员');
return;
}
this.$message.success('保存成功!');
this.projectBox = false;
this.onLoad(this.page, this.query);
})
.finally(() => {
this.submitLoading = false;
});
},
handleCancelProject() {
if (!['add', 'majorSupplement', 'change'].includes(this.dialogType)) {
this.projectBox = false;
return;
}
this.$confirm('确认后直接关闭弹窗,列表无数据变更,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.projectBox = false;
});
},
saveChangeProject() {
this.submitChangeForm(false);
},
@@ -1366,6 +1461,8 @@ export default {
const [businessStartDate, businessEndDate] = this.form.businessDateRange || [];
const submitRow = {
...this.form,
fundLimit: this.formatAmount(this.form.fundLimit),
receivableLimit: this.formatAmount(this.form.receivableLimit),
businessStartDate,
businessEndDate,
customerJson: JSON.stringify(this.customerRows),
@@ -1479,6 +1576,11 @@ export default {
})
.filter(Boolean);
},
normalizeReadonlyAmount(value) {
return value === null || value === undefined || value === '' || Number(value) === -1
? ''
: value;
},
mergeCustomerOptions(options = [], selectedOptions = []) {
const result = [...selectedOptions];
options.forEach(option => {
@@ -1507,7 +1609,7 @@ export default {
changeType:
row.changeType ||
(String(row.currentNode || '').includes('备案') ? '项目备案调整' : '项目变更'),
handler: row.handlerUserName || '',
handler: this.resolveUserRealName(row, 'handler'),
content: row.changeContent || '',
reason: row.changeReason || '',
status: row.approvalStatusName || row.approvalStatus || '',
@@ -1531,9 +1633,18 @@ export default {
},
loadDeptOptions() {
getDeptTree().then(res => {
this.deptOptions = this.flattenDept(res.data.data || []);
const deptTree = res.data.data || [];
this.deptOptions = this.flattenDept(deptTree);
this.deptTreeOptions = this.toDeptCascaderOptions(deptTree);
});
},
toDeptCascaderOptions(list = []) {
return list.map(item => ({
label: item.title || item.deptName || item.name,
value: item.id,
children: item.children?.length ? this.toDeptCascaderOptions(item.children) : undefined,
}));
},
flattenDept(list = [], level = 0) {
return list.reduce((result, item) => {
const label = item.title || item.deptName || item.name;
@@ -1568,7 +1679,11 @@ export default {
const loadingKey = type === '客户' ? 'customerLoading' : 'carrierLoading';
const optionsKey = type === '客户' ? 'customerOptions' : 'carrierOptions';
this[loadingKey] = true;
getCustomerArchiveList(1, 100, { customerType: type, status: 1 })
getCustomerArchiveList(1, 100, {
customerType: type,
status: 1,
...(type === '客户' ? { approvalStatus: 'approved' } : {}),
})
.then(res => {
const records = (res.data.data && res.data.data.records) || [];
const selectedOptions = this.buildCustomerOptionsFromRows(
@@ -1592,16 +1707,16 @@ export default {
raw: item,
};
},
handleCustomerChange(id) {
if (!id) {
handleCustomerChange(ids) {
if (!ids || !ids.length) {
this.customerRows = [];
this.form.customerNames = '';
this.form.customerJson = '';
return;
}
this.loadCustomerDetailRow(id, 'customer').then(row => {
this.customerRows = [row];
this.form.customerNames = row.customer;
Promise.all(ids.map(id => this.loadCustomerDetailRow(id, 'customer'))).then(rows => {
this.customerRows = rows;
this.form.customerNames = rows.map(item => item.customer).join('、');
this.form.customerJson = JSON.stringify(this.customerRows);
this.syncSelectedCustomerOptions();
this.$refs.projectForm?.validateField('customerNames');
@@ -1654,6 +1769,14 @@ export default {
this.form[prop] =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
},
formatAmount(value) {
if (value === null || value === undefined || value === '' || Number(value) === -1) return '';
const amount = Number(value);
return Number.isFinite(amount) ? amount.toFixed(2) : value;
},
formatAmountField(prop) {
this.form[prop] = this.formatAmount(this.form[prop]);
},
handleIntegerInput(prop, value) {
this.form[prop] = String(value || '').replace(/[^\d]/g, '');
},
@@ -1834,13 +1957,36 @@ export default {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
&__material-card {
margin-bottom: 16px;
background: #fff;
border-radius: 6px;
padding: 12px 16px;
padding: 14px 16px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
&__material-table {
width: 100%;
margin-bottom: 12px;
:deep(.el-table__header th) {
background: #f5f7fa;
color: #303133;
font-weight: 600;
}
:deep(.el-table__cell) {
border-color: #eff1f7;
}
}
&__material-warning {
margin-bottom: 12px;
}
&__material-title {
flex: 1;
}

View File

@@ -1,5 +1,5 @@
<template>
<business-crud-page :api="api" :config="config" :crud-option="option" />
<business-crud-page :api="api" :config="config" :crud-option="option" :menu-width="270" />
</template>
<script>

View File

@@ -1,5 +1,5 @@
<template>
<business-crud-page :api="api" :config="config" :crud-option="option" />
<business-crud-page :api="api" :config="config" :crud-option="option" :menu-width="270" />
</template>
<script>

View File

@@ -1,5 +1,5 @@
<template>
<business-crud-page :api="api" :config="config" :crud-option="option" />
<business-crud-page :api="api" :config="config" :crud-option="option" :menu-width="220" />
</template>
<script>

View File

@@ -40,23 +40,121 @@
</el-link>
</template>
<template #deptCategory="{ row }">
<el-tag>{{ row.deptCategoryName }}</el-tag>
<el-tag>{{ getDeptCategoryLabel(row.deptCategory) }}</el-tag>
</template>
<template #leaderId="{ row }">
{{ getLeaderValue(row.leaderId, 'realName') }}
</template>
<template #leaderPhone="{ row }">
{{ getLeaderValue(row.leaderId, 'phone') }}
</template>
<template #status="{ row }">
<el-tag :type="Number(row.status) === 1 ? 'success' : 'info'">
{{ Number(row.status) === 1 ? '启用' : '停用' }}
</el-tag>
</template>
</avue-crud>
<el-dialog v-model="carrierDialogVisible" title="选择承运商" width="1100px" append-to-body>
<el-form :inline="true" :model="carrierQuery" class="carrier-search-form">
<el-form-item label="名称">
<el-input v-model="carrierQuery.fullName" placeholder="请输入承运商名称" clearable />
</el-form-item>
<el-form-item label="准入标准">
<el-select v-model="carrierQuery.accessType" placeholder="全部" clearable>
<el-option label="临时" value="temporary" />
<el-option label="正式" value="formal" />
</el-select>
</el-form-item>
<el-form-item label="审批状态">
<el-select v-model="carrierQuery.approvalStatus" placeholder="全部" clearable>
<el-option label="草稿" value="draft" />
<el-option label="审核中" value="reviewing" />
<el-option label="审核通过" value="approved" />
<el-option label="审核不通过" value="rejected" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="carrierQuery.status" placeholder="全部" clearable>
<el-option label="启用" :value="1" />
<el-option label="停用" :value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="searchCarrier">搜索</el-button>
<el-button @click="resetCarrierQuery">清空</el-button>
</el-form-item>
</el-form>
<el-table :data="carrierData" border stripe height="420" @row-dblclick="selectCarrier">
<el-table-column prop="customerCode" label="客商编号" min-width="130" />
<el-table-column prop="shortName" label="客商简称" min-width="140" />
<el-table-column prop="fullName" label="客商名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="customerType" label="客户类型" min-width="120" />
<el-table-column prop="customerNature" label="客户性质" min-width="120" />
<el-table-column prop="unifiedCreditCode" label="统一信用代码" min-width="180" />
<el-table-column prop="deptName" label="所属组织" min-width="150" />
<el-table-column prop="accessType" label="准入标准" min-width="110">
<template #default="{ row }">{{ formatAccessType(row.accessType) }}</template>
</el-table-column>
<el-table-column prop="approvalStatus" label="审批状态" min-width="120">
<template #default="{ row }">{{ formatApprovalStatus(row.approvalStatus) }}</template>
</el-table-column>
<el-table-column prop="currentNode" label="当前节点" min-width="130" />
<el-table-column prop="currentProcessor" label="当前处理人" min-width="130" />
<el-table-column prop="approvedTime" label="审核通过时间" min-width="170" />
<el-table-column prop="status" label="状态" width="90">
<template #default="{ row }">{{ Number(row.status) === 1 ? '启用' : '停用' }}</template>
</el-table-column>
<el-table-column label="操作" width="90" fixed="right">
<template #default="{ row }">
<el-link type="primary" @click="selectCarrier(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<div class="carrier-pagination">
<el-pagination
v-model:current-page="carrierPage.current"
v-model:page-size="carrierPage.size"
:total="carrierPage.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@current-change="loadCarrierList"
@size-change="handleCarrierSizeChange"
/>
</div>
</el-dialog>
</basic-container>
</template>
<script>
import { getLazyList, remove, update, add, getDept, getDeptTree } from '@/api/system/dept';
import { getLeaderList } from '@/api/system/user';
import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive';
import { mapGetters } from 'vuex';
import website from '@/config/website';
import func from '@/utils/func';
const escapeRegExp = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
export default {
data() {
return {
form: {},
leaderOptions: [],
parentDeptCode: '',
parentDept: null,
carrierDialogVisible: false,
carrierData: [],
carrierQuery: {
fullName: '',
accessType: '',
approvalStatus: '',
status: undefined,
},
carrierPage: {
current: 1,
size: 10,
total: 0,
},
selectionList: [],
query: {},
loading: true,
@@ -73,29 +171,170 @@ export default {
searchShow: true,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchIndex: 4,
searchMenuPosition: 'right',
tree: true,
border: true,
index: true,
selection: true,
viewBtn: true,
menuWidth: 160,
menuWidth: 320,
dialogWidth: 800,
dialogClickModal: false,
labelPosition: 'right',
labelWidth: 'auto',
column: [
{
label: '机构名称',
label: '组织名称',
prop: 'deptName',
minWidth: 160,
order: 80,
search: true,
rules: [
{
required: true,
message: '请输入机构名称',
message: '请输入组织名称',
trigger: 'blur',
},
],
},
{
label: '组织编码',
prop: 'deptCode',
minWidth: 140,
order: 70,
maxlength: 30,
showWordLimit: true,
rules: [
{
required: true,
message: '请输入组织编码',
trigger: 'blur',
},
{
validator: (rule, value, callback) => {
const deptCode = String(value || '').trim();
if (deptCode.length > 30) {
callback(new Error('组织编码不能超过30个字符'));
return;
}
if (!this.parentDeptCode) {
callback(new Error('请先选择上级组织'));
return;
}
const pattern = new RegExp(`^${escapeRegExp(this.parentDeptCode)}-\\d+$`);
if (!pattern.test(deptCode)) {
callback(new Error(`组织编码格式应为:${this.parentDeptCode}-分段数字`));
return;
}
callback();
},
trigger: 'blur',
},
],
},
{
label: '组织简称',
prop: 'shortName',
minWidth: 140,
order: 60,
},
{
label: '组织类型',
type: 'select',
dicData: [
{ label: '一级集团', value: 1 },
{ label: '二级事业部', value: 2 },
{ label: '部门', value: 3 },
{ label: '三级分公司', value: 4 },
{ label: '物流园区', value: 5 },
{ label: '承运商', value: 6 },
],
dataType: 'number',
minWidth: 120,
order: 90,
prop: 'deptCategory',
slot: true,
change: ({ value }) => this.handleDeptCategoryChange(value),
rules: [
{
required: true,
message: '请选择组织类型',
trigger: 'change',
},
],
},
{
label: '负责人',
prop: 'leaderId',
minWidth: 120,
slot: true,
addDisplay: false,
editDisplay: false,
},
{
label: '联系电话',
prop: 'leaderPhone',
minWidth: 140,
slot: true,
addDisplay: false,
editDisplay: false,
},
{
label: '助记码',
prop: 'mnemonicCode',
minWidth: 120,
order: 40,
},
{
label: '拼音助记码',
prop: 'pinyinMnemonic',
minWidth: 140,
order: 50,
},
{
label: '排序',
prop: 'sort',
type: 'number',
align: 'right',
width: 80,
order: 30,
rules: [
{
required: true,
message: '请输入排序',
trigger: 'blur',
},
],
},
{
label: '状态',
prop: 'status',
width: 100,
slot: true,
addDisplay: false,
editDisplay: false,
},
{
label: '上级组织',
prop: 'parentId',
dicData: [],
type: 'tree',
hide: true,
addDisabled: false,
order: 100,
change: ({ value }) => this.handleParentChange(value),
props: {
label: 'title',
},
rules: [
{
required: true,
message: '请选择上级组织',
trigger: 'click',
},
],
},
{
label: '所属租户',
prop: 'tenantId',
@@ -104,112 +343,37 @@ export default {
addDisplay: false,
editDisplay: false,
viewDisplay: website.tenantMode,
span: 24,
props: {
label: 'tenantName',
value: 'tenantId',
},
hide: !website.tenantMode,
hide: true,
search: website.tenantMode,
rules: [
{
required: true,
message: '请输入所属租户',
trigger: 'click',
},
],
},
{
label: '机构全称',
prop: 'fullName',
search: true,
rules: [
{
required: true,
message: '请输入机构全称',
trigger: 'blur',
},
],
},
{
label: '上级机构',
prop: 'parentId',
dicData: [],
type: 'tree',
hide: true,
addDisabled: false,
props: {
label: 'title',
},
rules: [
{
required: false,
message: '请选择上级机构',
trigger: 'click',
},
],
},
{
label: '直属主管',
prop: 'leaderId',
type: 'tree',
multiple: true,
filterable: true,
dicData: [],
props: {
label: 'realName',
value: 'id',
},
clearable: true,
hide: true,
},
{
label: '机构类型',
type: 'select',
dicUrl: '/blade-system/dict/dictionary?code=org_category',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
width: 120,
prop: 'deptCategory',
slot: true,
rules: [
{
required: true,
message: '请输入机构类型',
trigger: 'blur',
},
],
},
{
label: '排序',
prop: 'sort',
type: 'number',
align: 'right',
width: 80,
rules: [
{
required: true,
message: '请输入排序',
trigger: 'blur',
},
],
addDisplay: false,
editDisplay: false,
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
span: 24,
minRows: 2,
hide: true,
addDisplay: false,
editDisplay: false,
},
],
},
data: [],
};
},
created() {
this.loadLeaderOptions();
},
computed: {
...mapGetters(['userInfo', 'permission']),
permissionList() {
@@ -234,13 +398,130 @@ export default {
const column = this.findColumn(this.option.column, 'parentId');
column.dicData = res.data.data;
});
getLeaderList(tenantId).then(res => {
const column = this.findColumn(this.option.column, 'leaderId');
column.dicData = res.data.data;
},
handleParentChange(parentId) {
this.loadParentDeptCode(parentId);
},
loadParentDeptCode(parentId) {
this.parentDeptCode = '';
this.parentDept = null;
if (!parentId || String(parentId) === '0') return;
getDept(parentId).then(res => {
this.parentDept = res.data.data || null;
this.parentDeptCode = this.parentDept?.deptCode || '';
this.updateDeptCategoryOptions();
});
},
updateDeptCategoryOptions() {
const categoryColumn = this.findColumn(this.option.column, 'deptCategory');
const parent = this.parentDept;
if (!parent) return;
let dicData = [];
if (this.isRootDept(parent)) {
dicData =
parent.deptName === '外部组织'
? [{ label: '承运商', value: 6 }]
: [{ label: '集团', value: 1 }];
} else if (Number(parent.deptCategory) === 1) {
dicData = [
{ label: '事业部', value: 2 },
{ label: '部门', value: 3 },
];
} else if ([2, 3].includes(Number(parent.deptCategory))) {
dicData = [
{ label: '分公司', value: 4 },
{ label: '物流园区', value: 5 },
{ label: '承运商', value: 6 },
];
}
categoryColumn.dicData = dicData;
if (!dicData.some(item => item.value === Number(this.form.deptCategory))) {
this.form.deptCategory = dicData[0]?.value;
}
if (Number(this.form.deptCategory) === 6) {
this.openCarrierDialog();
}
},
handleDeptCategoryChange(value) {
if (Number(value) === 6) {
this.openCarrierDialog();
}
},
openCarrierDialog() {
this.carrierDialogVisible = true;
this.loadCarrierList();
},
loadCarrierList() {
getCustomerArchiveList(this.carrierPage.current, this.carrierPage.size, {
...this.carrierQuery,
customerType: '承运商',
}).then(res => {
const page = res.data.data || {};
this.carrierData = page.records || [];
this.carrierPage.total = page.total || 0;
});
},
searchCarrier() {
this.carrierPage.current = 1;
this.loadCarrierList();
},
resetCarrierQuery() {
this.carrierQuery = { fullName: '', accessType: '', approvalStatus: '', status: undefined };
this.searchCarrier();
},
handleCarrierSizeChange(size) {
this.carrierPage.size = size;
this.carrierPage.current = 1;
this.loadCarrierList();
},
selectCarrier(row) {
this.form.carrierCustomerId = row.id;
this.form.deptName = row.fullName;
this.form.shortName = row.shortName;
this.form.mnemonicCode = row.mnemonicCode;
this.carrierDialogVisible = false;
},
formatAccessType(value) {
return value === 'formal' ? '正式' : value === 'temporary' ? '临时' : '-';
},
formatApprovalStatus(value) {
const statusMap = {
draft: '草稿',
reviewing: '审核中',
approved: '审核通过',
rejected: '审核不通过',
};
return statusMap[value] || '-';
},
loadLeaderOptions(tenantId) {
getLeaderList(tenantId).then(res => {
this.leaderOptions = res.data.data || [];
});
},
getDeptCategoryLabel(deptCategory) {
const categoryMap = {
1: '集团',
2: '事业部',
3: '部门',
4: '分公司',
5: '物流园区',
6: '承运商',
};
return categoryMap[Number(deptCategory)] || '-';
},
getLeaderValue(leaderId, field) {
const values = func.split(leaderId);
const result = values
.map(id => this.leaderOptions.find(item => String(item.id) === String(id))?.[field])
.filter(Boolean);
return result.length ? result.join('、') : '-';
},
isRootDept(row) {
return row && (row.parentId === 0 || String(row.parentId) === '0');
},
handleAdd(row) {
this.parentId = row.id;
this.loadParentDeptCode(row.id);
const column = this.findColumn(this.option.column, 'parentId');
column.value = row.id;
column.addDisabled = true;
@@ -248,7 +529,13 @@ export default {
},
rowSave(row, done, loading) {
row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId;
row.deptCode = String(row.deptCode || '').trim();
row.leaderId = func.join(row.leaderId);
if (Number(row.deptCategory) === 6 && !row.carrierCustomerId) {
this.$message.warning('请选择承运商');
loading();
return;
}
add(row).then(
res => {
// 获取新增数据的相关字段
@@ -271,7 +558,13 @@ export default {
},
rowUpdate(row, index, done, loading) {
row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId;
row.deptCode = String(row.deptCode || '').trim();
row.leaderId = func.join(row.leaderId);
if (Number(row.deptCategory) === 6 && !row.carrierCustomerId) {
this.$message.warning('请选择承运商');
loading();
return;
}
update(row).then(
() => {
this.$message({
@@ -288,6 +581,10 @@ export default {
);
},
rowDel(row, index, done) {
if (this.isRootDept(row)) {
this.$message.warning('根节点不能删除');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
@@ -310,13 +607,22 @@ export default {
this.$message.warning('请选择至少一条数据');
return;
}
const deletableRows = this.selectionList.filter(row => !this.isRootDept(row));
if (deletableRows.length === 0) {
this.$message.warning('根节点不能删除');
return;
}
if (deletableRows.length !== this.selectionList.length) {
this.$message.warning('根节点不能删除,已忽略根节点');
}
const deletableIds = deletableRows.map(row => row.id).join(',');
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return remove(this.ids);
return remove(deletableIds);
})
.then(() => {
// 刷新表格数据并重载
@@ -355,6 +661,7 @@ export default {
if (type === 'add') {
this.form.tenantId = this.form.tenantId || this.userInfo.tenantId || website.tenantId;
this.initData(this.form.tenantId);
this.loadParentDeptCode(this.form.parentId);
}
if (type === 'edit') {
this.initData(this.form.tenantId || this.userInfo.tenantId || website.tenantId);
@@ -367,6 +674,7 @@ export default {
if (this.form.parentId === '0') {
this.form.parentId = '';
}
this.loadParentDeptCode(this.form.parentId);
if (this.form.hasOwnProperty('leaderId')) {
this.form.leaderId = func.split(this.form.leaderId);
}
@@ -376,6 +684,8 @@ export default {
},
beforeClose(done) {
this.parentId = '';
this.parentDeptCode = '';
this.parentDept = null;
const column = this.findColumn(this.option.column, 'parentId');
column.value = '';
column.addDisabled = false;
@@ -408,4 +718,15 @@ export default {
};
</script>
<style></style>
<style scoped>
.carrier-search-form {
padding: 12px 12px 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.carrier-pagination {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
</style>

View File

@@ -37,7 +37,7 @@
v-if="permission.user_add && !auditMode"
type="primary"
icon="el-icon-plus"
@click="$refs.crud.rowAdd()"
@click="openUserDialog('add')"
>
</el-button>
<el-button
@@ -134,7 +134,7 @@
<el-link
type="primary"
v-if="this.permission.user_edit"
@click.stop="$refs.crud.rowEdit(row, index)"
@click.stop="openUserDialog('edit', row)"
>编辑
</el-link>
<el-dropdown @command="command => handleDropdownCommand(command, row, index)">
@@ -147,12 +147,6 @@
<el-dropdown-item command="grantRole" v-if="permission.user_role">
角色配置
</el-dropdown-item>
<el-dropdown-item
command="platformConfig"
v-if="userInfo.authority.includes('admin')"
>
平台配置
</el-dropdown-item>
<el-dropdown-item command="authLog" v-if="userInfo.authority.includes('admin')">
认证日志
</el-dropdown-item>
@@ -172,20 +166,62 @@
</template>
</el-dropdown>
</template>
<template #tenantName="{ row }">
<el-tag>{{ row.tenantName }}</el-tag>
</template>
<template #roleName="{ row }">
<el-tag>{{ row.roleName }}</el-tag>
</template>
<template #deptName="{ row }">
<el-tag>{{ row.deptName }}</el-tag>
</template>
<template #userTypeName="{ row }">
<el-tag>{{ row.userTypeName }}</el-tag>
</template>
</avue-crud>
<el-dialog
v-model="userDialog"
:title="userDialogMode === 'add' ? '新增用户' : '编辑用户'"
width="90%"
top="5vh"
append-to-body
destroy-on-close
class="user-edit-dialog"
>
<template #header>
<span class="dialog-title">{{ userDialogMode === 'add' ? '新增用户' : '编辑用户' }}</span>
</template>
<el-form ref="userFormRef" :model="form" :rules="userRules" label-position="right" label-width="120px">
<el-row :gutter="20">
<el-col :span="6"><el-form-item label="账号名" prop="account"><el-input v-model="form.account" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="姓名" prop="realName"><el-input v-model="form.realName" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="手机号" prop="phone"><el-input v-model="form.phone" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="性别"><el-select v-model="form.sex" class="full-width"><el-option label="男" :value="1" /><el-option label="女" :value="2" /><el-option label="未知" :value="3" /></el-select></el-form-item></el-col>
<el-col :span="6"><el-form-item label="岗位" prop="postId"><el-tree-select v-model="form.postId" :data="postList" :props="{ label: 'postName', value: 'id' }" check-strictly class="full-width" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="所属组织" prop="deptId"><el-tree-select v-model="form.deptId" :data="permissionDeptTree" :props="{ label: 'title', value: 'id' }" multiple check-strictly show-checkbox class="full-width" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="工号"><el-input v-model="form.code" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="人员类别"><el-select v-model="form.personCategory" class="full-width"><el-option label="内部员工" :value="1" /><el-option label="承运商" :value="2" /></el-select></el-form-item></el-col>
<el-col :span="18"><el-form-item label="数据权限范围"><el-radio-group v-model="form.dataScopeRange"><el-radio :value="1">仅所属组织</el-radio><el-radio :value="2">全部组织</el-radio><el-radio :value="3">自定义</el-radio></el-radio-group></el-form-item></el-col>
<el-col :span="18" :offset="6"><el-form-item label="数据层级范围"><el-radio-group v-model="form.dataLevelRange"><el-radio :value="1">包含下级</el-radio><el-radio :value="2">仅本级</el-radio></el-radio-group></el-form-item></el-col>
<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="200" show-word-limit /></el-form-item></el-col>
</el-row>
</el-form>
<el-tabs class="user-permission-tabs">
<el-tab-pane label="已拥有角色">
<el-table :data="formRoleList" border>
<el-table-column type="index" label="序号" width="70" />
<el-table-column prop="id" label="角色编号" />
<el-table-column prop="title" label="角色名称" />
<el-table-column label="操作" width="90"><template #default="{ row }"><el-link type="danger" @click="removeFormRole(row.id)">删除</el-link></template></el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane v-if="form.dataScopeRange === 3" label="数据权限">
<el-tree :data="permissionDeptTree" show-checkbox node-key="id" :default-checked-keys="form.dataScopeDeptIds || []" :props="{ label: 'title', children: 'children' }" @check="handleDataScopeCheck" />
</el-tab-pane>
<el-tab-pane label="客商权限">
<div class="permission-toolbar customer-toolbar"><el-checkbox v-model="form.includeNewCustomer" :true-value="1" :false-value="0">包含新增的客商</el-checkbox></div>
<el-table ref="customerTable" :data="customerList" border @selection-change="handleCustomerSelection"><el-table-column type="selection" width="55" /><el-table-column prop="customerCode" label="客商编码" /><el-table-column prop="fullName" label="客商名称" /></el-table>
</el-tab-pane>
</el-tabs>
<template #footer><span class="dialog-footer"><el-button type="primary" @click="submitUserForm">保存</el-button><el-button @click="userDialog = false">返回</el-button><el-button v-if="userDialogMode === 'edit'" @click="resetDialogPassword">重置密码</el-button><el-button @click="handleAddRole">添加角色</el-button></span></template>
</el-dialog>
<!-- 用户角色配置 -->
<el-dialog title="用户角色配置" append-to-body v-model="roleBox" width="345px">
<el-row
@@ -200,8 +236,13 @@
size="small"
@change="handleLinkedChange('treeRole')"
/>
<el-tooltip content="开启后勾选父节点会自动勾选所有子节点,关闭则可独立勾选任意节点" placement="top">
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"><el-icon-question-filled /></el-icon>
<el-tooltip
content="开启后勾选父节点会自动勾选所有子节点,关闭则可独立勾选任意节点"
placement="top"
>
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"
><el-icon-question-filled
/></el-icon>
</el-tooltip>
</span>
<el-button-group>
@@ -246,17 +287,6 @@
</avue-form>
</el-dialog>
<!-- 用户平台配置行级别 -->
<el-dialog title="用户平台配置" append-to-body v-model="platformBox" width="600px">
<avue-form ref="platformFormRef" :option="platformFormOption" v-model="platformForm" />
<template #footer>
<span class="dialog-footer">
<el-button @click="platformBox = false"> </el-button>
<el-button type="primary" @click="submitPlatformConfig"> </el-button>
</span>
</template>
</el-dialog>
<!-- 管理员修改密码 -->
<el-dialog title="修改密码" append-to-body v-model="passwordBox" width="460px">
<el-form
@@ -265,7 +295,7 @@
:rules="passwordRules"
label-width="auto"
>
<el-form-item label="登录账号">
<el-form-item label="账号">
<el-input v-model="passwordForm.account" disabled />
</el-form-item>
<el-form-item label="新密码" prop="password">
@@ -296,10 +326,8 @@
import {
getList,
getUser,
getUserPlatform,
remove,
update,
updatePlatform,
add,
grant,
resetPassword,
@@ -308,6 +336,7 @@ import {
auditRefuse,
setLeader,
getLeaderList,
getCustomerOptions,
} from '@/api/system/user';
import { exportBlob } from '@/api/common';
import { getDeptTree } from '@/api/system/dept';
@@ -315,14 +344,13 @@ import { getRoleTree } from '@/api/system/role';
import { getPostList } from '@/api/system/post';
import { mapGetters } from 'vuex';
import { h } from 'vue';
import website from '@/config/website';
import { getToken } from '@/utils/auth';
import { downloadXls } from '@/utils/util';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import func from '@/utils/func';
import { sensitive } from '@/utils/sensitive';
import { excelOption, platformFormOption, treeOption, userOption } from '@/option/system/user';
import { excelOption, treeOption, userOption } from '@/option/system/user';
import AuthLog from './authlog.vue';
import AuthLock from './authlock.vue';
@@ -331,13 +359,15 @@ export default {
data() {
return {
form: {},
userDialog: false,
userDialogMode: 'add',
search: {},
sensitiveManager: null,
roleBox: false,
roleLinked: false,
roleFormMode: false,
grantUserId: '',
excelBox: false,
platformBox: false,
passwordBox: false,
initFlag: true,
auditMode: false,
@@ -357,11 +387,12 @@ export default {
roleTreeObj: [],
treeDeptId: '',
treeData: [],
permissionDeptTree: [],
postList: [],
customerList: [],
treeOption: treeOption,
userOption: userOption(this),
platformFormOption: platformFormOption,
data: [],
platformForm: {},
passwordForm: {},
passwordRules: {
password: [
@@ -373,16 +404,17 @@ export default {
{ validator: this.validatePassword2, trigger: 'blur' },
],
},
userRules: {
account: [{ required: true, message: '请输入账号名', trigger: 'blur' }],
realName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
deptId: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
},
excelForm: {},
excelOption: excelOption,
};
},
watch: {
'form.tenantId'() {
if (this.form.tenantId !== '' && this.initFlag) {
this.initData(this.form.tenantId);
}
},
'excelForm.isCovered'() {
if (this.excelForm.isCovered !== '') {
const column = this.findColumn(this.excelOption.column, 'excelFile');
@@ -407,14 +439,20 @@ export default {
});
return ids.join(',');
},
formRoleList() {
const selected = new Set(func.split(this.form.roleId || '').map(String));
const roles = [];
const walk = nodes =>
(nodes || []).forEach(node => {
if (selected.has(String(node.id))) roles.push(node);
walk(node.children);
});
walk(this.roleGrantList);
return roles;
},
},
mounted() {
// 非租户模式默认加载管理组数据
if (!website.tenantMode) {
this.initData(website.tenantId);
} else {
this.initData();
}
this.initData(this.userInfo.tenantId || this.website.tenantId);
},
created() {
// 创建脱敏工具实例
@@ -437,8 +475,6 @@ export default {
this.$refs.authLog.openUserAuthLog(row.id, row.name);
} else if (command === 'lockUser') {
this.$refs.authLock.openLockUser(row.id, row.account);
} else if (command === 'platformConfig') {
this.handlePlatformForRow(row);
} else if (command === 'resetPassword') {
this.handleResetForRow(row);
} else if (command === 'setPassword') {
@@ -488,14 +524,17 @@ export default {
},
initData(tenantId) {
getRoleTree(tenantId).then(res => {
this.roleGrantList = res.data.data;
const column = this.findColumn(this.userOption.group, 'roleId');
column.dicData = res.data.data;
});
getDeptTree(tenantId).then(res => {
this.permissionDeptTree = res.data.data;
const column = this.findColumn(this.userOption.group, 'deptId');
column.dicData = res.data.data;
});
getPostList(tenantId).then(res => {
this.postList = res.data.data;
const column = this.findColumn(this.userOption.group, 'postId');
column.dicData = res.data.data;
});
@@ -503,9 +542,18 @@ export default {
const column = this.findColumn(this.userOption.group, 'leaderId');
column.dicData = res.data.data;
});
getCustomerOptions().then(res => {
this.customerList = res.data.data || [];
});
},
submitRole() {
const roleList = this.$refs.treeRole.getCheckedKeys().join(',');
if (this.roleFormMode) {
this.form.roleId = roleList;
this.roleBox = false;
this.roleFormMode = false;
return;
}
grant(this.grantUserId, roleList).then(() => {
this.roleBox = false;
this.$message({
@@ -515,6 +563,98 @@ export default {
this.onLoad(this.page);
});
},
handleAddRole() {
this.roleFormMode = true;
this.roleTreeObj = func.split(this.form.roleId || '');
this.roleLinked = false;
getRoleTree(this.userInfo.tenantId).then(res => {
this.roleGrantList = res.data.data;
this.roleBox = true;
this.$nextTick(() => this.applyCheckedKeys('treeRole', this.roleTreeObj));
});
},
removeFormRole(roleId) {
this.form.roleId = func
.split(this.form.roleId || '')
.filter(id => String(id) !== String(roleId));
},
handleDataScopeCheck(_, checked) {
this.form.dataScopeDeptIds = checked.checkedKeys;
},
handleCustomerSelection(rows) {
this.form.customerIds = rows.map(row => row.id);
},
openUserDialog(mode, row = {}) {
this.userDialogMode = mode;
if (mode === 'add') {
this.form = {
account: '',
realName: '',
phone: '',
email: '',
sex: 3,
postId: '',
deptId: [],
code: '',
personCategory: 1,
dataScopeRange: 1,
dataLevelRange: 1,
remark: '',
roleId: '',
dataScopeDeptIds: [],
customerIds: [],
includeNewCustomer: 0,
};
this.userDialog = true;
this.$nextTick(() => this.$refs.userFormRef && this.$refs.userFormRef.clearValidate());
return;
}
getUser(row.id).then(res => {
const user = res.data.data;
this.form = {
...user,
deptId: func.split(user.deptId || ''),
postId: func.split(user.postId || '')[0] || '',
roleId: func.join(user.roleId || ''),
dataScopeDeptIds: user.dataScopeDeptIds || [],
customerIds: user.customerIds || [],
includeNewCustomer: user.includeNewCustomer || 0,
personCategory: user.personCategory || 1,
dataScopeRange: user.dataScopeRange || 1,
dataLevelRange: user.dataLevelRange || 1,
};
this.sensitiveManager.saveInitialData(this.form);
this.userDialog = true;
this.$nextTick(() => {
this.$refs.userFormRef && this.$refs.userFormRef.clearValidate();
(this.form.customerIds || []).forEach(id => {
const customer = this.customerList.find(item => String(item.id) === String(id));
if (customer) this.$refs.customerTable && this.$refs.customerTable.toggleRowSelection(customer, true);
});
});
});
},
submitUserForm() {
this.$refs.userFormRef.validate(valid => {
if (!valid) return;
const row = {
...this.form,
deptId: func.join(this.form.deptId),
postId: func.join(this.form.postId),
roleId: func.join(this.form.roleId),
leaderId: func.join(this.form.leaderId),
};
const request = this.userDialogMode === 'add' ? add(row) : update(this.sensitiveManager.getSubmitData(row));
request.then(() => {
this.userDialog = false;
this.onLoad(this.page);
this.$message.success('操作成功!');
});
});
},
resetDialogPassword() {
this.handleResetForRow(this.form);
},
rowSave(row, done, loading) {
row.deptId = func.join(row.deptId);
row.roleId = func.join(row.roleId);
@@ -779,23 +919,6 @@ export default {
if (!tree) return;
tree.setCheckedKeys(keys);
},
// ========== 平台配置(行级别) ==========
handlePlatformForRow(row) {
getUserPlatform(row.id).then(res => {
this.platformForm = { ...res.data.data, account: row.account };
this.platformBox = true;
});
},
submitPlatformConfig() {
updatePlatform(
this.platformForm.id,
this.platformForm.userType,
this.platformForm.userExt
).then(() => {
this.platformBox = false;
this.$message({ type: 'success', message: '操作成功!' });
});
},
handlePasswordForRow(row) {
this.passwordForm = {
userId: row.id,
@@ -902,12 +1025,11 @@ export default {
},
onLoad(page, params = {}) {
this.loading = true;
getList(
page.currentPage,
page.pageSize,
Object.assign(params, this.query, { status: this.auditMode ? 0 : 1 }),
this.treeDeptId
).then(res => {
const queryParams = { ...this.query, ...params };
if (this.auditMode) {
queryParams.status = 0;
}
getList(page.currentPage, page.pageSize, queryParams, this.treeDeptId).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
@@ -931,4 +1053,62 @@ export default {
.box .el-scrollbar__wrap {
overflow: scroll;
}
.user-permission-tabs {
width: 100%;
margin-top: 8px;
}
.user-edit-dialog .dialog-title {
display: inline-flex;
align-items: center;
font-size: 16px;
font-weight: 600;
}
.user-edit-dialog .dialog-title::before {
width: 4px;
height: 18px;
margin-right: 8px;
background: #409eff;
content: '';
}
.user-edit-dialog .el-form-item {
margin-bottom: 16px;
}
.user-edit-dialog .el-form-item__label {
white-space: nowrap;
}
.user-edit-dialog .el-radio-group {
white-space: nowrap;
}
.full-width {
width: 100%;
}
.user-permission-tabs .el-tabs__header {
margin-bottom: 12px;
}
.permission-toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.customer-toolbar {
justify-content: flex-start;
}
.avue-dialog .el-form-item__label {
white-space: nowrap;
}
.avue-dialog .el-radio-group {
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<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">
<section v-for="section in sections" :key="section.title" class="ship-ledger__section">
<div class="dialog-section-title">{{ section.title }}</div>
<el-descriptions :column="3" border>
<el-descriptions-item v-for="item in section.items" :key="item.label" :label="item.label">
{{ display(item) }}
</el-descriptions-item>
</el-descriptions>
</section>
<section class="ship-ledger__section">
<div class="dialog-section-title">证书附件</div>
<div class="ship-ledger__images">
<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-empty v-else :image-size="36" description="暂无附件" />
</div>
</div>
</section>
<el-tabs v-model="activeTab" @tab-change="loadRecords">
<el-tab-pane v-for="tab in tabs" :key="tab.name" :label="tab.label" :name="tab.name" />
</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>
</el-dialog>
</template>
<script>
import { getDetail } from '@/api/transportCapacity/transport-ship';
import { getList as insurance } from '@/api/vehicle/insurance-record';
import { getList as violation } from '@/api/vehicle/violation-record';
import { getList as maintenance } from '@/api/vehicle/maintenance-record';
import { getList as maintenancePlan } from '@/api/vehicle/maintenance-plan';
import { getList as accident } from '@/api/vehicle/accident-record';
import { getList as annualInspection } from '@/api/vehicle/annual-inspection-record';
import { getList as change } from '@/api/vehicle/transport-change-record';
import { getList as oilElectric } from '@/api/vehicle/oil-electric-record';
import { getList as otherExpense } from '@/api/vehicle/other-expense-record';
const columns = (...items) => items.map(([label, prop]) => ({ label, prop }));
const tabConfigs = [
['insurance', '保险记录', insurance, columns(['船号', 'vehicleNo'], ['保险类型', 'insuranceType'], ['开始日期', 'startDate'], ['结束日期', 'endDate'], ['保费', 'premium'], ['保单号', 'policyNo'])],
['violation', '违章记录', violation, columns(['船号', 'vehicleNo'], ['驾驶人/船长', 'driverName'], ['类型', 'violationType'], ['事项', 'violationItem'], ['违章时间', 'violationTime'], ['罚款金额', 'fineAmount'])],
['maintenance', '维修记录', maintenance, columns(['船号', 'vehicleNo'], ['维修人', 'maintainer'], ['维修时间', 'maintenanceTime'], ['维修位置', 'location'], ['费用', 'cost'])],
['maintenancePlan', '保养记录', maintenancePlan, columns(['船号', 'vehicleNo'], ['保养人', 'maintainer'], ['保养时间', 'maintenanceTime'], ['保养项目', 'maintenanceItem'], ['费用', 'cost'])],
['accident', '事故记录', accident, columns(['船号', 'vehicleNo'], ['事故时间', 'accidentTime'], ['事故地点', 'accidentLocation'], ['事故类型', 'accidentType'], ['损失金额', 'lossAmount'])],
['annualInspection', '年检记录', annualInspection, columns(['船号', 'vehicleNo'], ['年检日期', 'inspectionDate'], ['年检有效期至', 'inspectionEndDate'], ['年检机构', 'inspectionOrganization'], ['费用', 'cost'])],
['change', '变更记录', change, columns(['船号', 'vehicleNo'], ['变更类型', 'changeType'], ['变更时间', 'changeTime'], ['变更前内容', 'beforeChange'], ['变更后内容', 'afterChange'])],
['equipment', '设备台账', null, columns(['设备编码', 'equipmentCode'], ['设备名称', 'equipmentName'], ['设备类型', 'equipmentType'], ['安装日期', 'installDate'], ['状态', 'status'])],
['oilElectric', '油电台账', oilElectric, columns(['船号', 'vehicleNo'], ['加油/充电日期', 'recordDate'], ['类型', 'energyType'], ['数量', 'quantity'], ['金额', 'amount'])],
['otherExpense', '其他费用记录', otherExpense, columns(['船号', 'vehicleNo'], ['费用日期', 'expenseDate'], ['费用类型', 'expenseType'], ['金额', 'amount'], ['备注', 'remark'])],
].map(([name, label, request, cols]) => ({ name, label, request, columns: cols }));
export default {
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 } }; },
computed: {
tabs() { return tabConfigs; }, activeConfig() { return tabConfigs.find(item => item.name === this.activeTab) || tabConfigs[0]; }, activeColumns() { return this.activeConfig.columns; },
imageFields() { return [['船舶所有权证书', 'ownershipCertImage'], ['内河船舶安全与环保证书', 'safetyCertImage'], ['船舶国籍证书', 'nationalityCertImage'], ['最低安全配员证书', 'safeManningCertImage'], ['船舶营业运输证', 'businessTransportCertImage'], ['光船租赁登记证书', 'leaseContractImage']].map(([label, prop]) => ({ label, prop })); },
imageUrls() { return this.imageFields.map(item => this.shipDetail[item.prop]).filter(Boolean); },
sections() { const s = this.shipDetail; return [
{ title: '船舶基本信息', items: [['船舶所属组织', 'organizationName'], ['船名', 'shipName'], ['安放龙骨日期/建造完工日期', 'constructionDate'], ['总长(m)', 'totalLength'], ['船宽(m)', 'shipWidth'], ['型深(m)', 'moldedDepth'], ['最大船高(m)', 'maxShipHeight'], ['空载吃水(t)', 'lightDraft'], ['满载吃水(t)', 'fullLoadDraft'], ['航区', 'navigationArea']] },
{ title: '船舶证书信息', items: [['登记号码', 'ownershipRegistrationNo'], ['初次登记号码', 'initialRegistrationNo'], ['船舶所有人', 'shipOwner'], ['取得所有权日期', 'ownershipAcquisitionDate'], ['船舶识别号', 'shipIdentifierNo'], ['总吨', 'grossTonnage'], ['净吨', 'netTonnage'], ['船检登记号', 'shipInspectionNo'], ['船舶类型', 'shipType'], ['国籍证书有效期', 'nationalityCertDate'], ['最低安全配员证书有效期', 'safeManningCertDate'], ['起租日期', 'leaseStartDate'], ['终止日期', 'leaseEndDate'], ['船舶承租人', 'shipLessee'], ['营业运输证证书编号', 'businessTransportCertNo'], ['营业运输证发证日期', 'businessTransportCertIssueDate'], ['营业运输证有效期至', 'businessTransportCertEndDate'], ['船舶经营人', 'shipOperator']] },
].map(section => ({ ...section, items: section.items.map(([label, prop]) => ({ label, prop, value: prop === 'constructionDate' ? `${s.keelLayingDate || '-'} / ${s.buildCompletionDate || '-'}` : prop === 'nationalityCertDate' ? `${s.nationalityCertStartDate || '-'}${s.nationalityCertEndDate || '-'}` : prop === 'safeManningCertDate' ? `${s.safeManningCertStartDate || '-'}${s.safeManningCertEndDate || '-'}` : s[prop] })) })); },
},
watch: { modelValue(value) { if (value) this.initialize(); } },
methods: {
display(item) { return item.value === null || item.value === undefined || item.value === '' ? '-' : item.value; },
async initialize() { this.activeTab = 'insurance'; this.page.current = 1; this.shipDetail = { ...this.ship }; if (this.ship.id) { this.detailLoading = true; try { const res = await getDetail(this.ship.id); this.shipDetail = { ...this.ship, ...(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.shipDetail.shipName || this.ship.shipName, 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>
<style lang="scss" scoped>
.ship-ledger__section { margin-bottom: 16px; }
.ship-ledger__images { display: flex; gap: 16px; overflow-x: auto; padding: 12px 0; }
.ship-ledger__image-item { width: 150px; flex: 0 0 150px; text-align: center; color: #606266; }
.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-empty) { height: 110px; border: 1px solid #eff1f7; }
.ship-ledger__pagination { display: flex; justify-content: flex-end; margin-top: 16px; }
</style>

View File

@@ -0,0 +1,141 @@
<template>
<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="vehicle-ledger">
<section class="vehicle-ledger__overview">
<div class="vehicle-ledger__title">
<span>{{ vehicleDetail.plateNo || vehicle.plateNo }}</span>
<el-tag :type="plateColorType">{{ vehicleDetail.plateColor || '-' }}</el-tag>
<el-tag type="primary">{{ vehicleDetail.businessRelation || '-' }}</el-tag>
</div>
<el-descriptions :column="4" border>
<el-descriptions-item label="所属组织">{{ vehicleDetail.organizationName || '-' }}</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="核定载质量(KG)">{{ vehicleDetail.approvedLoadKg ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="准牵引总质量(KG)">{{ vehicleDetail.tractionMassKg ?? '-' }}</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>
<div class="vehicle-ledger__images">
<div v-for="item in imageFields" :key="item.prop" class="vehicle-ledger__image-item">
<span>{{ item.label }}</span>
<el-image
v-if="vehicleDetail[item.prop]"
:src="vehicleDetail[item.prop]"
:preview-src-list="imageUrls"
preview-teleported
fit="cover"
/>
<el-empty v-else :image-size="36" description="暂无图片" />
</div>
</div>
</section>
<el-tabs 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-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>
</el-dialog>
</template>
<script>
import { getDetail } from '@/api/transportCapacity/transport-vehicle';
import { getList as getInsuranceList } from '@/api/vehicle/insurance-record';
import { getList as getViolationList } from '@/api/vehicle/violation-record';
import { getList as getMaintenanceList } from '@/api/vehicle/maintenance-record';
import { getList as getMaintenancePlanList } from '@/api/vehicle/maintenance-plan';
import { getList as getTireList } from '@/api/vehicle/tire-replacement-record';
import { getList as getAccidentList } from '@/api/vehicle/accident-record';
import { getList as getAnnualInspectionList } from '@/api/vehicle/annual-inspection-record';
import { getList as getMileageList } from '@/api/vehicle/mileage-record';
import { getList as getChangeList } from '@/api/vehicle/transport-change-record';
import { getList as getOilElectricList } from '@/api/vehicle/oil-electric-record';
import { getList as getEtcList } from '@/api/vehicle/etc-record';
import { getList as getOtherExpenseList } from '@/api/vehicle/other-expense-record';
const cols = (...items) => items.map(([label, prop, width]) => ({ label, prop, width }));
const tabConfigs = [
['insurance', '保险记录', getInsuranceList, cols(['车牌号', 'vehicleNo'], ['保险类型', 'insuranceType'], ['开始日期', 'startDate'], ['结束日期', 'endDate'], ['保费', 'premium'], ['保单号', 'policyNo'])],
['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 }));
export default {
props: { modelValue: Boolean, vehicle: { type: Object, default: () => ({}) } },
emits: ['update:modelValue'],
data() {
return { detailLoading: false, tableLoading: false, vehicleDetail: {}, activeTab: 'insurance', records: [], page: { current: 1, size: 10, total: 0 } };
},
computed: {
tabs() { return tabConfigs; },
activeConfig() { return tabConfigs.find(item => item.name === this.activeTab) || tabConfigs[0]; },
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(); } },
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 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>
<style lang="scss" scoped>
.vehicle-ledger__overview { padding: 16px; border: 1px solid #eff1f7; }
.vehicle-ledger__title { display: flex; align-items: center; gap: 12px; 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; }
.vehicle-ledger__pagination { display: flex; justify-content: flex-end; margin-top: 16px; }
</style>

View File

@@ -20,7 +20,6 @@
<el-button
type="primary"
icon="el-icon-plus"
plain
v-if="hasPermission('driver_add')"
@click="openDriver()"
>新增
@@ -42,23 +41,23 @@
>批量删除
</el-button>
</template>
<template #menu-right>
<el-radio-group
v-model="query.expireStatus"
class="driver-stat"
@change="handleExpireChange"
>
<el-radio-button label="">全部{{ expiryStat.total }}</el-radio-button>
<el-radio-button label="within30"
>30天内到期{{ expiryStat.within30 }}</el-radio-button
<template #expireStatus-search>
<div class="driver-page__expiry-tags">
<el-check-tag
v-for="item in expiryTagOptions"
:key="item.value"
:checked="query.expireStatus === item.value"
@change="handleExpireChange(item.value)"
>
<el-radio-button label="expired">已到期{{ expiryStat.expired }}</el-radio-button>
</el-radio-group>
{{ item.label }}{{ expiryStat[item.statKey] || 0 }}
</el-check-tag>
</div>
</template>
<template #driverName="{ row }">
<el-button type="primary" link @click="openDriver(row, true)">
{{ row.driverName }}
</el-button>
<div class="driver-page__identity">
<el-link type="primary" @click="openDriver(row, true)">{{ row.driverName }}</el-link>
<span>{{ row.idCardNo || '-' }}</span>
</div>
</template>
<template #status="{ row }">
<el-tag :type="row.status === 1 ? 'primary' : 'info'">
@@ -108,7 +107,7 @@
:title="dialogTitle"
append-to-body
v-model="driverBox"
width="92%"
width="90%"
top="4vh"
class="driver-dialog"
@closed="resetDriver"
@@ -125,7 +124,7 @@
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="司机姓名" prop="driverName">
<el-input v-model="driverForm.driverName" maxlength="20" show-word-limit />
<el-input v-model="driverForm.driverName" maxlength="10" show-word-limit />
</el-form-item>
</el-col>
<el-col :span="8">
@@ -346,8 +345,6 @@
v-model="driverForm.qualificationType"
clearable
filterable
allow-create
default-first-option
>
<el-option
v-for="item in qualificationTypeOptions"
@@ -419,32 +416,35 @@
<section-card title="司机联系信息">
<el-row :gutter="18">
<el-col :span="8">
<el-col :span="6">
<el-form-item label="司机类型" prop="driverType">
<el-select v-model="driverForm.driverType" filterable>
<el-option label="自有" value="自有" />
<el-option label="外协" value="外协" />
<el-option label="承运管理员" value="承运管理员" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-form-item label="手机号" prop="mobile">
<el-input v-model="driverForm.mobile" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-form-item label="紧急联系人姓名" prop="emergencyContactName">
<el-input v-model="driverForm.emergencyContactName" maxlength="20" />
<el-input
v-model="driverForm.emergencyContactName"
maxlength="20"
@input="driverForm.emergencyContactName = removeDigits($event)"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="18">
<el-col :span="8">
<el-col :span="6">
<el-form-item label="紧急联系人手机号" prop="emergencyContactMobile">
<el-input v-model="driverForm.emergencyContactMobile" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-form-item label="与联系人关系" prop="contactRelation">
<el-select
v-model="driverForm.contactRelation"
@@ -462,7 +462,7 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-form-item label="所属组织" prop="organizationName">
<el-select v-model="driverForm.organizationName" clearable filterable>
<el-option
@@ -503,6 +503,7 @@ import {
recognitionTransportCertificates,
} from '@/api/transportCapacity/driver';
import { getDeptTree } from '@/api/system/dept';
import { getDictionary } from '@/api/system/dictbiz';
import { getLazyTree } from '@/api/base/region';
import { exportBlob } from '@/api/common';
import { getToken } from '@/utils/auth';
@@ -640,18 +641,8 @@ export default {
],
educationOptions: ['初中', '高中', '中专', '大专', '本科', '硕士及以上'],
drivingTypeOptions: ['A1', 'A2', 'A3', 'B1', 'B2', 'C1', 'C2'],
qualificationTypeOptions: [
'放射品押运员',
'道路运输从业资格证',
'危险货物运输从业资格证',
'A1',
'A2',
'B1',
'B2',
'C1',
'C2',
],
relationOptions: ['父母', '配偶', '子女', '兄弟姐妹', '朋友', '同事'],
qualificationTypeOptions: [],
relationOptions: ['父母', '配偶', '子女', '兄弟姐妹', '朋友', '同事', '其他'],
organizationOptions: [],
regionOptions: [],
formRules: {
@@ -687,6 +678,7 @@ export default {
mobile: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
emergencyContactName: [
{ required: true, message: '请输入紧急联系人姓名', trigger: 'blur' },
{ validator: this.validateEmergencyContactName, trigger: 'blur' },
],
emergencyContactMobile: [
{ required: true, message: '请输入紧急联系人手机号', trigger: 'blur' },
@@ -727,10 +719,18 @@ export default {
emitPath: true,
};
},
expiryTagOptions() {
return [
{ label: '全部', value: '', statKey: 'total' },
{ label: '30天内到期', value: 'within30', statKey: 'within30' },
{ label: '已到期', value: 'expired', statKey: 'expired' },
];
},
},
created() {
this.initDeptTree();
this.initRegionOptions();
this.initQualificationTypeOptions();
},
methods: {
hasPermission(code) {
@@ -741,6 +741,16 @@ export default {
this.organizationOptions = this.formatDeptOptions(res.data.data || []);
const column = this.findColumn(this.option.column, 'organizationName');
column.dicData = this.organizationOptions;
if (this.driverBox && !this.driverForm.id && !this.driverForm.organizationName) {
this.driverForm.organizationName = this.getCurrentOrganizationName();
}
});
},
initQualificationTypeOptions() {
getDictionary({ code: 'type_of_cert' }).then(res => {
this.qualificationTypeOptions = (res.data.data || [])
.map(item => item.dictValue)
.filter(Boolean);
});
},
formatDeptOptions(tree = [], level = 0) {
@@ -750,6 +760,8 @@ export default {
result.push({
label: `${' '.repeat(level)}${label}`,
value: label,
rawLabel: label,
id: item.id || item.value,
});
if (item.children && item.children.length) {
result.push(...this.formatDeptOptions(item.children, level + 1));
@@ -757,6 +769,13 @@ export default {
});
return result;
},
getCurrentOrganizationName() {
const currentDeptId = this.userInfo.deptId || this.userInfo.dept_id;
const currentDept = this.organizationOptions.find(
item => String(item.id) === String(currentDeptId)
);
return currentDept?.rawLabel || this.userInfo.deptName || this.userInfo.dept_name || '';
},
initRegionOptions() {
getLazyTree().then(res => {
const regions = this.buildRegionTree(res.data.data || []);
@@ -886,10 +905,21 @@ export default {
}
callback();
},
validateEmergencyContactName(rule, value, callback) {
if (/\d/.test(value || '')) {
callback(new Error('紧急联系人姓名不能包含数字'));
return;
}
callback();
},
removeDigits(value = '') {
return String(value).replace(/\d/g, '');
},
openDriver(row, readonly = false) {
this.readonly = readonly;
if (!row?.id) {
this.driverForm = emptyForm();
this.driverForm.organizationName = this.getCurrentOrganizationName();
this.driverBox = true;
return;
}
@@ -922,9 +952,9 @@ export default {
this.setImage(prop, url);
this.recognizeIdCard(url);
},
handleDrivingLicenseUploadSuccess(prop, url, file = {}) {
handleDrivingLicenseUploadSuccess(prop, url) {
this.setImage(prop, url);
this.drivingLicenseUploads[prop] = this.getUploadRecognitionKey(file, url);
this.drivingLicenseUploads[prop] = url;
this.recognizeDrivingLicense();
},
recognizeIdCard(url = '') {
@@ -977,10 +1007,10 @@ export default {
buildDrivingLicenseRecognitionCertificates() {
return ['drivingLicenseFront', 'drivingLicenseBack']
.map(prop => {
const objectKey = this.drivingLicenseUploads[prop] || this.driverForm[prop];
if (!objectKey) return null;
const url = this.drivingLicenseUploads[prop] || this.driverForm[prop];
if (!url) return null;
return {
driverLicenseUrl: objectKey,
driverLicenseUrl: url,
};
})
.filter(Boolean);
@@ -1047,19 +1077,6 @@ export default {
});
});
},
getUploadRecognitionKey(file = {}, url = '') {
return (
file.objectKey ||
file.key ||
file.fileName ||
file.name ||
file.link ||
file.url ||
file.domain ||
url ||
''
);
},
applyIdCardRecognition(data = {}) {
const driverName = data.name || data.driverName || this.getOcrValue(data, ['name', '姓名']);
const idCardNo = String(
@@ -1207,7 +1224,8 @@ export default {
});
});
},
handleExpireChange() {
handleExpireChange(expireStatus) {
this.query.expireStatus = expireStatus;
this.page.currentPage = 1;
this.onLoad(this.page);
},
@@ -1323,8 +1341,24 @@ export default {
<style lang="scss" scoped>
.driver-page {
.driver-stat {
flex-shrink: 0;
&__identity {
display: flex;
flex-direction: column;
gap: 4px;
line-height: 20px;
}
&__expiry-tags {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
width: 100%;
:deep(.el-check-tag) {
margin: 0;
white-space: nowrap;
}
}
:deep(.el-table th .cell),
@@ -1410,9 +1444,8 @@ export default {
@media (max-width: 1200px) {
.driver-page {
.driver-stat {
display: flex;
flex-wrap: wrap;
&__expiry-tags {
flex: 1;
}
}
}

View File

@@ -20,7 +20,6 @@
<el-button
type="primary"
icon="el-icon-plus"
plain
v-if="hasPermission('transport_ship_add')"
@click="openShip()"
>
@@ -45,14 +44,17 @@
批量删除
</el-button>
</template>
<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
<template #expireStatus-search>
<div class="transport-ship-page__expiry-tags">
<el-check-tag
v-for="item in expiryTagOptions"
:key="item.value"
:checked="query.expireStatus === item.value"
@change="handleExpireChange(item.value)"
>
<el-radio-button label="expired">已到期{{ expiryStat.expired }}</el-radio-button>
</el-radio-group>
{{ item.label }}{{ expiryStat[item.statKey] || 0 }}
</el-check-tag>
</div>
</template>
<template #shipName="{ row }">
<el-button type="primary" link @click="openShip(row, true)">
@@ -70,14 +72,22 @@
</span>
</template>
<template #safeManningCertEndDate="{ row }">
<span :class="getDateClass(row, 'safeManningCertEndDate', 'safeManningCertLongTerm')">
<span
:class="
getCertificateExpiryClass(row, 'safeManningCertEndDate', 'safeManningCertLongTerm')
"
>
{{ formatEndDate(row.safeManningCertEndDate, row.safeManningCertLongTerm) }}
</span>
</template>
<template #businessTransportCertEndDate="{ row }">
<span
:class="
getDateClass(row, 'businessTransportCertEndDate', 'businessTransportCertLongTerm')
getCertificateExpiryClass(
row,
'businessTransportCertEndDate',
'businessTransportCertLongTerm'
)
"
>
{{ formatEndDate(row.businessTransportCertEndDate, row.businessTransportCertLongTerm) }}
@@ -95,7 +105,7 @@
<el-link
type="primary"
v-if="hasPermission('transport_ship_view')"
@click="openShip(row, true)"
@click="openShipLedger(row)"
>
综合台账
</el-link>
@@ -109,6 +119,8 @@
</template>
</avue-crud>
<ship-ledger v-model="shipLedgerBox" :ship="ledgerShip" />
<el-dialog
:title="dialogTitle"
append-to-body
@@ -147,7 +159,7 @@
</el-col>
<el-col :span="8">
<el-form-item label="船名" prop="shipName">
<el-input v-model="shipForm.shipName" maxlength="50" show-word-limit />
<el-input v-model="shipForm.shipName" maxlength="20" show-word-limit />
</el-form-item>
</el-col>
<el-col :span="8">
@@ -173,64 +185,64 @@
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="总长" prop="totalLength">
<el-input-number
<el-input
v-model="shipForm.totalLength"
:min="0"
:precision="2"
:controls="false"
/>
@input="normalizeNonNegativeDecimal('totalLength')"
>
<template #suffix>m</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="船宽" prop="shipWidth">
<el-input-number
<el-input
v-model="shipForm.shipWidth"
:min="0"
:precision="2"
:controls="false"
/>
@input="normalizeNonNegativeDecimal('shipWidth')"
>
<template #suffix>m</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="型深" prop="moldedDepth">
<el-input-number
<el-input
v-model="shipForm.moldedDepth"
:min="0"
:precision="2"
:controls="false"
/>
@input="normalizeNonNegativeDecimal('moldedDepth')"
>
<template #suffix>m</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="最大船高" prop="maxShipHeight">
<el-input-number
<el-input
v-model="shipForm.maxShipHeight"
:min="0"
:precision="2"
:controls="false"
/>
@input="normalizeNonNegativeDecimal('maxShipHeight')"
>
<template #suffix>m</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="空载吃水" prop="lightDraft">
<el-input-number
<el-input
v-model="shipForm.lightDraft"
:min="0"
:precision="2"
:controls="false"
/>
@input="normalizeNonNegativeDecimal('lightDraft')"
>
<template #suffix>t</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="满载吃水" prop="fullLoadDraft">
<el-input-number
<el-input
v-model="shipForm.fullLoadDraft"
:min="0"
:precision="2"
:controls="false"
/>
@input="normalizeNonNegativeDecimal('fullLoadDraft')"
>
<template #suffix>t</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
@@ -350,7 +362,12 @@
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="船舶类型" prop="shipType">
<el-input v-model="shipForm.shipType" maxlength="50" />
<el-select v-model="shipForm.shipType" placeholder="请选择船舶类型" clearable>
<el-option label="散货船" value="散货船" />
<el-option label="集装箱船" value="集装箱船" />
<el-option label="杂货船" value="杂货船" />
<el-option label="油船" value="油船" />
</el-select>
</el-form-item>
</el-col>
</el-row>
@@ -578,6 +595,7 @@ import { getToken } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
import { downloadXls } from '@/utils/util';
import ImageUploadField from '@/components/image-upload-field/main.vue';
import ShipLedger from './components/ship-ledger.vue';
const emptyForm = () => ({
id: '',
@@ -627,7 +645,7 @@ const emptyForm = () => ({
});
export default {
components: { ImageUploadField, InfoFilled },
components: { ImageUploadField, InfoFilled, ShipLedger },
data() {
return {
option,
@@ -642,13 +660,18 @@ export default {
page: { pageSize: 10, currentPage: 1, total: 0 },
expiryStat: { total: 0, within30: 0, expired: 0 },
shipBox: false,
shipLedgerBox: false,
ledgerShip: {},
readonly: false,
shipForm: emptyForm(),
uploadHeaders: getUploadHeaders(),
organizationOptions: [],
formRules: {
organizationName: [{ required: true, message: '请选择船舶所属组织', trigger: 'change' }],
shipName: [{ required: true, message: '请输入船名', trigger: 'blur' }],
shipName: [
{ required: true, message: '请输入船名', trigger: 'blur' },
{ max: 20, message: '船名不能超过20个字符', trigger: 'blur' },
],
keelLayingDate: [{ validator: this.validateKeelCompletionDate, trigger: 'change' }],
shipIdentifierNo: [{ required: true, message: '请输入船舶识别号', trigger: 'blur' }],
totalLength: [{ validator: this.validateNonNegativeField, trigger: 'change' }],
@@ -664,7 +687,7 @@ export default {
grossTonnage: [{ required: true, message: '请输入总吨', trigger: 'change' }],
netTonnage: [{ required: true, message: '请输入净吨', trigger: 'change' }],
shipInspectionNo: [{ required: true, message: '请输入船检登记号', trigger: 'blur' }],
shipType: [{ required: true, message: '请输入船舶类型', trigger: 'blur' }],
shipType: [{ required: true, message: '请选择船舶类型', trigger: 'change' }],
nationalityCertStartDate: [
{ validator: this.validateNationalityCertDate, trigger: 'change' },
],
@@ -705,6 +728,13 @@ export default {
if (this.readonly) return '船舶综合台账';
return this.shipForm.id ? '修改船舶' : '新增船舶';
},
expiryTagOptions() {
return [
{ label: '全部', value: '', statKey: 'total' },
{ label: '30天内到期', value: 'within30', statKey: 'within30' },
{ label: '已到期', value: 'expired', statKey: 'expired' },
];
},
},
created() {
this.initDeptTree();
@@ -718,6 +748,9 @@ export default {
this.organizationOptions = this.flattenDeptOptions(res.data.data || []);
const column = this.findColumn(this.option.column, 'organizationName');
column.dicData = this.organizationOptions;
if (this.shipBox && !this.shipForm.id && !this.shipForm.organizationName) {
this.shipForm.organizationName = this.getCurrentOrganizationName();
}
});
},
flattenDeptOptions(tree = [], level = 0) {
@@ -727,6 +760,8 @@ export default {
result.push({
label: `${' '.repeat(level)}${label}`,
value: label,
rawLabel: label,
id: item.id || item.value,
});
if (item.children && item.children.length) {
result.push(...this.flattenDeptOptions(item.children, level + 1));
@@ -734,9 +769,22 @@ export default {
});
return result;
},
getCurrentOrganizationName() {
const currentDeptId = this.userInfo.deptId || this.userInfo.dept_id;
const currentDept = this.organizationOptions.find(
item => String(item.id) === String(currentDeptId)
);
return currentDept?.rawLabel || this.userInfo.deptName || this.userInfo.dept_name || '';
},
validateField(prop) {
this.$refs.shipForm?.validateField(prop);
},
normalizeNonNegativeDecimal(prop) {
const value = String(this.shipForm[prop] ?? '').replace(/[^\d.]/g, '');
const [integerPart = '', ...decimalParts] = value.split('.');
const decimalPart = decimalParts.join('').slice(0, 2);
this.shipForm[prop] = decimalParts.length ? `${integerPart}.${decimalPart}` : integerPart;
},
validateNonNegativeField(rule, value, callback) {
if (value !== undefined && value !== null && value < 0) {
callback(new Error('数值不能小于0'));
@@ -819,6 +867,7 @@ export default {
this.readonly = readonly;
if (!row?.id) {
this.shipForm = emptyForm();
this.shipForm.organizationName = this.getCurrentOrganizationName();
this.shipBox = true;
return;
}
@@ -827,6 +876,10 @@ export default {
this.shipBox = true;
});
},
openShipLedger(row) {
this.ledgerShip = { ...row };
this.shipLedgerBox = true;
},
resetShip() {
this.shipForm = emptyForm();
this.readonly = false;
@@ -882,7 +935,8 @@ export default {
});
});
},
handleExpireChange() {
handleExpireChange(expireStatus) {
this.query.expireStatus = expireStatus;
this.page.currentPage = 1;
this.onLoad(this.page);
},
@@ -978,14 +1032,28 @@ export default {
if (endDate.diff(today, 'day') <= 30) return 'date-warning';
return '';
},
getCertificateExpiryClass(row, dateProp, longTermProp) {
if (row[longTermProp] === 1 || !row[dateProp]) return '';
const endDate = this.$dayjs(row[dateProp]);
return endDate.isValid() && endDate.diff(this.$dayjs(), 'day') <= 30 ? 'date-danger' : '';
},
},
};
</script>
<style lang="scss" scoped>
.transport-ship-page {
.ship-stat {
flex-shrink: 0;
&__expiry-tags {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
width: 100%;
:deep(.el-check-tag) {
margin: 0;
white-space: nowrap;
}
}
:deep(.el-table th .cell),

View File

@@ -20,7 +20,6 @@
<el-button
type="primary"
icon="el-icon-plus"
plain
v-if="hasPermission('transport_vehicle_add')"
@click="openVehicle()"
>新增
@@ -42,18 +41,17 @@
>批量删除
</el-button>
</template>
<template #menu-right>
<el-radio-group
v-model="query.expireStatus"
class="vehicle-stat"
@change="handleExpireChange"
>
<el-radio-button label="">全部{{ expiryStat.total }}</el-radio-button>
<el-radio-button label="within30"
>30天内到期{{ expiryStat.within30 }}</el-radio-button
<template #expireStatus-search>
<div class="transport-vehicle-page__expiry-tags">
<el-check-tag
v-for="item in expiryTagOptions"
:key="item.value"
:checked="query.expireStatus === item.value"
@change="handleExpireChange(item.value)"
>
<el-radio-button label="expired">已到期{{ expiryStat.expired }}</el-radio-button>
</el-radio-group>
{{ item.label }}{{ expiryStat[item.statKey] || 0 }}
</el-check-tag>
</div>
</template>
<template #plateNo="{ row }">
<el-button type="primary" link @click="openVehicle(row, true)">
@@ -65,6 +63,14 @@
{{ row.status === 1 ? '启用' : '停用' }}
</el-tag>
</template>
<template #boundDriver>
<span>-</span>
</template>
<template #certificationStatus="{ row }">
<el-tag :type="getCertificationStatus(row).type">
{{ getCertificationStatus(row).label }}
</el-tag>
</template>
<template #drivingLicenseEndDate="{ row }">
<span :class="getDateClass(row, 'drivingLicenseEndDate', 'drivingLicenseLongTerm')">
{{ formatEndDate(row.drivingLicenseEndDate, row.drivingLicenseLongTerm) }}
@@ -95,6 +101,10 @@
>
修改
</el-link>
<el-link type="primary" @click="openVehicleLedger(row)">综合台账</el-link>
<el-link type="primary" v-if="row.certificationStatus === 0" @click="openCertificationAudit(row)">
审核
</el-link>
<el-link
type="primary"
v-if="hasPermission('transport_vehicle_status')"
@@ -105,6 +115,26 @@
</template>
</avue-crud>
<vehicle-ledger v-model="vehicleLedgerBox" :vehicle="ledgerVehicle" />
<el-dialog v-model="certificationAuditBox" title="车辆认证审核" width="92%" top="4vh">
<el-descriptions :column="3" border>
<el-descriptions-item v-for="item in auditDetailItems" :key="item.label" :label="item.label">
{{ formatAuditValue(item) }}
</el-descriptions-item>
</el-descriptions>
<el-form v-if="showRejectReason" class="certification-audit__reason" label-position="right" label-width="auto">
<el-form-item label="驳回原因" required>
<el-input v-model="certificationRejectReason" type="textarea" maxlength="200" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="certificationAuditBox = false">取消</el-button>
<el-button type="danger" @click="handleCertificationReject">认证驳回</el-button>
<el-button type="primary" @click="handleCertificationApprove">认证通过</el-button>
</template>
</el-dialog>
<el-dialog
:title="dialogTitle"
append-to-body
@@ -202,29 +232,26 @@
<div class="dimension-group">
<div class="dimension-group__item">
<span class="dimension-group__label"></span>
<el-input-number
<el-input
v-model="vehicleForm.outerLength"
:min="0"
:precision="0"
controls-position="right"
inputmode="numeric"
@input="vehicleForm.outerLength = digitsOnly($event)"
/>
</div>
<div class="dimension-group__item">
<span class="dimension-group__label"></span>
<el-input-number
<el-input
v-model="vehicleForm.outerWidth"
:min="0"
:precision="0"
controls-position="right"
inputmode="numeric"
@input="vehicleForm.outerWidth = digitsOnly($event)"
/>
</div>
<div class="dimension-group__item">
<span class="dimension-group__label"></span>
<el-input-number
<el-input
v-model="vehicleForm.outerHeight"
:min="0"
:precision="0"
controls-position="right"
inputmode="numeric"
@input="vehicleForm.outerHeight = digitsOnly($event)"
/>
</div>
</div>
@@ -234,21 +261,19 @@
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="核定载质量(KG)" prop="approvedLoadKg">
<el-input-number
<el-input
v-model="vehicleForm.approvedLoadKg"
:min="0"
:precision="0"
controls-position="right"
inputmode="numeric"
@input="vehicleForm.approvedLoadKg = digitsOnly($event)"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="准牵引总质量(KG)" prop="tractionMassKg">
<el-input-number
<el-input
v-model="vehicleForm.tractionMassKg"
:min="0"
:precision="0"
controls-position="right"
inputmode="numeric"
@input="vehicleForm.tractionMassKg = digitsOnly($event)"
/>
</el-form-item>
</el-col>
@@ -510,6 +535,7 @@ import {
submit,
remove,
changeStatus,
auditCertification,
getExpiryStat,
recognitionTransportCertificates,
} from '@/api/transportCapacity/transport-vehicle';
@@ -519,6 +545,7 @@ import { getToken } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
import { downloadXls } from '@/utils/util';
import ImageUploadField from '@/components/image-upload-field/main.vue';
import VehicleLedger from './components/vehicle-ledger.vue';
const emptyForm = () => ({
id: '',
@@ -600,6 +627,7 @@ const plateColorOptions = ['黄牌', '蓝牌', '绿牌'].map(item => ({ label: i
export default {
components: {
ImageUploadField,
VehicleLedger,
},
data() {
return {
@@ -608,7 +636,9 @@ export default {
query: {
expireStatus: '',
},
searchForm: {},
searchForm: {
businessRelation: '',
},
loading: true,
submitLoading: false,
data: [],
@@ -625,6 +655,12 @@ export default {
expired: 0,
},
vehicleBox: false,
vehicleLedgerBox: false,
ledgerVehicle: {},
certificationAuditBox: false,
certificationAuditForm: {},
certificationRejectReason: '',
showRejectReason: false,
readonly: false,
vehicleForm: emptyForm(),
vehicleCertificateUploads: {},
@@ -644,7 +680,10 @@ export default {
energyTypeOptions: ['柴油', '汽油', '新能源', '天然气'],
formRules: {
organizationName: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
plateNo: [{ validator: this.validatePlateNo, trigger: 'change' }],
plateNo: [
{ required: true, message: '请输入车牌号', trigger: 'change' },
{ validator: this.validatePlateNo, trigger: 'change' },
],
vehicleType: [{ required: true, message: '请选择车辆类型', trigger: 'change' }],
approvedLoadKg: [{ required: true, message: '请输入核定载质量', trigger: 'blur' }],
businessRelation: [{ required: true, message: '请选择业务关系', trigger: 'change' }],
@@ -682,6 +721,25 @@ export default {
}
return this.vehicleForm.id ? '修改车辆' : '新增车辆';
},
auditDetailItems() {
return [
['所属组织', 'organizationName'], ['车牌号', 'plateNo'], ['车牌颜色', 'plateColor'],
['车辆类型', 'vehicleType'], ['外廓长度(mm)', 'outerLength'], ['外廓宽度(mm)', 'outerWidth'],
['外廓高度(mm)', 'outerHeight'], ['核定载质量(KG)', 'approvedLoadKg'], ['准牵引总质量(KG)', 'tractionMassKg'],
['业务关系', 'businessRelation'], ['能源类型', 'energyType'], ['强制报废日期', 'compulsoryScrapDate'],
['海关备案号', 'customsRecordNo'], ['行驶证档案编号', 'drivingLicenseNo'], ['行驶证有效期起', 'drivingLicenseStartDate'],
['行驶证有效期止', 'drivingLicenseEndDate'], ['道路运输证号', 'roadTransportCertNo'], ['道路运输证有效期起', 'roadTransportCertStartDate'],
['道路运输证有效期止', 'roadTransportCertEndDate'], ['道路运输年审有效期', 'annualReviewEndDate'],
['机动车登记编号', 'registrationNo'], ['机动车登记日期', 'registrationDate'], ['备注', 'remark'],
].map(([label, prop]) => ({ label, prop }));
},
expiryTagOptions() {
return [
{ label: '全部', value: '', statKey: 'total' },
{ label: '30天内到期', value: 'within30', statKey: 'within30' },
{ label: '已到期', value: 'expired', statKey: 'expired' },
];
},
},
created() {
this.initDeptTree();
@@ -695,6 +753,9 @@ export default {
this.organizationOptions = this.flattenDeptOptions(res.data.data || []);
const column = this.findColumn(this.option.column, 'organizationName');
column.dicData = this.organizationOptions;
if (this.vehicleBox && !this.vehicleForm.id && !this.vehicleForm.organizationName) {
this.vehicleForm.organizationName = this.getCurrentOrganizationName();
}
});
},
flattenDeptOptions(tree = [], level = 0) {
@@ -704,6 +765,8 @@ export default {
result.push({
label: `${' '.repeat(level)}${label}`,
value: label,
rawLabel: label,
id: item.id || item.value,
});
if (item.children && item.children.length) {
result.push(...this.flattenDeptOptions(item.children, level + 1));
@@ -711,6 +774,16 @@ export default {
});
return result;
},
getCurrentOrganizationName() {
const currentDeptId = this.userInfo.deptId || this.userInfo.dept_id;
const currentDept = this.organizationOptions.find(
item => String(item.id) === String(currentDeptId)
);
return currentDept?.rawLabel || this.userInfo.deptName || this.userInfo.dept_name || '';
},
digitsOnly(value = '') {
return String(value).replace(/\D/g, '');
},
validatePlateNo(rule, value, callback) {
const plateProvince = String(this.vehicleForm.plateProvince || '').trim();
const plateNoBody = String(this.vehicleForm.plateNoBody || '')
@@ -738,6 +811,7 @@ export default {
this.readonly = readonly;
if (!row?.id) {
this.vehicleForm = emptyForm();
this.vehicleForm.organizationName = this.getCurrentOrganizationName();
this.syncPlateNo(false);
this.vehicleBox = true;
return;
@@ -752,6 +826,38 @@ export default {
this.vehicleBox = true;
});
},
openVehicleLedger(row) {
this.ledgerVehicle = { ...row };
this.vehicleLedgerBox = true;
},
openCertificationAudit(row) {
getDetail(row.id).then(res => {
this.certificationAuditForm = { ...(res.data.data || {}) };
this.certificationRejectReason = '';
this.showRejectReason = false;
this.certificationAuditBox = true;
});
},
formatAuditValue(item) {
const value = this.certificationAuditForm[item.prop];
return value === undefined || value === null || value === '' ? '-' : value;
},
handleCertificationApprove() {
auditCertification(this.certificationAuditForm.id, 1).then(() => {
this.$message.success('认证已通过');
this.certificationAuditBox = false;
this.onLoad(this.page);
});
},
handleCertificationReject() {
if (!this.showRejectReason) { this.showRejectReason = true; return; }
if (!this.certificationRejectReason.trim()) { this.$message.warning('请输入认证驳回原因'); return; }
auditCertification(this.certificationAuditForm.id, 2, this.certificationRejectReason).then(() => {
this.$message.success('认证已驳回');
this.certificationAuditBox = false;
this.onLoad(this.page);
});
},
resetVehicle() {
this.vehicleForm = emptyForm();
this.vehicleCertificateUploads = {};
@@ -763,9 +869,9 @@ export default {
this.vehicleForm[prop] = url;
this.$refs.vehicleForm?.validateField(prop);
},
handleVehicleCertificateUploadSuccess(prop, url, file = {}) {
handleVehicleCertificateUploadSuccess(prop, url) {
this.setImage(prop, url);
this.vehicleCertificateUploads[prop] = this.getUploadRecognitionKey(file, url);
this.vehicleCertificateUploads[prop] = url;
this.recognizeVehicleCertificates();
},
recognizeVehicleCertificates() {
@@ -846,19 +952,6 @@ export default {
});
});
},
getUploadRecognitionKey(file = {}, url = '') {
return (
file.objectKey ||
file.key ||
file.fileName ||
file.name ||
file.link ||
file.url ||
file.domain ||
url ||
''
);
},
normalizeDate(value = '') {
const dateValue = String(value || '').trim();
const match = dateValue.match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$/);
@@ -949,7 +1042,8 @@ export default {
});
});
},
handleExpireChange() {
handleExpireChange(expireStatus) {
this.query.expireStatus = expireStatus;
this.page.currentPage = 1;
this.onLoad(this.page);
},
@@ -963,7 +1057,9 @@ export default {
});
},
searchReset() {
this.searchForm = {};
this.searchForm = {
businessRelation: '',
};
this.query = {
expireStatus: this.query.expireStatus,
};
@@ -1064,14 +1160,44 @@ export default {
}
return '';
},
getCertificationStatus(row) {
if (row.certificationStatus === 0) return { label: '认证中', type: 'warning' };
if (row.certificationStatus === 1) return { label: '已认证', type: 'success' };
if (row.certificationStatus === 2) return { label: '已驳回', type: 'danger' };
const today = this.$dayjs().startOf('day');
const certificateDates = [
[row.drivingLicenseEndDate, row.drivingLicenseLongTerm],
[row.roadTransportCertEndDate, row.roadTransportCertLongTerm],
];
if (certificateDates.some(([date, longTerm]) => longTerm !== 1 && !date)) {
return { label: '未认证', type: 'info' };
}
if (
certificateDates.some(
([date, longTerm]) => longTerm !== 1 && this.$dayjs(date).isBefore(today, 'day')
)
) {
return { label: '已过期', type: 'danger' };
}
return { label: '已认证', type: 'success' };
},
},
};
</script>
<style lang="scss" scoped>
.transport-vehicle-page {
.vehicle-stat {
flex-shrink: 0;
&__expiry-tags {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
width: 100%;
:deep(.el-check-tag) {
margin: 0;
white-space: nowrap;
}
}
:deep(.el-table th .cell),
@@ -1194,7 +1320,7 @@ export default {
}
.date-warning {
color: #f56c6c;
color: #d40000;
font-weight: 600;
}

View File

@@ -70,6 +70,28 @@
class="accident-record-page__input"
/>
</template>
<template #directEconomicLossForm>
<el-input
v-model="form.directEconomicLoss"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('directEconomicLoss', value)"
>
<template #suffix></template>
</el-input>
</template>
<template #insuranceClaimAmountForm>
<el-input
v-model="form.insuranceClaimAmount"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('insuranceClaimAmount', value)"
>
<template #suffix></template>
</el-input>
</template>
<template #attachments="{ row }">
<span>{{ formatAttachments(row.attachments) }}</span>
</template>
@@ -256,6 +278,13 @@ export default {
if (!value || typeof value === 'string') return value;
return JSON.stringify(value);
},
normalizeDecimalInput(prop, value) {
const sanitized = String(value ?? '').replace(/[^\d.]/g, '');
const [integerPart, ...decimalParts] = sanitized.split('.');
this.form[prop] = decimalParts.length
? `${integerPart || '0'}.${decimalParts.join('').slice(0, 2)}`
: integerPart;
},
normalizeRow(row) {
const values = { ...row };
values.vehicleType = values.vehicleType || '车辆';

View File

@@ -70,6 +70,17 @@
class="annual-inspection-record-page__input"
/>
</template>
<template #feeForm>
<el-input
v-model="form.fee"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('fee', value)"
>
<template #suffix></template>
</el-input>
</template>
<template #attachments="{ row }">
<span>{{ formatAttachments(row.attachments) }}</span>
</template>
@@ -256,6 +267,13 @@ export default {
if (!value || typeof value === 'string') return value;
return JSON.stringify(value);
},
normalizeDecimalInput(prop, value) {
const sanitized = String(value ?? '').replace(/[^\d.]/g, '');
const [integerPart, ...decimalParts] = sanitized.split('.');
this.form[prop] = decimalParts.length
? `${integerPart || '0'}.${decimalParts.join('').slice(0, 2)}`
: integerPart;
},
normalizeRow(row) {
const values = { ...row };
values.vehicleType = values.vehicleType || '车辆';

View File

@@ -20,35 +20,10 @@
<el-button
type="primary"
icon="el-icon-plus"
plain
v-if="hasPermission('credit_score_quantification_add')"
@click="openConfig()"
>新增
</el-button>
<el-button
type="primary"
icon="el-icon-upload"
plain
v-if="hasPermission('credit_score_quantification_import')"
@click="handleImport"
>批量导入
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="hasPermission('credit_score_quantification_template')"
@click="handleTemplate"
>下载模板
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="hasPermission('credit_score_quantification_export')"
@click="handleExport"
>批量导出
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
@@ -71,28 +46,28 @@
<template #menu="{ row }">
<el-link
type="primary"
v-if="hasPermission('credit_score_quantification_edit')"
@click="openConfig(row)"
v-if="hasPermission('credit_score_quantification_status') && row.status === 1"
@click="handleStatus(row)"
>
编辑
停用
</el-link>
<el-link
type="primary"
v-if="hasPermission('credit_score_quantification_publish') && row.status === 3"
v-if="hasPermission('credit_score_quantification_publish') && row.status !== 1"
@click="handlePublish(row)"
>
发布
</el-link>
<el-link
type="primary"
v-if="hasPermission('credit_score_quantification_status') && row.status !== 3"
@click="handleStatus(row)"
v-if="hasPermission('credit_score_quantification_edit') && row.status !== 1"
@click="openConfig(row)"
>
{{ row.status === 1 ? '停用' : '启用' }}
编辑
</el-link>
<el-link
type="danger"
v-if="hasPermission('credit_score_quantification_delete') && row.status === 3"
v-if="hasPermission('credit_score_quantification_delete')"
@click="rowDel(row)"
>
删除
@@ -100,16 +75,6 @@
</template>
</avue-crud>
<el-dialog title="评分量化表数据导入" append-to-body v-model="excelBox" width="555px">
<avue-form :option="excelOption" v-model="excelForm">
<template #excelTemplate>
<el-button type="primary" @click="handleTemplate">
点击下载<i class="el-icon-download el-icon--right"></i>
</el-button>
</template>
</avue-form>
</el-dialog>
<el-dialog
:title="readonly ? '查看评分量化表' : configForm.id ? '编辑评分量化表' : '新增评分量化表'"
append-to-body
@@ -128,10 +93,21 @@
:disabled="readonly"
>
<el-form-item label="评定表名称" required>
<el-input v-model="configForm.name" maxlength="30" show-word-limit />
<el-input
v-model="configForm.name"
class="score-config__name-input"
maxlength="30"
show-word-limit
/>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="configForm.remark" type="textarea" maxlength="500" show-word-limit />
<el-input
v-model="configForm.remark"
type="textarea"
:rows="1"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-form>
@@ -160,16 +136,6 @@
<el-table :data="configForm.standards" border class="config-table">
<el-table-column label="序号" type="index" width="90" align="center" />
<el-table-column label="信用等级" prop="creditLevel" width="120" align="center" />
<el-table-column label="得分率" width="160" align="center">
<template #default="{ row }"
>{{ row.scoreRateLower }}% - {{ row.scoreRateUpper }}%</template
>
</el-table-column>
<el-table-column label="最大资金使用额度(万元)" width="210" align="center">
<template #default="{ row }">
{{ row.creditLimitLower }} - {{ row.creditLimitUpper }}
</template>
</el-table-column>
<el-table-column label="标准说明" prop="standardDescription" min-width="320" />
<el-table-column label="操作" width="160" align="center">
<template #default="{ row, $index }">
@@ -192,7 +158,7 @@
append-to-body
v-model="categoryBox"
width="82%"
class="score-dialog"
class="score-dialog category-dialog"
:close-on-press-escape="false"
>
<div class="category-detail">
@@ -317,43 +283,62 @@
</el-col>
<el-col :span="8">
<el-form-item label="得分率(%)" required>
<div class="range-input">
<el-input-number
<div class="range-input standard-range-input">
<el-input
v-model="standardForm.scoreRateLower"
:min="0"
:max="100"
:precision="0"
inputmode="numeric"
@input="value => handleStandardNumberInput('scoreRateLower', value, 0, 100)"
/>
<span></span>
<el-input-number
<el-input
v-model="standardForm.scoreRateUpper"
:min="0"
:max="100"
:precision="0"
inputmode="numeric"
@input="value => handleStandardNumberInput('scoreRateUpper', value, 0, 100)"
/>
</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="得分率每增加" required>
<el-input-number v-model="standardForm.scoreRateIncrease" :min="0" :precision="0" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="最大资金使用额度(万元)" required>
<div class="range-input">
<el-input-number v-model="standardForm.creditLimitLower" :min="0" :precision="2" />
<span></span>
<el-input-number v-model="standardForm.creditLimitUpper" :min="0" :precision="2" />
</div>
<el-form-item label="得分率每增加" required>
<el-input
v-model="standardForm.scoreRateIncrease"
inputmode="numeric"
@input="value => handleStandardNumberInput('scoreRateIncrease', value)"
>
<template #suffix>%</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-form-item label="额度增加" required>
<el-input-number v-model="standardForm.creditLimitIncrease" :min="0" :precision="2" />
<span class="score-unit">万元</span>
<el-input
v-model="standardForm.creditLimitIncrease"
inputmode="decimal"
@input="value => handleStandardNumberInput('creditLimitIncrease', value, 2)"
>
<template #suffix>万元</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="最大资金使用额度(万元)" required>
<div class="range-input standard-range-input">
<el-input
v-model="standardForm.creditLimitLower"
inputmode="decimal"
@input="value => handleStandardNumberInput('creditLimitLower', value, 2)"
/>
<span></span>
<el-input
v-model="standardForm.creditLimitUpper"
inputmode="decimal"
@input="value => handleStandardNumberInput('creditLimitUpper', value, 2)"
/>
</div>
</el-form-item>
</el-col>
</el-row>
@@ -383,14 +368,8 @@ import {
publish,
changeStatus,
} from '@/api/vehicle/credit-score-quantification';
import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { getToken } from '@/utils/auth';
import { normalizeSearchRangeParams } from '@/utils/search-range';
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
const createTimeRangeMap = {
createTimeRange: ['createTimeStart', 'createTimeEnd'],
@@ -413,9 +392,7 @@ export default {
categoryBox: false,
itemBox: false,
standardBox: false,
excelBox: false,
readonly: false,
excelForm: {},
configForm: this.emptyConfig(),
currentCategory: { items: [] },
currentItemIndex: -1,
@@ -455,7 +432,7 @@ export default {
menuWidth: 160,
column: [
{
label: '评表名称',
label: '评表名称',
prop: 'name',
slot: true,
minWidth: 180,
@@ -464,23 +441,10 @@ export default {
rules: [{ required: true, message: '请输入评定表名称', trigger: 'blur' }],
},
{
label: '备注',
prop: 'remark',
minWidth: 260,
overHidden: true,
},
{
label: '状态',
prop: 'status',
type: 'select',
search: true,
slot: true,
dataType: 'number',
dicData: [
{ label: '正常', value: 1 },
{ label: '停用', value: 2 },
{ label: '草稿', value: 3 },
],
label: '创建人',
prop: 'createUserName',
minWidth: 120,
display: false,
},
{
label: '创建时间',
@@ -489,6 +453,22 @@ export default {
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
minWidth: 160,
display: false,
},
{
label: '状态',
prop: 'status',
type: 'select',
search: true,
slot: true,
dataType: 'number',
searchValue: '',
dicData: [
{ label: '全部', value: '' },
{ label: '正常', value: 1 },
{ label: '停用', value: 2 },
],
clearable: true,
},
{
label: '创建时间',
@@ -502,33 +482,6 @@ export default {
},
],
},
excelOption: {
labelPosition: 'right',
submitBtn: false,
emptyBtn: false,
column: [
{
label: '模板上传',
prop: 'excelFile',
type: 'upload',
drag: true,
loadText: '模板上传中,请稍等',
span: 24,
propsHttp: {
res: 'data',
},
tip: '请上传 .xls,.xlsx 标准格式文件',
action:
'/blade-transport/credit-score-quantification/import-credit-score-quantification',
},
{
label: '模板下载',
prop: 'excelTemplate',
formslot: true,
span: 24,
},
],
},
};
},
computed: {
@@ -723,6 +676,18 @@ export default {
this.standardForm.creditLevel = '';
}
},
handleStandardNumberInput(field, value, precision = 0, max) {
let normalized = String(value || '').replace(precision > 0 ? /[^\d.]/g : /\D/g, '');
if (precision > 0) {
const [integer = '', ...decimalParts] = normalized.split('.');
const decimal = decimalParts.join('').slice(0, precision);
normalized = decimalParts.length ? `${integer}.${decimal}` : integer;
}
if (max !== undefined && normalized !== '' && Number(normalized) > max) {
normalized = String(max);
}
this.standardForm[field] = normalized;
},
isValidCreditLevel(creditLevel) {
return this.creditLevelOptions.some(item => item.value === String(creditLevel || '').trim());
},
@@ -764,6 +729,16 @@ export default {
}
const standard = JSON.parse(JSON.stringify(this.standardForm));
standard.creditLevel = String(standard.creditLevel || '').trim();
[
'scoreRateLower',
'scoreRateUpper',
'scoreRateIncrease',
'creditLimitLower',
'creditLimitUpper',
'creditLimitIncrease',
].forEach(field => {
standard[field] = Number(standard[field]);
});
const nextStandards = JSON.parse(JSON.stringify(this.configForm.standards));
if (this.currentStandardIndex >= 0) {
nextStandards.splice(this.currentStandardIndex, 1, standard);
@@ -786,7 +761,13 @@ export default {
this.standardBox = false;
},
removeStandard(index) {
this.configForm.standards.splice(index, 1);
this.$confirm('确定删除该评估标准?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.configForm.standards.splice(index, 1);
});
},
isBlankValue(value) {
return value === undefined || value === null || value === '';
@@ -947,7 +928,7 @@ export default {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('仅草稿状态量表可删除,确定删除所选数据?', {
this.$confirm('确定删除所选评分量化表?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
@@ -1024,49 +1005,9 @@ export default {
this.loading = false;
});
},
handleImport() {
openImportDialog(this, '评分量化表');
},
handleExport() {
this.$confirm('是否导出评分量化表数据?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob(
'/blade-transport/credit-score-quantification/export-credit-score-quantification',
this.buildExportParams(),
{ feedback: true }
)
.then(res => {
downloadXls(res.data, `评分量化表${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
})
.finally(() => {
NProgress.done();
});
});
},
buildExportParams() {
return {
...this.buildQuery(),
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
},
buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
},
handleTemplate() {
exportBlob(
`/blade-transport/credit-score-quantification/export-template?${
this.website.tokenHeader
}=${getToken()}`,
{ feedback: true }
).then(res => {
downloadXls(res.data, '评分量化表模板.xlsx');
});
},
},
};
</script>
@@ -1074,6 +1015,10 @@ export default {
<style lang="scss" scoped>
.score-config {
padding: 4px 12px 12px;
&__name-input {
width: min(420px, 100%);
}
}
.base-form {
@@ -1111,10 +1056,14 @@ export default {
.category-line {
margin: 0 0 24px;
font-size: 16px;
font-weight: 600;
font-weight: 400;
color: #606266;
}
:deep(.category-dialog .el-dialog__title) {
font-weight: 700;
}
.score-item-form {
width: 100%;
max-width: 1400px;
@@ -1220,6 +1169,17 @@ export default {
gap: 10px;
}
.standard-range-input {
width: 100%;
min-width: 0;
:deep(.el-input) {
flex: 1;
width: 0;
min-width: 0;
}
}
.score-unit {
margin-left: 8px;
color: #606266;

View File

@@ -20,7 +20,6 @@
<el-button
type="primary"
icon="el-icon-plus"
plain
v-if="hasPermission('customer_archive_add')"
@click="openArchive()"
>新增
@@ -87,6 +86,16 @@
>
提交
</el-link>
<el-link
type="warning"
v-if="
hasPermission('customer_archive_submit') &&
['draft', 'reviewing'].includes(row.approvalStatus)
"
@click="handleWithdrawApproval(row)"
>
撤回
</el-link>
<el-link
type="success"
v-if="hasPermission('customer_archive_approve') && row.approvalStatus === 'reviewing'"
@@ -142,16 +151,23 @@
:disabled="readonly"
class="archive-form"
>
<section-card title="工商信息">
<div class="section-title dialog-section-title">基本信息</div>
<div class="archive-form__group">
<div class="dialog-section-title archive-form__group-title">工商信息</div>
<el-row :gutter="18">
<el-col :span="8">
<el-col :span="6">
<el-form-item label="客商编号" prop="customerCode">
<el-input v-model="archiveForm.customerCode" disabled placeholder="保存后自动生成" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-form-item label="客商性质" prop="customerNature">
<el-select v-model="archiveForm.customerNature" filterable clearable>
<el-select
v-model="archiveForm.customerNature"
placeholder="请输入"
filterable
clearable
>
<el-option
v-for="item in customerNatureOptions"
:key="item.value"
@@ -161,10 +177,11 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-form-item label="统一社会信用代码" prop="unifiedCreditCode">
<el-input
v-model="archiveForm.unifiedCreditCode"
placeholder="请输入"
maxlength="18"
show-word-limit
@input="
@@ -175,14 +192,25 @@
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-form-item label="客商名称" prop="fullName">
<el-input v-model="archiveForm.fullName" maxlength="100" show-word-limit />
<el-input
v-model="archiveForm.fullName"
placeholder="请输入"
maxlength="100"
show-word-limit
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-form-item label="客商类型" prop="customerType">
<el-select v-model="archiveForm.customerType" filterable clearable>
<el-select
v-model="archiveForm.customerType"
multiple
placeholder="请选择"
filterable
clearable
>
<el-option
v-for="item in customerTypeOptions"
:key="item.value"
@@ -192,13 +220,13 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-form-item label="企业法人" prop="legalPerson">
<el-input v-model="archiveForm.legalPerson" maxlength="10" show-word-limit />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="经营范围" prop="businessScope">
<el-col :span="6">
<el-form-item label="经营范围" prop="businessScope" required>
<el-select v-model="archiveForm.businessScope" multiple filterable clearable>
<el-option
v-for="item in businessScopeOptions"
@@ -209,8 +237,12 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="营业期限" prop="businessEndDate" required>
<el-col :span="6">
<el-form-item
label="营业期限"
prop="businessEndDate"
:required="archiveForm.businessTermType === '固定期限'"
>
<div class="business-term-field">
<el-date-picker
v-model="archiveForm.businessEndDate"
@@ -228,18 +260,23 @@
</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-form-item label="助记码" prop="mnemonicCode">
<el-input v-model="archiveForm.mnemonicCode" maxlength="15" show-word-limit />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-form-item label="客户简称" prop="shortName">
<el-input v-model="archiveForm.shortName" maxlength="20" show-word-limit />
<el-input
v-model="archiveForm.shortName"
placeholder="请输入"
maxlength="20"
show-word-limit
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="地址" prop="registeredDetailAddress">
<el-form-item label="地址" prop="registeredDetailAddress" required>
<div class="archive-form__address">
<el-cascader
ref="registeredRegionCascader"
@@ -260,9 +297,10 @@
</el-form-item>
</el-col>
</el-row>
</section-card>
</div>
<section-card title="联系信息">
<div class="archive-form__group">
<div class="dialog-section-title archive-form__group-title">联系信息</div>
<el-row :gutter="18">
<el-col :span="8">
<el-form-item label="客商负责人" prop="principal">
@@ -291,11 +329,12 @@
</el-form-item>
</el-col>
</el-row>
</section-card>
</div>
<section-card title="财务/信用信息">
<div class="archive-form__group">
<div class="dialog-section-title archive-form__group-title">财务/信用信息</div>
<el-row :gutter="18">
<el-col :span="8">
<el-col :span="6">
<el-form-item label="注册资金(万元)" prop="registeredCapital">
<el-input
v-model="archiveForm.registeredCapital"
@@ -304,7 +343,7 @@
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-form-item label="开票税点" prop="invoiceTaxRate">
<el-input
v-model="archiveForm.invoiceTaxRate"
@@ -313,12 +352,12 @@
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-form-item label="客户等级" prop="customerLevel">
<el-input v-model="archiveForm.customerLevel" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-form-item label="最大资金使用额度(万元)" prop="maxCreditLimit">
<el-input
v-model="archiveForm.maxCreditLimit"
@@ -328,19 +367,20 @@
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="申请总资金使用额度(万元)" prop="applyCreditLimit">
<el-input
v-model="archiveForm.applyCreditLimit"
maxlength="18"
@input="value => handleDecimalInput('applyCreditLimit', value)"
/>
</el-form-item>
<el-col :span="6">
<el-form-item label="申请总资金使用额度(万元)" prop="applyCreditLimit">
<el-input
v-model="archiveForm.applyCreditLimit"
maxlength="18"
disabled
/>
</el-form-item>
</el-col>
</el-row>
</section-card>
</div>
<section-card title="其他信息">
<div class="archive-form__group">
<div class="dialog-section-title archive-form__group-title">其他信息</div>
<el-row :gutter="18">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
@@ -353,7 +393,7 @@
</el-form-item>
</el-col>
</el-row>
</section-card>
</div>
</el-form>
<el-tabs v-model="activeTab" class="archive-tabs">
@@ -538,8 +578,9 @@
</el-tab-pane>
</el-tabs>
<section-card title="客商材料">
<template #extra>
<div class="customer-material-section">
<div class="section-head">
<div class="section-title dialog-section-title">客商材料</div>
<div v-if="!readonly" class="section-actions">
<el-button icon="el-icon-download" @click="downloadAttachments">批量下载</el-button>
<el-button type="primary" icon="el-icon-plus" @click="addAttachment"
@@ -549,11 +590,11 @@
OCR上传识别
</el-button>
</div>
</template>
</div>
<el-alert
type="info"
:closable="false"
title="支持 JPG、PNG、PDF、Word、Excel、PPT,单文件大小不超过 10MB。"
title="支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单文件大小限制500M"
class="archive-rule-alert"
/>
<el-alert
@@ -580,8 +621,8 @@
v-model="row.files"
:readonly="readonly"
:multiple="false"
accept=".jpg,.jpeg,.png,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"
:max-size="10"
accept=".pdf,.bmp,.jpeg,.png,.jpg,.doc,.docx,.ppt,.pptx,.xlsx,.xls,.eml,.msg,.zip"
:max-size="500"
button-text="上传文件"
:show-tip="false"
@success="file => handleQualificationUploadSuccess(file, row)"
@@ -616,16 +657,17 @@
</template>
</el-table-column>
</el-table>
</section-card>
</div>
<section-card title="变更记录">
<div class="change-record-section">
<div class="section-title dialog-section-title">变更记录</div>
<el-table :data="archiveForm.changeRecords" border>
<el-table-column label="序号" type="index" width="90" align="center" />
<el-table-column label="变更日期" prop="changeTime" min-width="170" />
<el-table-column label="变更内容" prop="changeContent" min-width="260" />
<el-table-column label="变更账号" prop="changeUserName" min-width="160" />
</el-table>
</section-card>
</div>
<template #footer>
<el-button @click="archiveBox = false">{{ readonly ? '关闭' : '取消' }}</el-button>
@@ -651,13 +693,13 @@
label-width="110px"
class="contact-form"
>
<el-form-item label="联系人姓名" prop="contactName">
<el-form-item label="联系人姓名" prop="contactName" class="contact-form__compact-field">
<el-input v-model="contactForm.contactName" maxlength="30" />
</el-form-item>
<el-form-item label="手机号" prop="contactPhone">
<el-form-item label="手机号" prop="contactPhone" class="contact-form__compact-field">
<el-input v-model="contactForm.contactPhone" maxlength="11" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-form-item label="邮箱" prop="email" class="contact-form__compact-field">
<el-input v-model="contactForm.email" maxlength="100" />
</el-form-item>
<el-form-item label="地址" prop="detailAddress">
@@ -678,13 +720,15 @@
/>
</div>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-form-item label="备注" prop="remark" class="contact-form__compact-field">
<el-input v-model="contactForm.remark" maxlength="200" />
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="saveContact">保存</el-button>
<el-button @click="contactBox = false">返回</el-button>
<div class="contact-dialog__footer-actions">
<el-button type="primary" @click="saveContact">保存</el-button>
<el-button @click="contactBox = false">返回</el-button>
</div>
</template>
</el-dialog>
@@ -771,8 +815,10 @@
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="saveReceipt">保存</el-button>
<el-button @click="receiptBox = false">返回</el-button>
<div class="receipt-dialog__footer-actions">
<el-button type="primary" @click="saveReceipt">保存</el-button>
<el-button @click="receiptBox = false">返回</el-button>
</div>
</template>
</el-dialog>
@@ -802,24 +848,24 @@
<el-input v-model="invoiceForm.registeredPhone" maxlength="20" />
</el-form-item>
<el-form-item label="注册地址" prop="registeredRegionName">
<div class="invoice-form__address">
<el-input
v-model="invoiceForm.registeredRegionName"
readonly
maxlength="100"
placeholder="点击后弹出地图组件"
suffix-icon="el-icon-location"
@click="handleInvoiceRegisteredMapPick"
/>
<el-input
v-model="invoiceForm.registeredDetailAddress"
readonly
maxlength="255"
placeholder="点击后弹出地图组件"
suffix-icon="el-icon-location"
@click="handleInvoiceRegisteredMapPick"
/>
</div>
<el-input
v-model="invoiceForm.registeredRegionName"
readonly
maxlength="100"
placeholder="点击后弹出地图组件"
suffix-icon="el-icon-location"
@click="handleInvoiceRegisteredMapPick"
/>
</el-form-item>
<el-form-item label="详细地址" prop="registeredDetailAddress">
<el-input
v-model="invoiceForm.registeredDetailAddress"
readonly
maxlength="255"
placeholder="点击后弹出地图组件"
suffix-icon="el-icon-location"
@click="handleInvoiceRegisteredMapPick"
/>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -873,8 +919,10 @@
</el-row>
</el-form>
<template #footer>
<el-button type="primary" @click="saveInvoice">保存</el-button>
<el-button @click="invoiceBox = false">返回</el-button>
<div class="invoice-dialog__footer-actions">
<el-button type="primary" @click="saveInvoice">保存</el-button>
<el-button @click="invoiceBox = false">返回</el-button>
</div>
</template>
</el-dialog>
@@ -1090,7 +1138,7 @@
</el-table-column>
<el-table-column label="操作" width="120" align="center" v-if="!readonly">
<template #default="{ $index }">
<el-link type="primary" @click="scoreAttachmentFiles.splice($index, 1)">
<el-link type="primary" @click="deleteScoreAttachment($index)">
删除
</el-link>
</template>
@@ -1113,6 +1161,7 @@ import {
getDetail,
submit,
submitApproval,
withdrawApproval,
approve,
reject,
changeStatus,
@@ -1180,11 +1229,11 @@ export default {
callback();
}
};
const validateInvoiceRegisteredAddress = (rule, value, callback) => {
const regionName = (this.invoiceForm.registeredRegionName || '').trim();
const detailAddress = (this.invoiceForm.registeredDetailAddress || '').trim();
const validateRegisteredAddress = (rule, value, callback) => {
const regionName = (this.archiveForm.registeredRegionName || '').trim();
const detailAddress = String(value || '').trim();
if (!regionName) {
callback(new Error('请选择注册地址'));
callback(new Error('请选择省市区'));
} else if (!detailAddress) {
callback(new Error('请输入详细地址'));
} else if (detailAddress.length > 255) {
@@ -1196,6 +1245,7 @@ export default {
return {
form: {},
query: {},
deptSearchInitialized: false,
loading: true,
data: [],
page: {
@@ -1305,8 +1355,18 @@ export default {
maxCreditLimit: [{ validator: validateNonNegative, trigger: 'blur' }],
applyCreditLimit: [{ validator: validateNonNegative, trigger: 'blur' }],
businessEndDate: [{ validator: validateBusinessEndDate, trigger: 'change' }],
registeredDetailAddress: [{ max: 255, message: '详细地址最多255个字符', trigger: 'blur' }],
businessScope: [{ type: 'array', message: '请选择经营范围', trigger: 'change' }],
registeredDetailAddress: [
{ validator: validateRegisteredAddress, trigger: ['change', 'blur'] },
],
businessScope: [
{
required: true,
type: 'array',
min: 1,
message: '请选择经营范围',
trigger: 'change',
},
],
remark: [{ max: 500, message: '备注最多500个字符', trigger: 'blur' }],
},
contactRules: {
@@ -1332,7 +1392,11 @@ export default {
registeredPhone: [{ required: true, message: '请输入注册电话', trigger: 'blur' }],
bankName: [{ required: true, message: '请输入开户行名称', trigger: 'blur' }],
registeredRegionName: [
{ required: true, validator: validateInvoiceRegisteredAddress, trigger: 'blur' },
{ required: true, message: '请选择注册地址', trigger: 'blur' },
],
registeredDetailAddress: [
{ required: true, message: '请输入详细地址', trigger: 'blur' },
{ max: 255, message: '详细地址最多255个字符', trigger: 'blur' },
],
bankAccount: [{ required: true, message: '请输入银行账号', trigger: 'blur' }],
receiverDetailAddress: [
@@ -1369,6 +1433,7 @@ export default {
slot: true,
search: true,
searchOrder: 10,
searchPlaceholder: '请输入',
minWidth: 140,
},
{
@@ -1376,6 +1441,7 @@ export default {
prop: 'shortName',
search: true,
searchOrder: 2,
searchPlaceholder: '请输入',
minWidth: 150,
},
{
@@ -1383,6 +1449,7 @@ export default {
prop: 'fullName',
search: true,
searchOrder: 9,
searchPlaceholder: '请输入',
minWidth: 220,
overHidden: true,
},
@@ -1392,8 +1459,10 @@ export default {
search: true,
searchType: 'select',
searchOrder: 7,
searchValue: '',
searchPlaceholder: '请输入',
minWidth: 140,
dicData: this.customerTypeOptions,
dicData: [{ label: '全部', value: '' }],
},
{
label: '客户性质',
@@ -1401,8 +1470,11 @@ export default {
type: 'select',
search: true,
searchOrder: 5,
searchValue: '',
searchPlaceholder: '请输入',
minWidth: 120,
dicData: [
{ label: '全部', value: '' },
{ label: '有限公司', value: '有限公司' },
{ label: '个体工商户', value: '个体工商户' },
],
@@ -1412,6 +1484,7 @@ export default {
prop: 'unifiedCreditCode',
search: true,
searchOrder: 3,
searchPlaceholder: '请输入',
minWidth: 190,
},
{
@@ -1420,8 +1493,9 @@ export default {
search: true,
searchType: 'select',
searchOrder: 1,
searchValue: '',
minWidth: 180,
dicData: [],
dicData: [{ label: '全部', value: '' }],
},
{
label: '准入标准',
@@ -1432,8 +1506,8 @@ export default {
searchOrder: 8,
minWidth: 120,
dicData: [
{ label: '临时客商', value: 'temporary' },
{ label: '正式客商', value: 'formal' },
{ label: '临时', value: 'temporary' },
{ label: '正式', value: 'formal' },
],
},
{
@@ -1443,8 +1517,10 @@ export default {
type: 'select',
search: true,
searchOrder: 4,
searchValue: '',
minWidth: 130,
dicData: [
{ label: '全部', value: '' },
{ label: '草稿', value: 'draft' },
{ label: '审核中', value: 'reviewing' },
{ label: '审核通过', value: 'approved' },
@@ -1468,9 +1544,11 @@ export default {
type: 'select',
search: true,
searchOrder: 6,
searchValue: '',
dataType: 'number',
minWidth: 100,
dicData: [
{ label: '全部', value: '' },
{ label: '启用', value: 1 },
{ label: '停用', value: 2 },
],
@@ -1618,7 +1696,7 @@ export default {
deptId: [],
deptIds: '',
deptName: '',
customerType: '',
customerType: [],
businessScope: [],
businessTermType: '固定期限',
registeredRegionPath: [],
@@ -1736,15 +1814,38 @@ export default {
};
},
initDeptTree() {
getDeptTree(this.userInfo.tenantId).then(res => {
this.deptTree = this.normalizeDeptTree(res.data.data || []);
this.deptOptions = this.flattenDept(this.deptTree);
const deptColumn = this.findColumn(this.option.column, 'deptName');
deptColumn.dicData = this.deptOptions.map(item => ({
label: item.label,
value: item.rawLabel,
}));
});
getDeptTree(this.userInfo.tenantId)
.then(res => {
this.deptTree = this.normalizeDeptTree(res.data.data || []);
this.deptOptions = this.flattenDept(this.deptTree);
const deptColumn = this.findColumn(this.option.column, 'deptName');
deptColumn.dicData = [
{ label: '全部', value: '' },
...this.deptOptions.map(item => ({
label: item.label,
value: item.rawLabel,
})),
];
this.applyDefaultDeptSearch(deptColumn);
})
.finally(() => {
this.deptSearchInitialized = true;
this.$nextTick(() => this.onLoad(this.page, this.query));
});
},
applyDefaultDeptSearch(deptColumn) {
if (this.isAdmin) return;
const currentDeptId = this.userInfo.deptId || this.userInfo.dept_id;
const currentDept = this.deptOptions.find(
item => String(item.value) === String(currentDeptId)
);
const deptName = currentDept?.rawLabel || this.userInfo.deptName || this.userInfo.dept_name;
if (!deptName) return;
deptColumn.searchValue = deptName;
this.query = { ...this.query, deptName };
if (this.$refs.crud?.searchForm) {
this.$refs.crud.searchForm.deptName = deptName;
}
},
initRegionOptions() {
getLazyTree().then(res => {
@@ -1827,10 +1928,12 @@ export default {
handleRegisteredRegionChange(value) {
if (!value || !value.length) {
this.archiveForm.registeredRegionName = '';
this.$refs.archiveForm?.validateField('registeredDetailAddress');
return;
}
this.$nextTick(() => {
this.archiveForm.registeredRegionName = this.getRegisteredRegionLabels(value).join('');
this.$refs.archiveForm?.validateField('registeredDetailAddress');
});
},
initBusinessDictionaries() {
@@ -1840,12 +1943,12 @@ export default {
['有限公司', '个体工商户'],
options => {
const customerNatureColumn = this.findColumn(this.option.column, 'customerNature');
customerNatureColumn.dicData = options;
customerNatureColumn.dicData = [{ label: '全部', value: '' }, ...options];
}
);
this.loadDictOptions('customer_type', 'customerTypeOptions', ['客户', '承运商'], options => {
const customerTypeColumn = this.findColumn(this.option.column, 'customerType');
customerTypeColumn.dicData = options;
const customerTypeColumn = this.findColumn(this.option.column, 'customerType');
customerTypeColumn.dicData = [{ label: '全部', value: '' }, ...options];
});
this.loadDictOptions('scope_of_business', 'businessScopeOptions');
this.loadDictOptions('bank_list', 'bankListOptions');
@@ -1922,7 +2025,12 @@ export default {
if (checked) {
this.archiveForm.businessEndDate = '';
}
this.$refs.archiveForm?.validateField('businessEndDate');
this.$nextTick(() => {
this.$refs.archiveForm?.clearValidate('businessEndDate');
if (!checked) {
this.$refs.archiveForm?.validateField('businessEndDate');
}
});
},
handleBusinessEndDateChange(value) {
if (value) {
@@ -2033,8 +2141,16 @@ export default {
});
},
deleteContact(index) {
this.archiveForm.contacts.splice(index, 1);
this.handleDetailCurrentChange('contact', this.contactPage.currentPage);
this.$confirm('确定删除该联系人?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.archiveForm.contacts.splice(index, 1);
this.handleDetailCurrentChange('contact', this.contactPage.currentPage);
})
.catch(() => {});
},
handleContactMapPick() {
this.contactMapTarget = 'contact';
@@ -2361,8 +2477,16 @@ export default {
});
},
deleteReceipt(index) {
this.archiveForm.receiptAccounts.splice(index, 1);
this.handleDetailCurrentChange('receipt', this.receiptPage.currentPage);
this.$confirm('确定删除该收款信息?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.archiveForm.receiptAccounts.splice(index, 1);
this.handleDetailCurrentChange('receipt', this.receiptPage.currentPage);
})
.catch(() => {});
},
normalizeInvoice(invoice = {}) {
const registeredDetailAddress =
@@ -2416,8 +2540,27 @@ export default {
});
},
deleteInvoice(index) {
this.archiveForm.invoices.splice(index, 1);
this.handleDetailCurrentChange('invoice', this.invoicePage.currentPage);
this.$confirm('确定删除该发票信息?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.archiveForm.invoices.splice(index, 1);
this.handleDetailCurrentChange('invoice', this.invoicePage.currentPage);
})
.catch(() => {});
},
deleteScoreAttachment(index) {
this.$confirm('确定删除该评分证明材料?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.scoreAttachmentFiles.splice(index, 1);
})
.catch(() => {});
},
parseAttachments(value) {
if (!value) return [];
@@ -2502,7 +2645,9 @@ export default {
...this.emptyArchive(),
...detail,
deptId: this.normalizeDeptIds(detail),
customerType: detail.customerType || '',
customerType: this.splitValue(detail.customerType),
invoiceTaxRate: this.normalizeOptionalAmount(detail.invoiceTaxRate),
registeredCapital: this.normalizeOptionalAmount(detail.registeredCapital),
businessScope,
businessTermType: detail.businessTermType || '固定期限',
registeredRegionPath: Array.isArray(detail.registeredRegionPath)
@@ -2541,10 +2686,15 @@ export default {
attachments: [],
details: [],
...score,
tempApplyCreditLimit: this.normalizeOptionalAmount(score.tempApplyCreditLimit),
attachments: score.attachments || this.parseAttachments(score.proofAttachments),
details: score.details || [],
};
},
normalizeOptionalAmount(value) {
const amount = Number(value);
return Number.isFinite(amount) && amount < 0 ? '' : value ?? '';
},
getScoreTimeValue(value) {
if (!value) return 0;
const time = this.$dayjs(value).valueOf();
@@ -2571,6 +2721,7 @@ export default {
const latestScore = this.getLatestCreditScore(archive.scores || []);
archive.customerLevel = latestScore?.creditLevel || archive.customerLevel || '';
archive.maxCreditLimit = latestScore?.maxCreditLimit || archive.maxCreditLimit || '';
archive.applyCreditLimit = archive.maxCreditLimit;
return archive;
},
syncArchiveCreditFromScores() {
@@ -2629,6 +2780,13 @@ export default {
},
normalizeArchive() {
const archive = JSON.parse(JSON.stringify(this.archiveForm));
if (archive.businessTermType === '长期') {
archive.businessEndDate = null;
}
const invoiceTaxRate = this.normalizeOptionalAmount(archive.invoiceTaxRate);
const registeredCapital = this.normalizeOptionalAmount(archive.registeredCapital);
archive.invoiceTaxRate = invoiceTaxRate === '' ? null : invoiceTaxRate;
archive.registeredCapital = registeredCapital === '' ? null : registeredCapital;
archive.customerType = Array.isArray(archive.customerType)
? archive.customerType.join(',')
: archive.customerType;
@@ -3165,6 +3323,18 @@ export default {
});
});
},
handleWithdrawApproval(row) {
this.$confirm('是否确认撤回该客商审批?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => withdrawApproval(row.id))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '已撤回至草稿状态!' });
});
},
handleApprove(row) {
this.$confirm('是否确认审核通过?', '提示', {
confirmButtonText: '确定',
@@ -3204,7 +3374,7 @@ export default {
});
},
searchReset() {
this.query = {};
this.query = this.isAdmin ? {} : { deptName: this.getCurrentDeptName() };
this.onLoad(this.page);
},
searchChange(params, done) {
@@ -3230,6 +3400,7 @@ export default {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
if (!this.deptSearchInitialized) return;
this.loading = true;
getList(page.currentPage, page.pageSize, this.buildQuery(params))
.then(res => {
@@ -3272,6 +3443,13 @@ export default {
buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
},
getCurrentDeptName() {
const currentDeptId = this.userInfo.deptId || this.userInfo.dept_id;
const currentDept = this.deptOptions.find(
item => String(item.value) === String(currentDeptId)
);
return currentDept?.rawLabel || this.userInfo.deptName || this.userInfo.dept_name || '';
},
},
};
</script>
@@ -3293,6 +3471,28 @@ export default {
}
}
.archive-form__group {
padding: 16px 20px;
margin-bottom: 16px;
background-color: #f0f2f5;
border-radius: 8px;
.dialog-section-title {
margin: 0 0 12px;
font-size: 14px;
font-weight: 500;
color: #303133;
}
// 内层白色卡片:字段区域白底,浮于灰色分组块之上
> .el-row {
margin: 0;
padding: 18px 16px 4px;
background-color: #fff;
border-radius: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
}
.business-term-field,
.archive-form__address {
@@ -3326,11 +3526,22 @@ export default {
}
}
.section-title {
margin: 16px 0 12px;
font-size: 18px;
font-weight: 700;
color: #303133;
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
margin: 18px 0 12px;
.section-title {
margin: 0;
}
}
.section-actions {
@@ -3470,6 +3681,13 @@ export default {
}
}
.customer-material-section {
margin-top: 18px;
}
.change-record-section {
margin-top: 18px;
}
.score-list-table,
.contact-table,
@@ -3532,6 +3750,37 @@ export default {
}
}
.contact-form__compact-field {
:deep(.el-input) {
width: 60%;
}
}
.contact-dialog__footer-actions {
display: flex;
justify-content: center;
gap: 12px;
}
.receipt-form {
:deep(.el-input),
:deep(.el-select) {
width: 60%;
}
}
.receipt-dialog__footer-actions {
display: flex;
justify-content: center;
gap: 12px;
}
.invoice-dialog__footer-actions {
display: flex;
justify-content: center;
gap: 12px;
}
.contact-form__map-toolbar {
display: flex;
gap: 10px;

View File

@@ -0,0 +1,343 @@
<template>
<basic-container class="equipment-ledger-page">
<avue-crud
ref="crud"
v-model="form"
v-model:page="page"
:option="option"
:data="data"
:permission="permissionList"
:table-loading="loading"
:before-open="beforeOpen"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button
v-if="hasPermission('equipment_ledger_import')"
type="primary"
plain
@click="handleImport"
>
批量导入
</el-button>
<el-button
v-if="hasPermission('equipment_ledger_template')"
type="primary"
plain
@click="handleTemplate"
>
下载模板
</el-button>
<el-button
v-if="hasPermission('equipment_ledger_export')"
type="primary"
plain
@click="handleExport"
>
批量导出
</el-button>
</template>
<template #vehicleNo="{ row, index }">
<el-link type="primary" @click="$refs.crud.rowView(row, index)">{{
row.vehicleNo
}}</el-link>
</template>
<template #vehicleNoForm>
<el-autocomplete
v-model="form.vehicleNo"
:disabled="boxType === 'view'"
:fetch-suggestions="fetchVehicleOptions"
:placeholder="vehicleNoPlaceholder"
clearable
value-key="value"
/>
</template>
<template #equipmentCodeForm>
<el-input v-model="form.equipmentCode" disabled />
</template>
<template #attachments="{ row }">
<span>{{ formatAttachments(row.attachments) }}</span>
</template>
<template #attachmentsForm>
<vehicle-attachment-upload v-model="form.attachments" :readonly="boxType === 'view'" />
</template>
<template #attachments-form>
<vehicle-attachment-upload v-model="form.attachments" :readonly="boxType === 'view'" />
</template>
</avue-crud>
<empty-pagination
:page="page"
@size-change="sizeChange"
@current-change="currentChange"
@load="onLoad(page, query)"
/>
<el-dialog title="设备台账数据导入" append-to-body v-model="excelBox" width="555px">
<avue-form :option="excelOption" v-model="excelForm">
<template #excelTemplate>
<el-button type="primary" @click="handleTemplate">点击下载</el-button>
</template>
</avue-form>
</el-dialog>
</basic-container>
</template>
<script>
import {
add,
getDetail,
getList,
getNextEquipmentCode,
remove,
update,
} from '@/api/vehicle/equipment-ledger';
import { getList as getVehicleList } from '@/api/transportCapacity/transport-vehicle';
import { getList as getShipList } from '@/api/transportCapacity/transport-ship';
import { exportBlob } from '@/api/common';
import { getDeptTree } from '@/api/system/dept';
import { getDictionary } from '@/api/system/dictbiz';
import { getToken } from '@/utils/auth';
import { openImportDialog } from '@/utils/import-excel';
import { downloadXls } from '@/utils/util';
import { mapGetters } from 'vuex';
import { excelOption, option } from '@/option/vehicle/equipment-ledger';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
export default {
data() {
return {
form: {},
query: {},
loading: true,
excelBox: false,
excelForm: {},
boxType: '',
option,
excelOption,
data: [],
selectionList: [],
page: { pageSize: 10, pageSizes: [10, 20, 50, 100], currentPage: 1, total: 0 },
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
return (this.userInfo.authority || '').includes('admin');
},
permissionList() {
return {
addBtn: this.hasPermission('equipment_ledger_add'),
viewBtn: this.hasPermission('equipment_ledger_view'),
editBtn: this.hasPermission('equipment_ledger_edit'),
delBtn: this.hasPermission('equipment_ledger_delete'),
};
},
vehicleNoPlaceholder() {
return this.form.vehicleType === '船舶' ? '输入船号模糊查询选择' : '输入车牌号模糊查询选择';
},
ids() {
return this.selectionList.map(item => item.id).join(',');
},
},
created() {
this.initDeptTree();
this.initEquipmentTypeOptions();
},
methods: {
hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false);
},
initDeptTree() {
getDeptTree(this.userInfo.tenantId).then(res => {
this.findColumn(this.option.column, 'createDept').dicData = res.data.data || [];
});
},
initEquipmentTypeOptions() {
getDictionary({ code: 'equip_type' }).then(res => {
this.findColumn(this.option.column, 'equipmentType').dicData = (res.data.data || []).map(
item => ({
label: item.dictValue,
value: item.dictValue,
})
);
});
},
fetchVehicleOptions(queryString, callback) {
const isShip = this.form.vehicleType === '船舶';
const request = isShip ? getShipList : getVehicleList;
const params = isShip ? { shipIdentifierNo: queryString } : { plateNo: queryString };
request(1, 20, params)
.then(res => {
callback(
(res.data.data.records || []).map(item => ({
value: isShip ? item.shipIdentifierNo || item.shipName : item.plateNo,
}))
);
})
.catch(() => callback([]));
},
formatAttachments(value) {
if (!value) return '';
if (Array.isArray(value)) return `${value.length}`;
try {
const attachments = JSON.parse(value);
return Array.isArray(attachments) ? `${attachments.length}` : '1 个';
} catch (error) {
return `${value.split(',').filter(Boolean).length}`;
}
},
parseAttachments(value) {
if (!value || Array.isArray(value)) return value;
try {
const attachments = JSON.parse(value);
return Array.isArray(attachments) ? attachments : [];
} catch (error) {
return value
.split(',')
.filter(Boolean)
.map(url => ({ name: url, url }));
}
},
normalizeRow(row) {
const values = { ...row };
values.vehicleType = values.vehicleType || '车辆';
values.vehicleNo = (values.vehicleNo || '').trim();
if (values.vehicleType === '车辆') values.vehicleNo = values.vehicleNo.toUpperCase();
values.attachments = Array.isArray(values.attachments)
? JSON.stringify(values.attachments)
: values.attachments;
return values;
},
rowSave(row, done, loading) {
add(this.normalizeRow(row)).then(
() => {
this.onLoad(this.page);
this.$message.success('操作成功');
done();
},
() => loading()
);
},
rowUpdate(row, index, done, loading) {
update(this.normalizeRow(row)).then(
() => {
this.onLoad(this.page);
this.$message.success('操作成功');
done();
},
() => loading()
);
},
rowDel(row) {
this.$confirm('确认删除该条数据?删除后将不可恢复!', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => remove(row.id))
.then(() => {
this.onLoad(this.page);
this.$message.success('操作成功');
});
},
beforeOpen(done, type) {
this.boxType = type;
if (type === 'add') {
getNextEquipmentCode().then(res => {
this.form = { vehicleType: '车辆', equipmentCode: res.data.data };
done();
});
return;
}
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = {
...res.data.data,
attachments: this.parseAttachments(res.data.data.attachments),
};
done();
});
return;
}
done();
},
searchChange(params, done) {
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
},
searchReset() {
this.query = {};
this.onLoad(this.page);
},
selectionChange(list) {
this.selectionList = list;
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
this.loading = true;
getList(page.currentPage, page.pageSize, { ...this.query, ...params })
.then(res => {
const result = res.data.data;
this.page.total = result.total;
this.data = result.records;
})
.finally(() => {
this.loading = false;
});
},
handleImport() {
openImportDialog(this, '设备台账');
},
handleExport() {
this.$confirm('是否导出设备台账数据?', '提示', { type: 'warning' }).then(() => {
NProgress.start();
exportBlob(
'/blade-transport/equipment-ledger/export-equipment-ledger',
{ ...this.query, ids: this.ids, [this.website.tokenHeader]: getToken() },
{ feedback: true }
)
.then(res =>
downloadXls(res.data, `设备台账${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`)
)
.finally(() => NProgress.done());
});
},
handleTemplate() {
exportBlob(
`/blade-transport/equipment-ledger/export-template?${
this.website.tokenHeader
}=${getToken()}`,
{ feedback: true }
).then(res => downloadXls(res.data, '设备台账模板.xlsx'));
},
},
};
</script>
<style lang="scss" scoped>
.equipment-ledger-page {
:deep(.el-table th .cell),
:deep(.el-table td .cell) {
white-space: nowrap;
}
}
</style>

View File

@@ -70,6 +70,17 @@
class="etc-record-page__input"
/>
</template>
<template #transactionAmountForm>
<el-input
v-model="form.transactionAmount"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('transactionAmount', value)"
>
<template #suffix></template>
</el-input>
</template>
<template #attachments="{ row }">
<span>{{ formatAttachments(row.attachments) }}</span>
</template>
@@ -209,6 +220,13 @@ export default {
isEmpty(value) {
return value === undefined || value === null || value === '';
},
normalizeDecimalInput(prop, value) {
const sanitized = String(value ?? '').replace(/[^\d.]/g, '');
const [integerPart, ...decimalParts] = sanitized.split('.');
this.form[prop] = decimalParts.length
? `${integerPart || '0'}.${decimalParts.join('').slice(0, 2)}`
: integerPart;
},
normalizeRow(row) {
const values = { ...row };
values.dataSource = values.dataSource || '手工录入';
@@ -379,11 +397,9 @@ export default {
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob(
'/blade-transport/etc-record/export-etc-record',
this.buildExportParams(),
{ feedback: true }
)
exportBlob('/blade-transport/etc-record/export-etc-record', this.buildExportParams(), {
feedback: true,
})
.then(res => {
downloadXls(res.data, `ETC记录${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
})

View File

@@ -69,6 +69,28 @@
class="insurance-record-page__vehicle-input"
/>
</template>
<template #insuredAmountForm>
<el-input
v-model="form.insuredAmount"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('insuredAmount', value)"
>
<template #append></template>
</el-input>
</template>
<template #premiumForm>
<el-input
v-model="form.premium"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('premium', value)"
>
<template #append></template>
</el-input>
</template>
<template #policyFileForm>
<div class="insurance-record-page__upload">
<div class="insurance-record-page__upload-info">
@@ -177,6 +199,7 @@ export default {
excelBox: false,
excelForm: {},
isDetailLoading: false,
boxType: '',
option,
excelOption,
page: {
@@ -285,6 +308,13 @@ export default {
}
return values;
},
normalizeDecimalInput(prop, value) {
const sanitized = String(value ?? '').replace(/[^\d.]/g, '');
const [integerPart, ...decimalParts] = sanitized.split('.');
this.form[prop] = decimalParts.length
? `${integerPart || '0'}.${decimalParts.join('').slice(0, 2)}`
: integerPart;
},
buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
},
@@ -369,6 +399,7 @@ export default {
});
},
beforeOpen(done, type) {
this.boxType = type;
if (type === 'add') {
this.form.vehicleType = '车辆';
this.form.ocrTemplate = '紫金-机动车交强险';

View File

@@ -70,6 +70,39 @@
class="maintenance-plan-page__input"
/>
</template>
<template #mileageForm>
<el-input
v-model="form.mileage"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('mileage', value)"
>
<template #append>{{ form.mileageUnit || '公里' }}</template>
</el-input>
</template>
<template #costForm>
<el-input
v-model="form.cost"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('cost', value)"
>
<template #append></template>
</el-input>
</template>
<template #nextMaintenanceMileageForm>
<el-input
v-model="form.nextMaintenanceMileage"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('nextMaintenanceMileage', value)"
>
<template #append>{{ form.mileageUnit || '公里' }}</template>
</el-input>
</template>
<template #mileage="{ row }">
<span>{{ formatMileage(row.mileage, row.mileageUnit) }}</span>
</template>
@@ -169,10 +202,11 @@ export default {
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
column: [
{
label: '车船类型',
@@ -223,12 +257,10 @@ export default {
{
label: '里程/航程数',
prop: 'mileage',
type: 'number',
precision: 2,
type: 'input',
minWidth: 130,
slot: true,
span: 12,
append: '公里',
placeholder: '请输入',
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
@@ -255,9 +287,8 @@ export default {
{
label: '费用',
prop: 'cost',
type: 'number',
precision: 2,
append: '元',
type: 'input',
slot: true,
span: 12,
placeholder: '请输入',
rules: [
@@ -302,12 +333,10 @@ export default {
{
label: '下次保养里程/航程',
prop: 'nextMaintenanceMileage',
type: 'number',
precision: 2,
type: 'input',
minWidth: 170,
slot: true,
span: 12,
append: '公里',
placeholder: '请输入',
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
@@ -531,6 +560,13 @@ export default {
if (!value || typeof value === 'string') return value;
return JSON.stringify(value);
},
normalizeDecimalInput(prop, value) {
const sanitized = String(value ?? '').replace(/[^\d.]/g, '');
const [integerPart, ...decimalParts] = sanitized.split('.');
this.form[prop] = decimalParts.length
? `${integerPart || '0'}.${decimalParts.join('').slice(0, 2)}`
: integerPart;
},
initDeptTree() {
getDeptTree(this.userInfo.tenantId).then(res => {
const column = this.findColumn(this.option.column, 'createDept');

View File

@@ -70,6 +70,28 @@
class="maintenance-record-page__input"
/>
</template>
<template #costForm>
<el-input
v-model="form.cost"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('cost', value)"
>
<template #append></template>
</el-input>
</template>
<template #mileageForm>
<el-input
v-model="form.mileage"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('mileage', value)"
>
<template #append>{{ form.mileageUnit || '公里' }}</template>
</el-input>
</template>
<template #mileage="{ row }">
<span>{{ formatMileage(row.mileage, row.mileageUnit) }}</span>
</template>
@@ -167,10 +189,11 @@ export default {
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
column: [
{
label: '车船类型',
@@ -239,9 +262,8 @@ export default {
{
label: '费用',
prop: 'cost',
type: 'number',
precision: 2,
append: '元',
type: 'input',
slot: true,
span: 12,
placeholder: '请输入',
rules: [
@@ -287,10 +309,8 @@ export default {
{
label: '里程/航程数',
prop: 'mileage',
type: 'number',
precision: 2,
type: 'input',
minWidth: 130,
append: '公里',
slot: true,
span: 12,
placeholder: '请输入',
@@ -526,6 +546,13 @@ export default {
if (!value || typeof value === 'string') return value;
return JSON.stringify(value);
},
normalizeDecimalInput(prop, value) {
const sanitized = String(value ?? '').replace(/[^\d.]/g, '');
const [integerPart, ...decimalParts] = sanitized.split('.');
this.form[prop] = decimalParts.length
? `${integerPart || '0'}.${decimalParts.join('').slice(0, 2)}`
: integerPart;
},
initDeptTree() {
getDeptTree(this.userInfo.tenantId).then(res => {
const column = this.findColumn(this.option.column, 'createDept');

View File

@@ -82,6 +82,50 @@
class="mileage-record-page__input"
/>
</template>
<template #previousMonthMileageForm>
<el-input
v-model="form.previousMonthMileage"
:disabled="boxType === 'view'"
inputmode="numeric"
placeholder="请输入"
@input="value => normalizeIntegerInput('previousMonthMileage', value)"
>
<template #suffix>km</template>
</el-input>
</template>
<template #currentMonthMileageForm>
<el-input
v-model="form.currentMonthMileage"
:disabled="boxType === 'view'"
inputmode="numeric"
placeholder="请输入"
@input="value => normalizeIntegerInput('currentMonthMileage', value)"
>
<template #suffix>km</template>
</el-input>
</template>
<template #monthlyMileageForm>
<el-input
v-model="form.monthlyMileage"
:disabled="boxType === 'view'"
inputmode="numeric"
placeholder="请输入"
@input="value => normalizeIntegerInput('monthlyMileage', value)"
>
<template #suffix>km</template>
</el-input>
</template>
<template #totalMileageForm>
<el-input
v-model="form.totalMileage"
:disabled="boxType === 'view'"
inputmode="numeric"
placeholder="请输入"
@input="value => normalizeIntegerInput('totalMileage', value)"
>
<template #suffix>km</template>
</el-input>
</template>
<template #totalMileageStart-search="{ row }">
<div class="mileage-record-page__range">
<el-input
@@ -224,7 +268,6 @@ export default {
Object.keys(labelMap).forEach(prop => {
const column = this.findColumn(this.option.column, prop);
column.label = labelMap[prop];
column.append = unit;
});
vehicleTypeColumn.disabled = Boolean(this.form.id);
const unitColumn = this.findColumn(this.option.column, 'mileageUnit');
@@ -304,6 +347,9 @@ export default {
toMoney(value) {
return Math.round(Number(value) * 100) / 100;
},
normalizeIntegerInput(prop, value) {
this.form[prop] = String(value ?? '').replace(/\D/g, '');
},
normalizeRow(row) {
const values = { ...row };
values.vehicleType = values.vehicleType || '车辆';

View File

@@ -70,6 +70,48 @@
class="oil-electric-record-page__input"
/>
</template>
<template #quantityForm>
<el-input
v-model="form.quantity"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('quantity', value)"
/>
</template>
<template #unitPriceForm>
<el-input
v-model="form.unitPrice"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('unitPrice', value)"
>
<template #suffix></template>
</el-input>
</template>
<template #transactionAmountForm>
<el-input
v-model="form.transactionAmount"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('transactionAmount', value)"
>
<template #suffix></template>
</el-input>
</template>
<template #balanceForm>
<el-input
v-model="form.balance"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('balance', value)"
>
<template #suffix></template>
</el-input>
</template>
<template #attachments="{ row }">
<span>{{ formatAttachments(row.attachments) }}</span>
</template>
@@ -190,9 +232,7 @@ export default {
},
updateFeeType(feeType) {
const oilProductColumn = this.findColumn(this.option.column, 'oilProduct');
const unitPriceColumn = this.findColumn(this.option.column, 'unitPrice');
oilProductColumn.display = feeType !== '充电';
unitPriceColumn.append = feeType === '充电' ? '元/度' : '元/升';
if (feeType === '充电') {
this.form.oilProduct = undefined;
}
@@ -249,6 +289,13 @@ export default {
isEmpty(value) {
return value === undefined || value === null || value === '';
},
normalizeDecimalInput(prop, value) {
const sanitized = String(value ?? '').replace(/[^\d.]/g, '');
const [integerPart, ...decimalParts] = sanitized.split('.');
this.form[prop] = decimalParts.length
? `${integerPart || '0'}.${decimalParts.join('').slice(0, 2)}`
: integerPart;
},
normalizeRow(row) {
const values = { ...row };
values.vehicleType = values.vehicleType || '车辆';

View File

@@ -70,6 +70,17 @@
class="other-expense-record-page__input"
/>
</template>
<template #amountForm>
<el-input
v-model="form.amount"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('amount', value)"
>
<template #suffix></template>
</el-input>
</template>
<template #attachments="{ row }">
<span>{{ formatAttachments(row.attachments) }}</span>
</template>
@@ -234,6 +245,13 @@ export default {
isEmpty(value) {
return value === undefined || value === null || value === '';
},
normalizeDecimalInput(prop, value) {
const sanitized = String(value ?? '').replace(/[^\d.]/g, '');
const [integerPart, ...decimalParts] = sanitized.split('.');
this.form[prop] = decimalParts.length
? `${integerPart || '0'}.${decimalParts.join('').slice(0, 2)}`
: integerPart;
},
normalizeRow(row) {
const values = { ...row };
values.vehicleType = values.vehicleType || '车辆';

View File

@@ -70,6 +70,26 @@
class="tire-replacement-record-page__input"
/>
</template>
<template #tireQuantityForm>
<el-input
v-model="form.tireQuantity"
:disabled="boxType === 'view'"
inputmode="numeric"
placeholder="请输入"
@input="value => normalizeIntegerInput('tireQuantity', value)"
/>
</template>
<template #replacementCostForm>
<el-input
v-model="form.replacementCost"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('replacementCost', value)"
>
<template #suffix></template>
</el-input>
</template>
<template #attachments="{ row }">
<span>{{ formatAttachments(row.attachments) }}</span>
</template>
@@ -225,6 +245,16 @@ export default {
if (!value || typeof value === 'string') return value;
return JSON.stringify(value);
},
normalizeDecimalInput(prop, value) {
const sanitized = String(value ?? '').replace(/[^\d.]/g, '');
const [integerPart, ...decimalParts] = sanitized.split('.');
this.form[prop] = decimalParts.length
? `${integerPart || '0'}.${decimalParts.join('').slice(0, 2)}`
: integerPart;
},
normalizeIntegerInput(prop, value) {
this.form[prop] = String(value ?? '').replace(/\D/g, '');
},
normalizeRow(row) {
const values = { ...row };
if (values.vehicleNo) {

View File

@@ -90,6 +90,26 @@
placeholder="请输入船长"
/>
</template>
<template #fineAmountForm>
<el-input
v-model="form.fineAmount"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('fineAmount', value)"
>
<template #append></template>
</el-input>
</template>
<template #deductPointsForm>
<el-input
v-model="form.deductPoints"
:disabled="boxType === 'view'"
inputmode="numeric"
placeholder="请输入"
@input="value => normalizeIntegerInput('deductPoints', value)"
/>
</template>
<template #processStatus="{ row }">
<span :class="row.processStatus === '未处理' ? 'violation-record-page__danger' : ''">
{{ row.processStatus }}
@@ -141,6 +161,7 @@ import { getList as getVehicleList } from '@/api/transportCapacity/transport-veh
import { getList as getShipList } from '@/api/transportCapacity/transport-ship';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { getDeptTree } from '@/api/system/dept';
import { getDictionary } from '@/api/system/dictbiz';
import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
@@ -179,6 +200,7 @@ export default {
},
created() {
this.initDeptTree();
this.initViolationTypeOptions();
},
computed: {
...mapGetters(['permission', 'userInfo']),
@@ -229,6 +251,15 @@ export default {
column.dicData = res.data.data;
});
},
initViolationTypeOptions() {
getDictionary({ code: 'type_of_traffic_violation' }).then(res => {
const column = this.findColumn(this.option.column, 'violationType');
column.dicData = (res.data.data || []).map(item => ({
label: item.dictValue,
value: item.dictValue,
}));
});
},
updateVehicleTypeDisplays(vehicleType, oldValue) {
const vehicleTypeColumn = this.findColumn(this.option.column, 'vehicleType');
const vehicleNoColumn = this.findColumn(this.option.column, 'vehicleNo');
@@ -240,6 +271,17 @@ export default {
driverColumn.label = vehicleType === '船舶' ? '船长' : '驾驶人';
typeColumn.display = vehicleType !== '船舶';
itemColumn.display = vehicleType === '船舶';
typeColumn.rules =
vehicleType === '船舶'
? []
: [{ required: true, message: '请选择类型', trigger: 'change' }];
itemColumn.rules =
vehicleType === '船舶'
? [
{ required: true, message: '请输入事项', trigger: 'blur' },
{ max: 100, message: '最多 100 个字符', trigger: 'blur' },
]
: [{ max: 100, message: '最多 100 个字符', trigger: 'blur' }];
vehicleTypeColumn.disabled = Boolean(this.form.id);
if (!this.isDetailLoading && oldValue && vehicleType !== oldValue) {
this.form.vehicleNo = '';
@@ -315,6 +357,16 @@ export default {
if (!value || typeof value === 'string') return value;
return JSON.stringify(value);
},
normalizeDecimalInput(prop, value) {
const sanitized = String(value ?? '').replace(/[^\d.]/g, '');
const [integerPart, ...decimalParts] = sanitized.split('.');
this.form[prop] = decimalParts.length
? `${integerPart || '0'}.${decimalParts.join('').slice(0, 2)}`
: integerPart;
},
normalizeIntegerInput(prop, value) {
this.form[prop] = String(value ?? '').replace(/\D/g, '');
},
normalizeRow(row) {
const values = { ...row };
values.vehicleType = values.vehicleType || '车辆';
@@ -337,6 +389,14 @@ export default {
return values;
},
validateRow(row) {
if (row.vehicleType === '船舶' && !row.violationItem) {
this.$message.warning('请输入事项');
return false;
}
if (row.vehicleType !== '船舶' && !row.violationType) {
this.$message.warning('请选择类型');
return false;
}
if (
row.violationTime &&
new Date(row.violationTime.replace(/-/g, '/')).getTime() > Date.now()

View File

@@ -62,11 +62,11 @@ export default ({
server: {
port: 2888,
proxy: {
// 开发期同源代理:浏览器请求 /api/xxx -> 由 Vite 转发到 http://172.16.203.228:8000/api/xxx
// 必须保留 /api 前缀(不要 rewrite 去掉),与 openresty/nginx 期望的路径一致,从而规避跨域(CORS)问题
'/api': {
target: 'http://localhost',
//target: 'https://saber3.bladex.cn/api',
target: 'http://172.16.203.228:8000',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
},
},
},