Compare commits

..

36 Commits

Author SHA1 Message Date
04ab648f5b Merge remote-tracking branch 'websoft/master' 2026-08-07 13:15:44 +08:00
f550b55341 1、新增复评权限判断 2026-08-07 13:15:38 +08:00
5aefa23f50 fix(system): 修复部门管理操作列错位问题并统一客商档案弹窗风格
- 修复/system/dept页面操作列竖排错位问题,通过提升CSS选择器特异性并强制inline-flex布局
- 在department页面添加scoped样式,双保险保证操作列横排显示及间距正确
- 将客商档案/vehicle/customer-archive中的表单分组和弹窗内容统一改为section-card组件风格
- 清理废弃旧样式类,提升界面一致性及可维护性
- 调整相关弹窗中卡片内容结构,消除双层灰色背景,使弹窗风格与/business/project-apply一致
- 对评分、联系人、收款信息、发票信息、客商材料、变更记录和证明材料等模块使用全局section-card组件包裹
- 优化样式细节,确保按钮组进入卡头右侧,保证操作体验一致
- 验证构建成功及热更新生效,提醒用户发布需清除旧CSS缓存
2026-08-07 12:47:19 +08:00
c92d2a405a style(ui): 统一操作列文字字重和全站表格文字溢出提示
- 取消操作列内 el-link 默认加粗,统一为常规字体粗细,避免字体显得突兀
- 在 element-ui.scss 中新增样式覆盖 el-link 的 font-weight 为 normal
- 全站表格开启 Element Plus 全局配置 showOverflowTooltip,统一显示单元格文字省略提示
- Avue 全局 crudOption 和手写表格均启用 showOverflowTooltip,覆盖所有相关表格列
- 通过 CSS 豁免操作列单元格不受 tooltip 固定宽度裁切影响,保证操作列内部链接显示完整
- 调整部分菜单宽度以适配视觉和布局需求
- 验证构建成功,无新增 CSS 警告,确保兼容性和功能正常
2026-08-07 12:24:17 +08:00
be4ccc89f6 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/views/system/dept.vue
2026-08-07 12:02:41 +08:00
565553b3f7 refactor(table): 统一调整全仓表格操作列宽度及按钮样式
- 将所有操作按钮数量≤3的表格操作列宽度统一调整为180px
- 调整涉及全局默认值、各模块和页面中menuWidth显式设定
- 单独修改/authority/role页面操作列宽度为240px,/system/menu及流程配置对应调为220px
- 通过CSS新规则视觉统一Avue内置操作按钮为el-link链接样式,无功能变更
- 优化通知公告操作列宽度,解决三按钮链接换行问题
- 调整首页Logo图标尺寸及边距以适配设计需求
- 保证所有相关改动经过npm run build验证通过
2026-08-07 12:02:29 +08:00
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
6364b593ce style(business): 优化派遣摘要及路线项布局样式
- 调整派遣摘要网格列宽,改善整体布局比例
- 增加派遣路线容器间距,提升视觉分隔效果
- 修改派遣路线项最大宽度和内边距,提升内容展示空间
- 添加派遣路线项最小高度及虚线边框,增强区块感和可视性
- 调整派遣路线项边角圆角,优化界面圆润度
2026-08-07 01:41:43 +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
02b2e3b673 refactor(transportCapacity): 使用 section-card 重构司机管理弹窗布局
- 将司机管理页面新增/编辑弹窗中四个分组改为使用标准 section-card 组件包裹
- 删除手写的扁平样式和红色竖条,统一使用全局 element-ui 样式维护弹窗视觉风格
- 调整司机证件上传区尺寸为 240x151 px,保持身份证比例并左对齐显示
- 优化弹窗中各表单项布局,提高视觉层次与一致性
- 更新 scoped 样式,移除无用的 section-title 样式,保持代码清洁
2026-08-06 16:53:23 +08:00
53676a1150 refactor(vehicle): 优化customer-archive弹窗结构与样式
- 使用<section-card>替换旧版灰底分组块与内层白卡,统一表单分组样式
- 删除冗余的基本信息大标题,减少嵌套层级
- 将客商材料和变更记录区域改用<section-card>,操作按钮通过插槽放置卡头右侧
- 清理无效的scoped样式,简化样式管理
- 统一弹窗内部灰底背景并增加16px内边距,与project-apply保持一致
- 确认vue文件与sass编译无误,保证改动稳定性
2026-08-06 15:09:44 +08:00
5e92124710 Merge remote-tracking branch 'websoft/master' 2026-08-06 11:52:14 +08:00
8a262d9d41 style(ui): 统一弹窗分组白卡样式并改造相关弹窗布局
- 新增全局弹窗灰底背景色,提升视觉一致性
- 新增通用分组白卡组件 section-card,标题融入卡头,支持额外插槽
- 统一 Avue 弹窗表单及分组为白卡样式
- 调整 common-route.vue 弹窗,使用 section-card 分组包裹查询条件与常用地址
- 改造 loading-manage.vue 弹窗所有分区,采用 section-card 统一风格
- 全局样式调整弹窗内容区表单项间距及两列表单布局工具类
- 在 main.js 全局注册 section-card 组件,便于项目统一复用
2026-08-06 09:27:49 +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
819a59e6fc style(project-apply): 调整项目申请样式优化布局
- 将项目申请表单内边距改为0取消左右填充
- 修改弹窗主体溢出控制,新增横向溢出隐藏设置
- 调整弹窗主体内边距为16px统一四周间距
- 设置弹窗背景色为浅灰色提升视觉一致性
2026-08-05 21:13:38 +08:00
f663098b4a style(project-apply): 调整项目申请样式优化布局
- 将项目申请表单内边距改为0取消左右填充
- 修改弹窗主体溢出控制,新增横向溢出隐藏设置
- 调整弹窗主体内边距为16px统一四周间距
- 设置弹窗背景色为浅灰色提升视觉一致性
2026-08-05 21:11:35 +08:00
ef417a0290 style(ui): 优化分页箭头及project-apply弹窗样式
- 撤销分页中“上一页/下一页”按钮隐藏,恢复原生箭头图标显示
- 重新删除element-ui.scss中多余的分页箭头文字注入和display:none规则
- 全局Avue CRUD弹窗内容区背景改为灰色 #f5f6fa,提升视觉层次感
- project-apply弹窗背景改为灰色底,分组区块改为白色卡片风格,统一界面风格
- 分区标题与内容区域合并为单张白色卡片,调整圆角和阴影实现视觉统一
- 相应区域内边距、圆角和阴影细节优化,保持结构和功能不变
2026-08-05 20:53:48 +08:00
e5a7aca09e style(vehicle): 移除红色背景样式以优化界面显示
- 删除了 .archive-dialog 的红色背景色定义
- 保留了弹窗主体的最大高度和滚动样式
- 改善了归档对话框的视觉效果和用户体验
2026-08-05 18:25:24 +08:00
4d0cc2d595 feat(customer-archive): 优化客商档案弹窗基本信息分组UI
- 将单一el-row拆分为工商信息、联系信息、财务/信用信息、其他信息四个分组
- 每个分组外层使用灰色背景块,内层字段区为白底卡片,提升视觉层次感
- 字段顺序重新调整,保留全部字段并优化布局,联系信息三项均分一行
- 新增.archive-form__group样式,定义背景色、圆角、内外边距及阴影效果
- 复用全局.dialog-section-title样式作为分组标题样式,保持风格统一
- 确保表单校验正常,vue/compiler-sfc解析无错误,标签闭合平衡
- 在项目长期记忆新增设计约定,明确弹窗字段分组的UI方案及理由
2026-08-05 18:05:07 +08:00
f6451643e8 Merge remote-tracking branch 'origin/master' 2026-08-05 15:55:19 +08:00
8dddb72328 fix(cargo-type): 优化导入失败原因展示格式
- 移除导入失败原因中的“第N行”前缀,简化信息展示
- 失败原因编号从字母改为数字序号,提升可读性
- 备注超长提示编号改为数字编号,保持格式统一
- 保留导入失败原因列标红及列宽自动换行等已有样式调整
2026-08-05 15:55:11 +08:00
122 changed files with 7695 additions and 3311 deletions

View File

@@ -10,3 +10,54 @@
2. `vite.config.mjs``server.proxy['/api']` target 改为 `http://172.16.203.228:8000`,并去掉 `rewrite` 去掉 `/api` 的逻辑(保留 `/api` 前缀,对齐 openresty 期望路径)。
- 注意:改 `.env` / `vite.config.mjs` 后必须重启 `npm run dev`Vite 启动时加载环境变量,运行中改动不会热更新)。
- 若确需直连绝对地址(如生产/部署):必须让后端 CORS 把 Allow-Origin 改为具体前端域名blade-gateway 的 CorsConfig 用 `setAllowedOriginPatterns` 或显式域名),不可用 `*` 配合 credentials。
## 调整:货物类型导入失败原因展示格式
- 文件:`src/views/base/cargo-type.vue`
- 修改 `decorateCargoTypeFailDetail`
- 移除「第N行」前缀
- 失败原因编号从 `A. B. C.` 改为 `1. 2. 3.`
- 备注超长兜底提示也同步使用数字编号。
## UI 优化客商档案弹窗「基本信息」分组vehicle/customer-archive.vue
- 背景:原弹窗「基本信息」是一整块 `el-row` 平铺 20 个字段,信息密度过高。
- 方案(与用户确认):采用「分区标题平铺 + 浅色背景块」而非嵌套卡片(契合 AGENTS.md 去卡片化哲学;全局 `.dialog-section-title` 已自带 4px 主色竖条)。
- 改动:
- 模板:将单一 `el-row` 拆分为 4 个 `.archive-form__group`:工商信息 / 联系信息(3项 span=8) / 财务·信用信息 / 其他信息(备注 span=24);字段顺序重组但全部保留,地址、备注保持 span=24。
- 样式:新增 `.archive-form__group`#fafafa 背景、8px 圆角、16px/20px 内边距、组间距 16px子标题复用 `.dialog-section-title`14px/500
- 校验:@vue/compiler-sfc 解析 0 错误;标签闭合平衡。
-此前讨论中提及的「Tab 内容加内边距」「材料区标题竖条」未纳入本次(材料区已用 dialog-section-title如需可后续追加。
## 排查:分页「上一页/下一页原生箭头」样式被还原
- 现象昨日08-04已对 `src/styles/element-ui.scss` 三处历史遗留规则做删除(隐藏 `.el-icon` 箭头、用 `::before` 注入「上一页/下一页」文字、整体 `display:none` 隐藏 `.btn-prev/.btn-next`),今早用户反馈效果被覆盖。
- 排查:重新 Grep 确认 `element-ui.scss` 三处原隐藏规则全部恢复成原始状态;而 `src/utils/pagination.js` 的注释("统一展示为原生箭头图标")未被还原,说明只有该样式文件被外部覆盖(疑似 git 还原 / 重新拉取 / 某生成流程),并非逻辑层问题。
- 已重新逐条应用三处删除(单条提交,避开并行编辑的缓存冲突),并验证无遗留 `display:none`/`content:'上一页'``pagination.js` 无需再改。
- 提醒:`element-ui.scss` 若处于版本控制或生成流程中,可能被再次还原;如需长期生效,建议把该改动提交入 git或排查是否有自动覆盖该文件的脚本。
## UI 调整Avue CRUD 弹窗内容区背景改为灰色 #f5f6fa
- 文件:`src/styles/element-ui.scss` 第 225 行附近 `.avue-crud__dialog .el-dialog__body`
- 改动:在原 `max-height`/`overflow-y` 规则中新增 `background: #f5f6fa;`
- 范围:**全局生效**(所有 Avue CRUD 弹窗的内容区都会变灰。起因是用户想在货物类型cargo-type新增弹窗用灰底衬托。
- 注意:弹窗默认 `append-to-body`scoped 样式选不到;如需仅货物类型单页变灰,需给 option 加 `dialogCustomClass` 再单独写选择器。用户未要求收窄,暂保持全局。
## UI 改造project-apply 弹窗灰底+白卡片布局
- 文件:`src/views/business/project-apply.vue`
- 需求:弹窗背景色 `#f5f6fa`,每个分组区域用白色卡片
- 改动:
- 弹窗 bodyglobal 选择器):加 `background: #f5f6fa`
- `__grid``__table``__textarea-list``__material-head``__upload``__change-textarea` 统一加白底 + 6px 圆角 + 极淡阴影 + 内边距
- 效果:灰蓝底弹窗上浮白色分组卡片,与客商档案弹窗的「灰色分组块+白色内层卡片」方案一致
## 二次优化project-apply 分区标题也包进白色卡片
- 文件:`src/views/business/project-apply.vue`
- 需求:把「项目基本信息」这类分区标题也装进白色卡片内(标题+内容合一张白卡)
- 改动:
- `.dialog-section-title`(排除 material 标题)加白底 + 上圆角 `6px 6px 0 0` + `z-index:1`,紧接内容块
- `__grid`/`__table`/`__textarea-list`/`__change-textarea` 圆角改为下圆角 `0 0 6px 6px`,与标题拼接成完整白卡
- 材料区(`__material-head`/`__upload`)标题与按钮本就在白卡内,不受影响
- 纯 CSS 改动,未动模板结构;用 `:not()` 排除材料标题避免破坏既有布局

View File

@@ -0,0 +1,116 @@
# 2026-08-06 工作日志
## 全站表格操作列文字链接统一为蓝色
- 需求:客户档案页(/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 作强调色需另行排除。
## 运输计划调度弹窗 UX 分析
- 完成 `/business/transport-plan` 调度弹窗的 UX 审查,对比 project-apply 页面风格
- 输出 6 项改进方案P0-P5核心问题为缺少灰底(#f5f6fa)+白卡(section-card)分组设计模式
- 所有改动集中在 `business-crud-page.vue` 一个文件
- 待用户确认后实施第一阶段P0灰底 + P1白卡分组
## 已实施的改进(全部在 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 缺失),属环境预存问题,非本次引入
## 运输计划调度弹窗二次视觉紧凑化
- 客户反馈:弹窗“空、不紧凑、内容多但不好看”
- 改动仍集中在 `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` 成功
## /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` 未改。
## /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」约定按用户明确要求链接支持换行兜底。
## /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」约定按用户明确要求链接支持换行兜底。
## 操作列宽度的三种设置入口(备忘)
- 独立 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
## /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 个按钮会多行展示、不裁切。
## 修复 process-config.js 末尾语法错误(红色警)
- 现象:用户反馈 `src/option/business/process-config.js` 最后一行有红色警。
- 根因:上一轮把 `applyTableMenuWidth({...}, 5)` 去掉外层函数包裹时,结尾 `}, 5);` 被改成 `});`,多留了一个 `)`(因 `export const option = {` 本身没有左括号),导致括号不匹配。
- 改动:末尾 `});``};`。移除 `applyTableMenuWidth` import 时无误。
- 验证node 动态 import 不再报 SyntaxError仅剩 node 无法解析 extensionless `./common` 的模块解析提示,与语法无关),说明语法已恢复。
## /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 换行兜底。
## 车/船务管理模块下所有记录页操作列宽度统一为 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` 不在截图菜单内,保持不动。
## 车/船务管理模块操作列宽度 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

@@ -0,0 +1,110 @@
# 2026-08-07 工作日志
## 运输计划调度弹窗恢复左右分栏布局
- 需求:客户提供「计划详情」参考截图,要求调度弹窗改回「左侧基本信息卡 + 右侧收发货路线卡」的左右分栏布局。
- 改动:`src/views/business/components/business-crud-page.vue`
- 模板:把基本信息与收发货路线重新包进 `business-crud-page__dispatch-summary` 双列容器;左侧 `section-card` 内恢复 3 列网格(客户/项目/合同编号),货物信息占 2 列、计划执行时间占 1 列、附件独占一行;右侧 `section-card` 内恢复左右排列的起点/终点(徽章+名称+地址+联系人),中间用方向虚线连接。
- 样式:
- 外容器恢复 `grid-template-columns: minmax(0, 1.25fr) minmax(420px, 1fr)`gap 16px卡片等高拉伸。
- 基本信息网格恢复 3 列,列间距 16px、行间距 12px。
- 路线区恢复 `grid-template-columns: 1fr 56px 1fr`,徽章 32px标题 16px地址/联系人 13px连接线 56px。
- 保留紧凑化后的字号/间距(标签 12px、值 14px避免回到原先过松的状态。
- 验证:`npm run build` 成功。
## 运输计划调度弹窗基本信息改为 2 列
- 需求:客户截图反馈左侧「基本信息」还嫌松,要求改为 2 列更紧凑。
- 改动:`src/views/business/components/business-crud-page.vue`
- 模板:调整基本信息字段顺序为 客户/项目、合同编号/计划执行时间、货物信息(占满 2 列)、附件(占满 2 列)。
- 样式:`business-crud-page__dispatch-summary-grid``repeat(3, minmax(0, 1fr))` 改为 `repeat(2, minmax(0, 1fr))`,保持 gap 12px 16px。
- 验证:`npm run build` 成功。
## 运输计划调度弹窗路线连接改为车辆图标
- 需求:客户希望把收发货路线中间的方向虚线换成车辆图标。
- 改动:`src/views/business/components/business-crud-page.vue`
- 模板:把 `<div class="business-crud-page__dispatch-route-line" />` 替换为 `<div class="business-crud-page__dispatch-route-vehicle"><el-icon><Van /></el-icon></div>`
- 样式:删除原 `&__dispatch-route-line` 虚线箭头样式,新增 `&__dispatch-route-vehicle`,使用主色圆形背景、白色 `Van` 图标,尺寸 36×36px居中与路线节点对齐。
- 验证:`npm run build` 成功。
## 运输计划调度弹窗收发货路线视觉美化
- 需求:客户反馈右上角「收发货路线」卡片仍不够美观,要求继续优化。
- 改动:`src/views/business/components/business-crud-page.vue`
- 样式:
- 路线容器去掉 `padding-top`,改为 `align-items: center` 垂直居中。
- 起点/终点 item 改为 `align-items: center`,徽章从方角改为圆形,并加轻微阴影。
- 车辆图标改为白底 + 主色边框 + 主色 `Van` 图标,尺寸加大到 44×44px两侧各延伸 20px 虚线,形成「路线」流动感。
- 城市名称限制单行并溢出省略,地址/联系人间距收紧、颜色变淡。
- 验证:`npm run build` 成功。
## 通知公告 /desk/notice 操作列加宽
- 需求:客户截图反馈操作列中「查看、编辑、删除」三个文字链接被挤成两行,要求加宽。
- 改动:`src/views/desk/notice.vue`
- 在 Avue CRUD option 中新增 `menuWidth: 220`,使操作列可在一行容纳三个操作链接;符合项目样式规范「三个操作按钮时宽度不小于 220px」。
## 全仓表格操作列宽度统一≤3 按钮统一 180
- 需求:客户提供新截图,要求所有操作按钮 ≤3 个的表格操作列统一宽度为 180>3 个按钮保持 320。
- 核心改动:
- `src/utils/table-menu.js``TABLE_MENU_WIDTH``default``three` 都从 160/220 统一改为 180`four` 保持 320`resolveTableMenuWidth` 逻辑不变。
- `src/main.js`:全局 `crudOption.menuWidth` 从 160 改为 180覆盖未显式设置 menuWidth 的 Avue CRUD 页面。
- `src/views/desk/notice.vue``menuWidth` 从 220 改为 180。
- `src/option/business/process-config.js``menuWidth` 从 200 改为 180。
- `src/views/system/param.vue`:手写 el-table 操作列宽度从 220 改为 180。
- 批量替换:对 44 个显式设置 `menuWidth: 160``.vue` / `.js` / `const/*.js` 文件统一改为 180含部分文件内多处
- 验证:`npm run build` 成功exit code 0
## 角色管理 /authority/role 操作列改 180
- 需求:用户明确要求把 /authority/role 页面操作列宽度设为 180该页面此前设为 320因含「查看/编辑/删除 + 权限设置」自定义菜单)。
- 改动:`src/views/authority/role.vue`
- option 内联 `menuWidth: 320` 改为 `menuWidth: 180`
## 全站操作列按钮统一为链接el-link风格
- 需求:用户要求「操作栏里的按钮改成链接的方案,全站风格统一」,并先确认了对功能是否有影响。
- 调研结论(关键):全仓手写 el-table 操作列10 个文件、约 28 列)与 53 个 avue-crud 自定义 `#menu` 插槽**已全部是 `<el-link>`**,全库 `type="text"` 残留为 0真正未链接化的只剩下 **Avue 内置 viewBtn/editBtn/delBtn**(渲染为 `<el-button is-text>`,包在 `div.avue-crud__menu` 内,且自带 `permission` 指令与 `rowView/rowEdit/rowDel` 事件)。
- 实施手法:采用「方案 A纯 CSS 视觉对齐」(零功能影响、不动事件与权限,符合用户「不要影响功能」的诉求)。
- 改动:`src/styles/element-ui.scss`
- 新增 `.avue-crud__menu > .el-button.is-text` 规则,把 Avue 内置行操作按钮外观对齐为 el-link主色文字、无边框/无背景、hover 下划线 + 浅色、禁用态浅色无下划线、`height:auto / padding:0`(配合父级 `inline-flex; gap:8px` 与已存在的主色/间距覆盖规则)。
- 选择器仅命中行操作列 `.avue-crud__menu` 内的 `is-text` 按钮,不误伤顶部工具栏(`.avue-crud__header`)与弹窗按钮(`.avue-crud__dialog__menu`)。
- 注意:现有全局规则 `.avue-crud__menu .el-link` 会强制操作列内所有 el-link`type="danger"` 的删除)显示为主色蓝。如需删除按钮保留红色告警,需单独调整该覆盖规则(本次未改,保持统一蓝)。
- 验证:`npm run build` 成功exit code 0构建日志中的 CSS `*` 告警来自第三方库压缩,与本次无关(历史既有)。
## 操作列文字取消加粗(统一常规字重)
- 需求:用户要求操作栏下的按钮字体都不要加粗,统一风格。
- 根因Element Plus `el-link` 默认 `font-weight: var(--el-font-weight-primary)`500 中等粗),导致操作列里的链接(查看/编辑/删除等)显示为加粗;而 Avue 内置 `is-text` 按钮此前已被规则设为 normal。
- 改动:`src/styles/element-ui.scss`
- 在既有 `.avue-crud__menu .el-link, .el-table ... .cell:has(.el-link) .el-link` 统一规则中增加 `font-weight: normal;`(0,2,0) 优先级高于 el-link 自身 (0,1,0) 的默认字重,可覆盖)。
- 覆盖范围Avue 行菜单内的 el-link + 任意含 el-link 的表格体单元格手写操作列一次统一全站操作列字重Avue 内置 `is-text` 按钮与顶部工具栏/弹窗按钮字重保持原样不受影响。
- 验证:`npm run build` 成功exit code 0
## 全站表格省略文字 hover tooltip 提示统一
- 需求:用户截图反馈合同管理等页面中「所属项目」等列文字被省略,要求像 `/base/common-address` 一样,鼠标移上去显示完整提示;全站统一加,改完列出文件。
- 参考实现:`src/option/base/common-address.js` 中 Avue 列配置使用 `showOverflowTooltip: true`
- 调研结论:全站表格可见列约 848 个Avue+ 424 个手写 `<el-table-column>`,其中 801 个 Avue 列、268 个手写列未开启 tooltip。Element Plus 支持全局 `table: { showOverflowTooltip: true }` 配置Avue 3.9.2 完整透传,可一行代码覆盖全站。
- 改动文件清单:
- `src/main.js`
- Element Plus 全局配置新增 `table: { showOverflowTooltip: true }`,一次性覆盖所有 Avue CRUD 与手写 el-table。
- Avue 全局 `crudOption` 新增 `showOverflowTooltip: true` 作为双保险,避免页面级 option 被错误覆盖时失效。
- `src/styles/element-ui.scss`
- 新增 `.el-table .cell.avue-crud__menu, .el-table .el-table__body .cell.el-tooltip:has(> .el-link)` 规则,豁免操作列单元格的固定宽度与溢出隐藏,防止全局 tooltip 导致操作列内 el-link 换行后被裁切,保持原有 8px gap 与自动换行。
- `src/option/business/common.js`
- 共享工厂函数 `createCrudOption` 新增 `showOverflowTooltip: true`,覆盖 business 模块通过该工厂生成的所有 CRUD option。
- 覆盖范围:全站所有表格可见文本列(含 Avue 与手写 el-table`type="selection"` 选择列由 Element Plus 自动豁免;操作列通过 CSS 豁免不被影响。
- 验证:`npm run build` 成功exit code 0构建日志中 CSS `*` 告警来自第三方库压缩,与本次无关(历史既有)。
## 修复 /system/dept 操作栏错位(链接竖排)
- 现象:用户截图反馈机构管理页操作列「查看 / 编辑 / 删除 / 新增子项」竖排且偏右错位。
- 根因Avue 自带默认样式 `.avue-crud .avue-crud__menu{display:block}`(特异性 0,2,0与项目全局覆盖样式选择器完全相同项目原规则虽带 `!important`,但在 dev 模式 CSS 注入时序或浏览器缓存旧 element-ui.scss 时,`.avue-crud__menu` 退化成 block导致内部按钮/链接竖排。
- 修复:
- `src/styles/element-ui.scss`:把 `.avue-crud .avue-crud__menu` 改为重复类名 `.avue-crud .avue-crud__menu.avue-crud__menu`(特异性升到 0,3,0并新增 `width: 100% !important` 让操作列容器占满单元格、左对齐横排更稳。
- `src/views/system/dept.vue`scoped 样式新增 `:deep(.avue-crud__menu)` 兜底inline-flex + gap 8px + 子元素 inline-flex双保险确保本页操作列横排。
- 验证dev server 已热更新curl 确认两处编译结果均含 `display: inline-flex !important`;构建需用户硬刷新浏览器清旧 CSS 缓存。
## 客商档案 /vehicle/customer-archive 弹窗风格统一为 section-card
- 需求:用户截图反馈弹窗有「双层灰色背景」,且「工商信息」等标题没有放到卡片里面;要求改成和 `/business/project-apply` 一致的白卡风格。
- 根因:原页面主表单使用 `.archive-form__group` 灰色容器,内部再包一层白色 `.el-row` 卡片,标题坐在灰色背景上,形成外层 dialog body 灰 + 分组灰的「双层灰」;客商材料、变更记录、评分材料、发票/邮寄信息也沿用旧的分节标题样式。
- 改动:`src/views/vehicle/customer-archive.vue`
- 主表单 4 个分组改为 `<section-card title="工商信息 / 联系信息 / 财务/信用信息 / 其他信息">` 全局白卡组件,删除外层「基本信息」大标题。
- 评分/联系人/收款/发票 4 个 Tab 内容改为 `<section-card>` + `<template #extra>`,按钮组进入卡头右侧。
- 客商材料改为 `<section-card title="客商材料" #extra>`,变更记录改为 `<section-card title="变更记录">`
- 发票信息弹窗的「发票信息 / 邮寄信息」改为两张 `<section-card>`
- 评分详情弹窗的「证明材料」改为 `<section-card title="证明材料" class="score-material-card">`
- 清理不再使用的样式:`.archive-form__group``.section-title``.section-head``.customer-material-section``.change-record-section``.invoice-form__section-title``.score-material-section` 等。
- 验证:`npm run build` 成功exit code 0

View File

@@ -0,0 +1,66 @@
# 项目长期记忆tms-erp-web-ws / Saber3 客商模块)
## 设计约定(经用户确认的决策)
### 弹窗内多字段表单分组:使用全局 `<section-card>` 组件(项目统一标准)
**整体结构**
- 整个弹窗 body 由 `src/styles/element-ui.scss` 全局 `.el-dialog__body { background: #f5f6fa; padding: 16px }` 统一灰底(无需页面再覆盖)。
- 每个业务分组 = 一张白底卡片。用全局组件 `<section-card title="分组名">...</section-card>`,自动渲染 4px 主色竖条 + 8px 间距的卡头与白底 body圆角 6px + 极淡阴影),与 `loading-manage.vue` / `customer-archive.vue` 等已落地页面一致。
- 严禁再手写 `.xxx-form__block` + `.dialog-section-title` override 拼凑卡片(项目已有 `<section-card>` 标准组件)。
**HTML 结构**(每组三件套):
```vue
<section-card title="分组标题">
<el-row :gutter="18">
<el-col :span="8">...</el-col>
</el-row>
<!-- 同一组的所有 row 都放同一张白卡内 -->
</section-card>
```
**适用**:新增/编辑弹窗中字段超过 ~10 个、需按业务语义分组时。
### 简单分组(仅 3 项)
- 联系信息等仅 3 项的分组,可用 `el-col :span="8"` 让三项均分一行,避免 4 列网格留白。
### 上传证件区域尺寸规范transportCapacity 模块强制)
- **比例**:中国二代身份证标准尺寸 85.6 × 53.98 mm → 宽:高 = **1.586 : 1**(横放)
- **尺寸**:取真实身份证的 ~75% → 固定 **240 × 151 px**1.586:1 精确151 × 1.586 ≈ 240
- **位置**:在 el-col 列内 `margin: 0` 左对齐显示(不再 `width: 100%` 占满整栏,也不再居中)
- **适用范围**:所有 `<image-upload-field large>` 上传区,包括身份证正反面、大头照、机动车行驶证主页正/反面/副页正/反面、道路运输证、机动车登记本、船舶所有权/安全环保/国籍/最低安全配员/光船租赁/营业运输 等所有 cert 图片
- **实现方式**:在每个页面 scoped 样式里覆盖 class-prefix 对应的 `--large` 选择器(`.driver-uploader--large` / `.vehicle-uploader--large` / `.ship-uploader--large`
```scss
:deep(.xxx-uploader) { display: block; } // 不再 width:100%
:deep(.xxx-uploader--large) {
display: block;
width: 240px;
margin: 0;
}
:deep(.xxx-uploader--large .el-upload),
:deep(.xxx-uploader--large.el-upload) {
width: 100%;
height: 151px;
}
```
- **CSS 顺序要点**`.xxx-uploader`(特定性 0,0,1,0必须写在 `.xxx-uploader--large`(同为 0,0,1,0之前源码位置决定覆盖生效。
### 上传图片点击放大预览(全局组件 `image-upload-field` 已实现)
- **组件**`src/components/image-upload-field/main.vue`。原裸 `<img>` 改为 `<el-image :preview-src-list="[value]" fit="contain" preview-teleported :z-index="3000" @click.stop>`。点击图片即全屏预览(滚轮缩放 / 旋转 / 下载),零额外依赖。
- **预览与上传分离**:图片 `@click.stop` 阻止冒泡到 `el-upload`(否则会触发重传);"更换"入口form 模式图片下方 `el-link`、card 模式 head 的 `actionText`)通过 `el-upload` 内部隐藏 `input.el-upload__input` 的 `.click()` 触发文件框。
- **为什么用 DOM click**Element Plus `el-upload` 仅 expose `abort/submit/clearFiles/handleStart/handleRemove`**没有** `openFileDialog`,故用 `this.$refs.uploadRef.$el.querySelector('.el-upload__input').click()`。
- **readonly 模式**:仅渲染 `el-image` 预览,不显示上传入口(查看档案时也能放大看证件)。
- **z-index 注意**:务必 `preview-teleported`(预览层挂 body+ `:z-index="3000"`,否则会被 `el-dialog` 遮罩2000 区间)盖住。
- **适用范围**全仓通用组件driver / vehicle / ship 三个页面自动获得该能力(改动一次组件即可)。
## 关键文件
- 通用分组白卡组件:`src/components/section-card/main.vue`(已全局注册为 `<section-card>`,支持 `title` / `#title` / `#extra` slot
- 弹窗灰底 body + section-card 全局样式:`src/styles/element-ui.scss``.el-dialog__body`、`.section-card { &__header / &__bar / &__title / &__extra / &__body }`)。
- 司机档案弹窗:`src/views/transportCapacity/driver.vue`(手写 `el-form` + `<section-card>` 4 大分组:基础身份信息 / 驾驶证信息 / 从业资格证信息 / 司机联系信息)。
- 车辆档案弹窗:`src/views/transportCapacity/vehicle.vue`(手写 `el-form` + `<section-card>` 2 大分组:基础车辆信息 / 车辆资质图片6 张大图按身份证比例 240×151 左对齐)。
- 船舶档案弹窗:`src/views/transportCapacity/ship.vue`(手写 `el-form` + `<section-card>` 2 大分组:基础船舶信息 / 船舶证书信息;内部 6 张 cert 图片按身份证比例 240×151 左对齐;子证书分段用 `.cert-block` + border-top
- 客商档案弹窗:`src/views/vehicle/customer-archive.vue`(工商信息 / 联系信息 / 财务/信用信息 / 其他信息 等分组也用 `<section-card>`)。
- 项目补录弹窗:`src/views/business/project-apply.vue`(手写 `__grid` / `__table` / `__textarea-list` 同款白卡结构,先于 `<section-card>` 组件化之前落地,可视为视觉参照)。
## 环境注意
- Vite 代理跨域Saber3 axios 开 `withCredentials=true`,直连绝对地址后端须让 CORS `Allow-Origin` 为具体域名(不能用 `*`),否则浏览器拒绝。改 `.env`/`vite.config.mjs` 必须重启 dev。

View File

@@ -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

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

View File

@@ -0,0 +1,28 @@
<template>
<div class="section-card">
<div v-if="title || $slots.title || $slots.extra" class="section-card__header">
<span class="section-card__bar" />
<span class="section-card__title">
<slot name="title">{{ title }}</slot>
</span>
<span class="section-card__extra">
<slot name="extra" />
</span>
</div>
<div class="section-card__body">
<slot />
</div>
</div>
</template>
<script>
export default {
name: 'SectionCard',
props: {
title: {
type: String,
default: ''
}
}
}
</script>

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

@@ -147,7 +147,7 @@ export const option = {
viewBtn: true,
grid: true,
selection: true,
menuWidth: 160,
menuWidth: 180,
column: [
{
label: '数据源',

View File

@@ -39,6 +39,8 @@ import codeSetting from './views/tool/codesetting.vue';
import formSetting from './views/tool/formsetting.vue';
import tenantPackage from './views/system/tenantpackage.vue';
import tenantDatasource from './views/system/tenantdatasource.vue';
// 通用弹窗分组白卡(灰底弹窗内使用,标题在卡内)
import sectionCard from './components/section-card/main.vue';
window.$crudCommon = crudCommon;
debug();
@@ -61,6 +63,7 @@ app.component('codeSetting', codeSetting);
app.component('formSetting', formSetting);
app.component('tenantPackage', tenantPackage);
app.component('tenantDatasource', tenantDatasource);
app.component('sectionCard', sectionCard);
app.config.globalProperties.$app = app;
app.config.globalProperties.$dayjs = dayjs;
app.config.globalProperties.website = website;
@@ -72,6 +75,7 @@ app.use(store);
app.use(router);
app.use(ElementPlus, {
locale: messages[language],
table: { showOverflowTooltip: true },
});
app.use(Avue, {
axios,
@@ -82,7 +86,8 @@ app.use(Avue, {
searchLabelWidth: 160,
searchMenuSpan: 24,
searchMenuPosition: 'right',
menuWidth: 160,
menuWidth: 180,
showOverflowTooltip: true,
viewBtnIcon: ' ',
editBtnIcon: ' ',
copyBtnIcon: ' ',

View File

@@ -48,7 +48,7 @@ export const getCargoTypeOption = ctx => ({
editBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
column: [
{
label: '类型',
@@ -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 = [
@@ -202,7 +204,8 @@ export const createCrudOption = columns => ({
delBtn: false,
dialogClickModal: false,
menuFixed: 'right',
menuWidth: 160,
menuWidth: 180,
showOverflowTooltip: true,
column: columns,
});

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,48 +73,98 @@ export const option = applyTableMenuWidth({
rules: textRule('项目', 1000, true),
},
{
label: '包含过程节点',
label: '过程节点',
prop: 'includedNodes',
search: true,
searchOrder: 3,
type: 'select',
multiple: true,
dicData: nodeOptions,
minWidth: 180,
rules: selectRule('包含过程节点'),
minWidth: 260,
overHidden: false,
slot: true,
addDisplay: false,
editDisplay: false,
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: '',
@@ -127,22 +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%',
searchSpan: 8,
}, 5);
menuWidth: 220,
};

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

@@ -718,5 +718,5 @@ export const option = {
]),
dialogWidth: '96%',
dialogTop: '10px',
menuWidth: 320,
menuWidth: 280,
};

View File

@@ -130,7 +130,7 @@ export default {
selection: true,
grid: true,
labelWidth: 'auto',
menuWidth: 160,
menuWidth: 180,
dialogWidth: 1200,
dialogClickModal: false,
tabs: true,

View File

@@ -13,7 +13,7 @@ export default {
selection: true,
grid: true,
labelWidth: 'auto',
menuWidth: 160,
menuWidth: 180,
dialogClickModal: false,
column: [
{

View File

@@ -22,7 +22,7 @@ export const authLockOption = {
viewBtn: false,
editBtn: false,
delBtn: false,
menuWidth: 160,
menuWidth: 180,
dialogType: 'drawer',
dialogWidth: 800,
dialogClickModal: false,

View File

@@ -21,7 +21,7 @@ export const authLogOption = {
editBtn: false,
delBtn: false,
menu: true,
menuWidth: 160,
menuWidth: 180,
dialogType: 'drawer',
dialogWidth: 800,
dialogClickModal: false,

View File

@@ -11,7 +11,7 @@ export const optionParent = {
index: true,
selection: true,
viewBtn: true,
menuWidth: 160,
menuWidth: 180,
dialogWidth: 880,
dialogClickModal: false,
column: [
@@ -104,7 +104,7 @@ export const optionChild = {
index: true,
selection: true,
viewBtn: true,
menuWidth: 160,
menuWidth: 180,
dialogWidth: 880,
dialogClickModal: false,
column: [

View File

@@ -11,7 +11,7 @@ export const optionParent = {
index: true,
selection: true,
viewBtn: true,
menuWidth: 160,
menuWidth: 180,
dialogWidth: 880,
dialogClickModal: false,
column: [
@@ -105,7 +105,7 @@ export const optionChild = {
index: true,
selection: true,
viewBtn: true,
menuWidth: 160,
menuWidth: 180,
dialogWidth: 880,
dialogClickModal: false,
column: [

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,
},
],
},
@@ -365,7 +359,7 @@ export const platformOption = {
selection: true,
viewBtn: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
editBtnText: '配置',
column: [
{

View File

@@ -14,7 +14,7 @@ export const codeOption = {
index: true,
selection: true,
labelWidth: 'auto',
menuWidth: 160,
menuWidth: 180,
viewBtn: true,
dialogClickModal: false,
tabs: true,

View File

@@ -14,7 +14,7 @@ export default {
selection: true,
labelWidth: 'auto',
searchLabelWidth: 160,
menuWidth: 160,
menuWidth: 180,
dialogClickModal: false,
column: [
{

View File

@@ -14,7 +14,7 @@ export default {
selection: true,
labelWidth: 'auto',
searchLabelWidth: 160,
menuWidth: 160,
menuWidth: 180,
dialogWidth: 500,
dialogClickModal: false,
column: [

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: 180,
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

@@ -42,7 +42,7 @@ export default {
justify-content: center;
}
.logo-img {
width: 40px;
margin-top: 5px;
width: 30px;
margin-top: 10px;
}
</style>

View File

@@ -207,6 +207,102 @@
justify-content: center;
}
// =====================================================================
// 弹窗标准(与 business/project-apply 弹窗视觉一致)
// 灰底弹窗体 + 分组白卡标题融入卡头4px 主色竖条 + 8px 间距)
// =====================================================================
// 1) 所有弹窗内容区统一灰底 + 16px 内边距(与 project-apply 一致,避免内容贴边)
.el-dialog__body {
background-color: #f5f6fa;
padding: 16px;
}
// 2) Avue 默认弹窗表单自动包成白卡,无需逐页改 option 即统一标准
.el-dialog .avue-form {
background: #fff;
border-radius: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
padding: 16px;
}
// 3) Avue 弹窗内分组option.group渲染为白卡标题在卡内
.el-dialog .avue-form__group {
background: #fff;
border-radius: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
margin-bottom: 16px;
overflow: hidden;
.avue-group__header {
border-bottom: none;
padding: 14px 16px 4px;
font-weight: 500;
color: #303133;
}
.avue-group__item {
padding: 14px 16px 4px;
}
}
// 4) 弹窗内表单项默认上下间距 16px覆盖默认 18px / 22px
.el-dialog .el-form-item {
margin-bottom: 16px;
}
// 5) 通用分组白卡组件(<section-card>):标题融入卡头
.section-card {
background: #fff;
border-radius: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
margin-bottom: 16px;
overflow: hidden;
&:last-child {
margin-bottom: 0;
}
&__header {
display: flex;
align-items: center;
gap: 8px;
padding: 14px 16px 4px;
font-weight: 500;
color: #303133;
}
&__bar {
flex: none;
width: 4px;
height: 16px;
border-radius: 2px;
background-color: var(--el-color-primary);
}
&__title {
flex: 1;
min-width: 0;
}
&__extra {
margin-left: auto;
flex: none;
}
&__body {
padding: 14px 16px 4px;
}
}
// 6) 弹窗内两列表单栅格工具类(与 project-apply 一致)
.form-grid-2 {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
column-gap: 72px;
row-gap: 0;
}
.avue-crud__dialog .avue-dialog__footer > .el-button {
margin-left: 12px;
}
@@ -225,6 +321,7 @@
.avue-crud__dialog .el-dialog__body {
max-height: calc(100vh - 200px);
overflow-y: auto;
//background: #f5f6fa;
}
// 统一表格线条颜色
@@ -267,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 {
@@ -337,12 +447,7 @@
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.el-pagination .btn-prev,
.el-pagination .btn-next,
.erp-pagination-first,
.erp-pagination-last {
min-width: 54px;
}
// 上一页 / 下一页 箭头按钮与页码按钮等宽,不再单独加宽
.el-pagination .el-pager {
gap: 6px;
@@ -350,18 +455,7 @@
padding: 0;
}
.el-pagination .btn-prev .el-icon,
.el-pagination .btn-next .el-icon {
display: none;
}
.el-pagination .btn-prev::before {
content: '上一页';
}
.el-pagination .btn-next::before {
content: '下一页';
}
// 上一页 / 下一页 改为原生箭头图标:不再隐藏原生图标,也不再注入「上一页 / 下一页」文字
.el-pagination.is-background .el-pager li.is-active,
.el-pagination .el-pager li.is-active {
@@ -447,14 +541,7 @@
color: #fff;
}
// 全站统一:隐藏分页的「上一页 / 下一页」原生导航按钮
// (「首页 / 尾页」由 src/utils/pagination.js 的全局增强器统一不再注入)
.el-pagination {
.btn-prev,
.btn-next {
display: none !important;
}
}
// 全站统一:分页展示「上一页 / 下一页」原生箭头图标(不再隐藏;首页 / 尾页仍不注入)
// 全站统一:隐藏操作栏按钮前的图标
.avue-crud__menu .el-button--text i,
@@ -462,16 +549,49 @@
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 自带 .avue-crud .avue-crud__menu{display:block}(特异性同为 0,2,0
// 此处重复类名升至 0,3,0 并强制 display:inline-flex !important彻底压过 Avue 内置样式,
// 避免 dev 模式下 CSS 注入顺序导致 .avue-crud__menu 退化成 block 而让操作项竖排错位。
.avue-crud .avue-crud__menu.avue-crud__menu {
display: inline-flex !important;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: start;
width: 100% !important;
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 {
// 覆盖 Element Plus el-link 默认的 font-weight: var(--el-font-weight-primary)500 中等粗),
// 操作列文字统一为常规字重,避免「查看/编辑/删除」等链接显得加粗。
font-weight: normal;
--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;
@@ -483,6 +603,44 @@
font-weight: normal;
}
// 全站统一Element Plus 全局开启 show-overflow-tooltip 后,
// 操作列单元格会被加上 .el-tooltip 类并写死宽度,导致内部 el-link 换行后被裁切。
// 此处豁免操作列单元格:让 Avue 行菜单与手写表格操作列保持原有 flex 自动换行,不被固定宽度裁切。
.el-table .cell.avue-crud__menu,
.el-table .el-table__body .cell.el-tooltip:has(> .el-link) {
width: auto !important;
overflow: visible;
text-overflow: clip;
}
// 全站统一Avue 内置行操作按钮(查看 / 编辑 / 删除 / 复制等)视觉对齐为 el-link 链接样式。
// 选择器仅命中表格行操作列 .avue-crud__menu 内的 is-text 按钮,不误伤顶部工具栏(.avue-crud__header
// 与弹窗按钮(.avue-crud__dialog__menu。事件rowView/rowEdit/rowDel与权限permission 指令)
// 由 Avue 自带,此处只改外观,零功能影响。
.avue-crud__menu > .el-button.is-text {
height: auto;
padding: 0;
border: none;
background: transparent;
color: var(--el-color-primary);
font-weight: normal;
&:hover,
&:focus,
&:active {
background: transparent;
color: var(--el-color-primary-light-3);
text-decoration: underline;
}
&.is-disabled,
&.is-disabled:hover {
background: transparent;
color: var(--el-color-primary-light-5);
text-decoration: none;
}
}
@media (max-width: 768px) {
.avue-crud__pagination .el-pagination,
.empty-pagination .el-pagination,

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

@@ -1,6 +1,6 @@
export const TABLE_MENU_WIDTH = {
default: 160,
three: 220,
default: 180,
three: 180,
four: 320,
};

View File

@@ -321,7 +321,7 @@ export default {
height: 'auto',
calcHeight: 32,
dialogWidth: 700,
menuWidth: 160,
menuWidth: 180,
tip: false,
searchShow: true,
searchMenuSpan: 24,
@@ -500,7 +500,7 @@ export default {
editBtn: false,
addBtn: false,
delBtn: false,
menuWidth: 160,
menuWidth: 180,
dialogType: 'drawer',
dialogWidth: 800,
column: [

View File

@@ -164,7 +164,7 @@ export default {
selection: true,
labelWidth: 'auto',
searchLabelWidth: 160,
menuWidth: 160,
menuWidth: 180,
dialogWidth: 900,
dialogClickModal: false,
column: [
@@ -275,7 +275,7 @@ export default {
editBtn: false,
addBtn: false,
delBtn: false,
menuWidth: 160,
menuWidth: 180,
dialogClickModal: false,
column: [
{
@@ -336,7 +336,7 @@ export default {
viewBtn: true,
selection: true,
labelWidth: 'auto',
menuWidth: 160,
menuWidth: 180,
dialogWidth: 900,
dialogClickModal: false,
column: [

View File

@@ -165,7 +165,7 @@ export default {
selection: true,
labelWidth: 'auto',
searchLabelWidth: 160,
menuWidth: 160,
menuWidth: 180,
dialogWidth: 900,
dialogClickModal: false,
column: [
@@ -312,7 +312,7 @@ export default {
editBtn: false,
addBtn: false,
delBtn: false,
menuWidth: 160,
menuWidth: 180,
dialogClickModal: false,
column: [
{
@@ -375,7 +375,7 @@ export default {
viewBtn: true,
selection: true,
labelWidth: 'auto',
menuWidth: 160,
menuWidth: 180,
dialogWidth: 900,
dialogClickModal: false,
column: [

View File

@@ -211,7 +211,7 @@ export default {
index: true,
viewBtn: true,
selection: true,
menuWidth: 160,
menuWidth: 180,
dialogClickModal: false,
column: [
{
@@ -366,7 +366,7 @@ export default {
editBtn: false,
addBtn: false,
delBtn: false,
menuWidth: 160,
menuWidth: 180,
dialogType: 'drawer',
dialogWidth: 800,
column: [

View File

@@ -250,7 +250,7 @@ export default {
viewBtn: true,
labelWidth: 'auto',
searchLabelWidth: 160,
menuWidth: 160,
menuWidth: 240,
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

@@ -253,7 +253,7 @@ export default {
editBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
column: [
{
label: '编码',
@@ -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"
>
@@ -160,7 +160,7 @@ const CARGO_TYPE_FAIL_COL_WIDTH = {
/**
* 加工后端返回的导入失败明细 Excel
* - 「导入失败原因」列按 A. B. C. 编号列出,多条以分号隔开
* - 「导入失败原因」列按 1. 2. 3. 编号列出,多条以分号隔开
* - 每个错误后追加对应规则说明,仅失败原因列标红
* - 适当加宽各列、原因列自动换行,避免内容拥挤
*/
@@ -197,7 +197,7 @@ export const decorateCargoTypeFailDetail = async workbook => {
.filter(Boolean);
if (parts.length === 0) return;
const decorated = parts.map((part, index) => {
const label = `${String.fromCharCode(65 + index)}.`; // A. B. C.
const label = `${index + 1}.`; // 1. 2. 3.
const rule = CARGO_TYPE_FAIL_RULES.find(r => part.includes(r.match));
if (rule) {
// 规则说明若已包含在错误文本中则不再重复拼接
@@ -212,10 +212,10 @@ export const decorateCargoTypeFailDetail = async workbook => {
row.getCell(remarkCol).value == null ? '' : row.getCell(remarkCol).value
).trim();
if (remarkVal.length > 200 && !decorated.some(d => d.includes('备注不能超过200字'))) {
decorated.push(`${String.fromCharCode(65 + decorated.length)}.备注不能超过200字`);
decorated.push(`${decorated.length + 1}.备注不能超过200字`);
}
}
reasonCell.value = `${rowNumber}行:${decorated.join('')}`;
reasonCell.value = decorated.join('');
// 仅操作 G 列:黑色字体、无背景填充,其余列保持后端原样
reasonCell.fill = { type: 'pattern', pattern: 'none' };
reasonCell.font = { color: { argb: 'FF000000' } };
@@ -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

@@ -170,7 +170,7 @@ export default {
editBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
menuFixed: 'right',
column: [
{

View File

@@ -101,7 +101,7 @@ export default {
editBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
column: [
{
label: '中文名称',

View File

@@ -141,7 +141,7 @@ export default {
editBtn: false,
selection: false,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
column: [
{
label: '费用类型',

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;
}
}
@@ -381,15 +387,44 @@ export default {
editBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
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

@@ -219,7 +219,7 @@ export default {
editBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 160,
menuWidth: 180,
column: [
{
label: '编码',
@@ -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
@@ -141,103 +166,149 @@
class="common-route-page__address-dialog"
@opened="loadAddressList"
>
<div class="common-route-page__address-search">
<el-form :model="addressQuery" label-position="right" label-width="86px">
<el-row :gutter="16">
<el-col :span="6">
<el-form-item label="地址名称">
<el-input v-model="addressQuery.addressName" clearable placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="地址类型">
<el-select
v-model="addressQuery.addressType"
clearable
placeholder="请选择"
style="width: 100%"
>
<el-option
v-for="item in addressTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细地址">
<el-input v-model="addressQuery.detailAddress" clearable placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="6" class="common-route-page__address-search-actions">
<el-button type="primary" @click="handleAddressSearch">查询</el-button>
<el-button @click="handleAddressReset">重置</el-button>
</el-col>
<el-col :span="6">
<el-form-item label="组织">
<el-select
v-model="addressQuery.deptId"
clearable
filterable
placeholder="请选择"
style="width: 100%"
>
<el-option
v-for="item in deptOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<section-card title="查询条件">
<div class="common-route-page__address-search">
<el-form :model="addressQuery" label-position="right" label-width="86px">
<el-row :gutter="16">
<el-col :span="6">
<el-form-item label="地址名称">
<el-input v-model="addressQuery.addressName" clearable placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="地址类型">
<el-select
v-model="addressQuery.addressType"
clearable
placeholder="请选择"
style="width: 100%"
>
<el-option
v-for="item in addressTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细地址">
<el-input v-model="addressQuery.detailAddress" clearable placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="6" class="common-route-page__address-search-actions">
<el-button type="primary" @click="handleAddressSearch">查询</el-button>
<el-button @click="handleAddressReset">重置</el-button>
</el-col>
<el-col :span="6">
<el-form-item label="组织">
<el-select
v-model="addressQuery.deptId"
clearable
filterable
placeholder="请选择"
style="width: 100%"
>
<el-option
v-for="item in deptOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</section-card>
<el-table
border
:data="addressData"
v-loading="addressLoading"
height="310"
highlight-current-row
@row-click="selectAddressRow"
>
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column prop="addressName" label="地址名称" min-width="150" />
<el-table-column prop="addressCode" label="地址编号" min-width="150" />
<el-table-column prop="addressType" label="类型" min-width="150" />
<el-table-column label="站点编码" min-width="170">
<template #default="{ row }">
{{ row.addressType === '常规地址' ? '/' : row.siteCodeDisplay || row.siteCode || '' }}
</template>
</el-table-column>
<el-table-column prop="detailAddress" label="详细地址" min-width="220" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-link type="primary" @click.stop="selectAddressRow(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<section-card title="常用地址">
<el-table
border
:data="addressData"
v-loading="addressLoading"
height="310"
highlight-current-row
@row-click="selectAddressRow"
>
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column prop="addressName" label="地址名称" min-width="150" />
<el-table-column prop="addressCode" label="地址编号" min-width="150" />
<el-table-column prop="addressType" label="类型" min-width="150" />
<el-table-column label="站点编码" min-width="170">
<template #default="{ row }">
{{ row.addressType === '常规地址' ? '/' : row.siteCodeDisplay || row.siteCode || '' }}
</template>
</el-table-column>
<el-table-column prop="detailAddress" label="详细地址" min-width="220" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-link type="primary" @click.stop="selectAddressRow(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<div class="common-route-page__address-pagination">
<el-pagination
v-model:current-page="addressPage.currentPage"
v-model:page-size="addressPage.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, prev, pager, next, sizes"
:total="addressPage.total"
@size-change="handleAddressSizeChange"
@current-change="handleAddressCurrentChange"
/>
</div>
<div class="common-route-page__address-pagination">
<el-pagination
v-model:current-page="addressPage.currentPage"
v-model:page-size="addressPage.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, prev, pager, next, sizes"
:total="addressPage.total"
@size-change="handleAddressSizeChange"
@current-change="handleAddressCurrentChange"
/>
</div>
</section-card>
<template #footer>
<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>
@@ -247,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: {},
@@ -266,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,
@@ -313,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 => {
@@ -400,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 => {
@@ -609,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>
@@ -650,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>

View File

@@ -358,8 +358,8 @@
label-width="auto"
class="loading-manage-dialog__form"
>
<div class="dialog-section-title">配载基本信息</div>
<div class="loading-manage-dialog__grid">
<section-card title="配载基本信息">
<div class="loading-manage-dialog__grid">
<el-form-item label="配载单号">
<el-input v-model="dialogForm.loadingNo" disabled placeholder="系统自动生成" />
</el-form-item>
@@ -449,12 +449,13 @@
/>
</el-form-item>
</div>
</section-card>
<div class="dialog-section-title loading-manage-dialog__title-action">
<span>筛选待配载运单</span>
<el-link v-if="!dialogReadonly" type="primary" @click="loadCandidateWaybills">筛选</el-link>
</div>
<div class="loading-manage-dialog__candidate-search" v-if="candidateSearchVisible">
<section-card title="筛选待配载运单">
<template #extra>
<el-link v-if="!dialogReadonly" type="primary" @click="loadCandidateWaybills">筛选</el-link>
</template>
<div class="loading-manage-dialog__candidate-search" v-if="candidateSearchVisible">
<el-row :gutter="16">
<el-col :span="6">
<el-form-item label="运单号">
@@ -733,13 +734,14 @@
@current-change="handleCandidateCurrentChange"
/>
</div>
</section-card>
<div class="dialog-section-title">待配载列表</div>
<el-table
:data="selectedWaybillRows"
border
class="loading-manage-dialog__selected-table"
>
<section-card title="待配载列表">
<el-table
:data="selectedWaybillRows"
border
class="loading-manage-dialog__selected-table"
>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="waybillNo" label="运单号" min-width="150">
<template #default="{ row }">
@@ -768,10 +770,11 @@
>移除</el-link>
</template>
</el-table-column>
</el-table>
</el-table>
</section-card>
<div class="dialog-section-title">路线信息</div>
<div class="loading-manage-dialog__route-list" v-if="routeNodes.length">
<section-card title="路线信息">
<div class="loading-manage-dialog__route-list" v-if="routeNodes.length">
<div
v-for="(node, index) in routeNodes"
:key="node.key"
@@ -788,11 +791,12 @@
<el-tag v-for="tag in node.tags" :key="tag" size="small">{{ tag }}</el-tag>
</span>
</div>
</div>
<el-empty v-else description="添加待配载运单后自动生成路线" :image-size="72" />
</div>
<el-empty v-else description="添加待配载运单后自动生成路线" :image-size="72" />
</section-card>
<div class="dialog-section-title">货物信息</div>
<el-table :data="cargoRows" border class="loading-manage-dialog__cargo-table">
<section-card title="货物信息">
<el-table :data="cargoRows" border class="loading-manage-dialog__cargo-table">
<el-table-column type="index" label="序号" width="70" align="center" fixed="left" />
<el-table-column prop="projectName" label="项目名称" min-width="150" />
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
@@ -806,10 +810,11 @@
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
<el-table-column prop="equipmentCode" label="设备编码" min-width="130" />
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
</el-table>
</el-table>
</section-card>
<div class="dialog-section-title">任务信息</div>
<div class="loading-manage-dialog__carrier-tabs">
<section-card title="任务信息">
<div class="loading-manage-dialog__carrier-tabs">
<el-segmented
v-model="dialogForm.carrierType"
:options="carrierTypeOptions"
@@ -913,6 +918,7 @@
/>
</el-form-item>
</div>
</section-card>
</el-form>
</div>
@@ -1882,10 +1888,8 @@ export default {
}
.loading-manage-dialog__candidate-search {
padding: 12px 12px 4px;
padding: 4px 0;
margin-bottom: 8px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.loading-manage-dialog__candidate-actions,

View File

@@ -24,10 +24,9 @@
<el-button
type="primary"
icon="el-icon-plus"
plain
v-if="hasPermission('process_config_add')"
@click="$refs.crud.rowAdd()"
>
>建过程配置
</el-button>
<el-button
type="primary"
@@ -53,13 +52,23 @@
</el-tag>
</template>
<template #includedNodes="{ row }">
<span class="process-config-page__node-value">
{{ formatProcessNodes(row.includedNodes) }}
</span>
</template>
<template #basicInfo-form>
<div class="process-config-page__section">
<div class="dialog-section-title">基本信息</div>
<el-row :gutter="16">
<el-col :span="12">
<el-col :span="8">
<div class="process-config-page__field">
<span class="process-config-page__field-label">配置名称</span>
<span
class="process-config-page__field-label process-config-page__field-label--required"
>
配置名称
</span>
<el-input
v-model="form.configName"
:disabled="dialogReadonly"
@@ -68,9 +77,13 @@
/>
</div>
</el-col>
<el-col :span="12">
<el-col :span="8">
<div class="process-config-page__field">
<span class="process-config-page__field-label">项目</span>
<span
class="process-config-page__field-label process-config-page__field-label--required"
>
项目
</span>
<el-select
v-model="selectedProjectId"
clearable
@@ -92,33 +105,33 @@
</el-select>
</div>
</el-col>
<el-col :span="12">
<el-col :span="8">
<div class="process-config-page__field">
<span class="process-config-page__field-label">默认后台完成运输天数</span>
<span class="process-config-page__field-label">运输完成时限</span>
<div class="process-config-page__finish-days">
<span>默认</span>
<el-input
v-model="form.defaultFinishDays"
:disabled="dialogReadonly"
maxlength="3"
placeholder="X"
placeholder="请输入"
@input="handleFinishDaysInput"
/>
<span>天后完成运输</span>
<span>天后完成运输</span>
</div>
</div>
</el-col>
<el-col :span="12">
<el-col :span="24">
<div class="process-config-page__field">
<span class="process-config-page__field-label">备注</span>
<el-input
v-model="form.remark"
:disabled="dialogReadonly"
:rows="3"
maxlength="500"
placeholder="请输入"
show-word-limit
type="textarea"
:rows="4"
:disabled="dialogReadonly"
maxlength="500"
show-word-limit
placeholder="请输入"
/>
</div>
</el-col>
@@ -196,7 +209,7 @@
<el-radio-group
v-model="row.confirmMode"
:disabled="dialogReadonly || !row.enabled"
@change="syncNodeFields"
@change="handleReturnConfirmChange(row)"
>
<el-radio label="yes"></el-radio>
<el-radio label="no_confirm_complete">无需确认完成</el-radio>
@@ -390,6 +403,7 @@
<script>
import * as api from '@/api/business/process-config';
import { getList as getProjectList } from '@/api/business/project-apply';
import { getDeptTree } from '@/api/system/dept';
import { exportBlob } from '@/api/common';
import { config, option } from '@/option/business/process-config';
import { getToken } from '@/utils/auth';
@@ -513,11 +527,35 @@ export default {
created() {
this.resetNodeRows();
this.loadProjectOptions('');
this.initDeptOptions();
},
methods: {
hasPermission(code) {
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
},
handleReturnConfirmChange(row) {
row.confirmInternal = row.confirmMode === 'yes';
this.syncNodeFields();
},
initDeptOptions() {
getDeptTree(this.userInfo.tenantId).then(res => {
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
if (deptColumn) {
deptColumn.dicData = this.flattenDept(res.data.data || []);
}
});
},
flattenDept(tree, level = 0) {
return tree.flatMap(item => [
{
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
value: item.id,
},
...(item.children && item.children.length
? this.flattenDept(item.children, level + 1)
: []),
]);
},
displayStatus(status) {
const statusMap = {
0: '草稿',
@@ -526,6 +564,13 @@ export default {
};
return statusMap[status] || status || '未知';
},
formatProcessNodes(nodes) {
return String(nodes || '')
.split(/[,]/)
.map(item => item.trim())
.filter(Boolean)
.join('-');
},
statusTagType(status) {
if (status === 1) return 'success';
if (status === 2) return 'info';
@@ -558,7 +603,7 @@ export default {
voucherTypes: [],
frequencyDays: '1',
timeStart: '00:00',
timeEnd: '24:00',
timeEnd: '23:59',
supportCargo: false,
supportVoucher: false,
voucherOptions: [],
@@ -705,7 +750,7 @@ export default {
result.push(`打卡定位:${node.location ? '是' : '否'}`);
if (node.punch) {
result.push(`打卡频次:每${node.frequencyDays || 1}天打卡1次`);
result.push(`打卡时段:${node.timeStart || '00:00'}-${node.timeEnd || '24:00'}`);
result.push(`打卡时段:${node.timeStart || '00:00'}-${node.timeEnd || '23:59'}`);
}
} else {
result.push(`打卡操作:${node.punch ? '是' : '否'}`);
@@ -744,9 +789,15 @@ export default {
this.$message.warning('项目不能为空');
return false;
}
if (row.defaultFinishDays && !/^[1-9]\d{0,2}$/.test(String(row.defaultFinishDays))) {
this.$message.warning('请输入正整数');
return false;
if (row.defaultFinishDays !== undefined && row.defaultFinishDays !== '') {
if (!Number.isInteger(Number(row.defaultFinishDays)) || Number(row.defaultFinishDays) < 0) {
this.$message.warning('完成天数不能小于0');
return false;
}
if (Number(row.defaultFinishDays) > 999) {
this.$message.warning('完成天数不能超过999');
return false;
}
}
if (row.remark && row.remark.length > 500) {
this.$message.warning('备注不能超过500个字符');
@@ -774,43 +825,61 @@ export default {
loading();
}
},
rowSave(row, done, loading) {
this.syncNodeFields();
const submitRow = this.normalizeRow(row);
if (!this.validateRow(submitRow)) {
this.stopSubmitLoading(loading);
return;
needsDuplicateVoucherConfirm() {
const signNode = this.nodeRows.find(item => item.key === 'sign');
const returnNode = this.nodeRows.find(item => item.key === 'return');
const hasSignVoucher =
signNode &&
signNode.enabled &&
signNode.uploadVoucher &&
signNode.voucherTypes.includes('签收单');
const hasReturnVoucher =
returnNode &&
returnNode.enabled &&
returnNode.uploadVoucher &&
returnNode.voucherTypes.includes('签收单');
return hasSignVoucher && hasReturnVoucher;
},
confirmDuplicateVoucher() {
if (!this.needsDuplicateVoucherConfirm()) {
return Promise.resolve();
}
api.submit(submitRow).then(
() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
window.console.log(error);
this.stopSubmitLoading(loading);
return this.$confirm(
'签收与回单同时勾选了上传凭证,司机将需重复上传凭证,是否确认?',
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
);
},
rowUpdate(row, index, done, loading) {
submitRow(row, done, loading) {
this.syncNodeFields();
const submitRow = this.normalizeRow(row);
if (!this.validateRow(submitRow)) {
this.stopSubmitLoading(loading);
return;
}
api.submit(submitRow).then(
() => {
this.confirmDuplicateVoucher()
.then(() => api.submit(submitRow))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
window.console.log(error);
})
.catch(error => {
if (error !== 'cancel' && error !== 'close') {
window.console.log(error);
}
this.stopSubmitLoading(loading);
}
);
});
},
rowSave(row, done, loading) {
this.submitRow(row, done, loading);
},
rowUpdate(row, index, done, loading) {
this.submitRow(row, done, loading);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
@@ -914,7 +983,11 @@ export default {
onLoad(page, params = {}) {
this.loading = true;
api
.getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: 0 })
.getList(page.currentPage, page.pageSize, {
...params,
...this.query,
allDept: this.isAdmin ? 1 : 0,
})
.then(res => {
const result = res.data.data || {};
this.page.total = result.total || 0;
@@ -928,7 +1001,7 @@ export default {
buildExportParams() {
return {
...this.query,
allDept: 0,
allDept: this.isAdmin ? 1 : 0,
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
@@ -1004,6 +1077,17 @@ export default {
font-weight: 500;
text-align: right;
white-space: nowrap;
&--required::before {
margin-right: 4px;
color: #f56c6c;
content: '*';
}
}
&__node-value {
white-space: normal;
word-break: break-all;
}
&__finish-days {

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, '');
},
@@ -1734,7 +1857,7 @@ export default {
<style lang="scss" scoped>
.project-apply-form {
padding: 0 8px;
padding: 0;
:deep(.el-input),
:deep(.el-select),
@@ -1752,12 +1875,30 @@ export default {
line-height: 20px;
}
// 分区标题融入白色卡片顶部(与下方内容块拼成一张完整卡片)
:deep(.dialog-section-title:not(.project-apply-form__material-title)) {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 8px;
background: #fff;
border-radius: 6px 6px 0 0;
padding: 14px 16px 4px;
margin-bottom: 0;
font-weight: 500;
}
&__grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
column-gap: 72px;
row-gap: 0;
margin-bottom: 8px;
margin-bottom: 16px;
background: #fff;
border-radius: 0 0 6px 6px;
padding: 14px 16px 4px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
&__picker-icon {
@@ -1779,7 +1920,11 @@ export default {
&__table {
width: 100%;
margin-bottom: 24px;
margin-bottom: 16px;
background: #fff;
border-radius: 0 0 6px 6px;
padding: 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
:deep(.el-table__header th) {
background: #f5f7fa;
@@ -1793,6 +1938,12 @@ export default {
}
&__textarea-list {
margin-bottom: 16px;
background: #fff;
border-radius: 0 0 6px 6px;
padding: 14px 16px 4px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
:deep(.el-form-item) {
align-items: flex-start;
}
@@ -1806,12 +1957,38 @@ export default {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
margin-bottom: 12px;
}
&__material-card {
margin-bottom: 16px;
background: #fff;
border-radius: 6px;
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;
margin-bottom: 14px;
}
&__material-warn {
@@ -1822,11 +1999,19 @@ export default {
}
&__upload {
margin: -12px 0 24px;
margin: 0 0 16px;
background: #fff;
border-radius: 6px;
padding: 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
&__change-textarea {
margin-bottom: 24px;
margin-bottom: 16px;
background: #fff;
border-radius: 0 0 6px 6px;
padding: 14px 16px 4px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
:deep(.el-form-item__content) {
width: 100%;
@@ -1838,7 +2023,9 @@ export default {
:global(.project-apply-dialog .el-dialog__body) {
max-height: 76vh;
overflow-y: auto;
padding: 16px 24px 8px;
overflow-x: hidden;
padding: 16px;
background: #f5f6fa;
}
.project-apply-dialog__footer {

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

@@ -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="250" />
</template>
<script>

View File

@@ -99,7 +99,7 @@ export default {
editBtn: false,
addBtn: false,
delBtn: false,
menuWidth: 160,
menuWidth: 180,
labelWidth: 'auto',
dialogType: 'drawer',
column: [

View File

@@ -59,6 +59,7 @@ export default {
height: 'auto',
calcHeight: 32,
dialogWidth: 950,
menuWidth: 180,
tip: false,
searchShow: true,
searchMenuSpan: 24,

View File

@@ -73,7 +73,7 @@ export default {
addBtn: false,
viewBtn: false,
dialogWidth: 900,
menuWidth: 160,
menuWidth: 180,
dialogClickModal: false,
column: [
{

View File

@@ -156,7 +156,7 @@ export default {
viewBtn: false,
delBtn: false,
dialogWidth: 900,
menuWidth: 160,
menuWidth: 180,
dialogClickModal: false,
column: [
{

View File

@@ -213,7 +213,7 @@ export default {
viewBtn: false,
delBtn: false,
dialogWidth: 900,
menuWidth: 160,
menuWidth: 180,
dialogClickModal: false,
size: 'default',
searchSize: 'default',

View File

@@ -51,7 +51,7 @@ export default {
editBtn: false,
addBtn: false,
delBtn: false,
menuWidth: 160,
menuWidth: 180,
dialogType: 'drawer',
column: [
{

View File

@@ -51,7 +51,7 @@ export default {
editBtn: false,
addBtn: false,
delBtn: false,
menuWidth: 160,
menuWidth: 180,
dialogType: 'drawer',
column: [
{

View File

@@ -51,7 +51,7 @@ export default {
editBtn: false,
addBtn: false,
delBtn: false,
menuWidth: 160,
menuWidth: 180,
dialogType: 'drawer',
column: [
{

View File

@@ -92,7 +92,7 @@ export default {
viewBtn: true,
selection: true,
grid: true,
menuWidth: 160,
menuWidth: 180,
labelWidth: 'auto',
dialogWidth: 880,
dialogClickModal: false,

View File

@@ -91,7 +91,7 @@ export default {
viewBtn: true,
selection: true,
grid: true,
menuWidth: 160,
menuWidth: 180,
labelWidth: 'auto',
dialogWidth: 880,
dialogClickModal: false,

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,34 @@ 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;
}
/* 操作列兜底:确保「查看 / 编辑 / 删除 / 新增子项」横向排列并保留 8px 间距,
避免退化成 block 导致操作项竖排错位。全局样式已统一处理,此处为双保险。 */
:deep(.avue-crud__menu) {
display: inline-flex !important;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: flex-start;
width: 100%;
text-align: left;
> .el-button,
> .el-link,
> .el-dropdown {
flex-shrink: 0;
display: inline-flex;
}
}
</style>

View File

@@ -88,7 +88,7 @@ export default {
index: true,
selection: true,
viewBtn: true,
menuWidth: 320,
menuWidth: 220,
dialogClickModal: false,
column: [
{

View File

@@ -81,7 +81,7 @@
</el-table-column>
</template>
<!-- 操作栏模块 -->
<el-table-column prop="menu" label="操作" :width="220" align="center">
<el-table-column prop="menu" label="操作" :width="180" align="center">
<template #="{ row }">
<el-link
v-if="this.permissionList.viewBtn"

View File

@@ -237,7 +237,7 @@ export default {
selection: true,
addBtn: false,
viewBtn: true,
menuWidth: 160,
menuWidth: 240,
dialogWidth: 900,
dialogClickModal: false,
column: [

View File

@@ -171,7 +171,7 @@ export default {
index: true,
viewBtn: true,
selection: true,
menuWidth: 160,
menuWidth: 180,
dialogWidth: 900,
dialogClickModal: false,
column: [

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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -72,7 +72,7 @@ export default {
index: true,
selection: true,
viewBtn: true,
menuWidth: 160,
menuWidth: 180,
dialogWidth: 880,
column: [
{

Some files were not shown because too many files have changed in this diff Show More