Compare commits

...

214 Commits

Author SHA1 Message Date
刘泉佳 b077ba5be4 fix: 审批通过提示改用后端返回文案,金蝶自动同步失败弹警告 2026-10-11 14:35:57 +08:00
刘泉佳 4294c92e43 fix: 金蝶票池隐藏金蝶单据号 2026-10-08 14:22:28 +08:00
刘泉佳 b840530fe9 fix: 选票客商下拉改取客商发票信息抬头/税号(票池税号是开票税号,非统一信用代码) 2026-10-07 19:24:06 +08:00
刘泉佳 e6a1370de2 feat: 选票弹窗销/购方条件改客商选择器——按名称远程搜客商档案带出税号过滤,粘贴裸税号直查 2026-10-07 17:55:38 +08:00
刘泉佳 65296618a7 feat: 金蝶票池选票弹窗回切本地镜像标准分页(total跳页+名称模糊+认领登记三态过滤,下线游标翻页;附件查看下载与可用金额列保留) 2026-10-05 17:52:44 +08:00
刘泉佳 c4ca347f0e fix: 金蝶OFD附件预览短路提示下载;正式结算发票信息操作列固定最右侧 2026-10-05 15:52:31 +08:00
刘泉佳 b4ecc90470 feat: 收票登记发票附件查看下载与查看态操作列(金蝶piaozone path附件代理)
- vehicle-attachment-table 新增 kingdee-invoice-no/show-view-actions props:piaozone path 型附件查看经 A801-2 代理取流进弹窗预览(objectURL 弹窗关闭回收),下载/批量下载走金蝶代理串行触发
- 查看态展示查看/下载操作列,编辑态为查看/下载/删除;未传 props 的页面行为不变
- 收票登记表单页接入(编辑/查看态均生效),kingdee-attachment.js 导出 requestKingdeeFile 供组件复用
2026-10-01 18:32:22 +08:00
刘泉佳 0484c2359d Merge remote-tracking branch 'refs/remotes/origin/master' into feature/bfi-api-rate
# Conflicts:
#	pnpm-lock.yaml
#	src/axios.js
#	src/views/settlement/formal-settlement.vue
#	src/views/vehicle/customer-archive.vue
2026-10-01 13:46:44 +08:00
gxwebsoft 0b81001ce7 refactor(wel): 重构欢迎页为模块化组件
- 将欢迎页整体结构拆分为多个功能区组件,提高代码可维护性
- 移除原有欢迎横幅、指标卡、图表区、下方区及快捷入口的静态实现
- 引入并注册 SectionKpi、SectionStructure、SectionBusiness 等八个子组件
- 删除冗余样式,更新整体背景和布局样式
- 精简模板内容,仅保留子组件渲染及页脚版权信息
- 移除无用的响应式数据、计算属性及方法
- 包含依赖包 echarts 的新增,支持后续图表功能开发
2026-09-30 17:39:42 +08:00
b2894lxlx 50c1afe9c8 ✨ 列表导出补齐创建人创建时间,并修复组织类型提交
多模块列表与导出展示创建人、创建时间;组织类型强制提交数字编码,避免中文 dictKey 反序列化失败。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-30 14:22:39 +08:00
刘泉佳 7fca74907f feat: 正式结算单详情新增开票发票查看下载(金蝶回写直链)
- 发票信息卡片内新增只读开票发票表格(issuedInvoices,金蝶A303-5回写)
- 查看:数电PDF优先,退预览图/板式文件直链新窗口打开
- 下载:按直链扩展名命名,不经A801-2代理
2026-09-30 14:16:19 +08:00
b2894lxlx e5a7ad616c 🐛 超管业务列表默认查全部组织,并简化区划编号录入
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 21:41:48 +08:00
刘泉佳 d7ddeacbbf feat: 已推送金蝶的认领隐藏作废按钮 2026-09-29 11:58:26 +08:00
刘泉佳 3d59203dbe fix: 作废确认弹窗去掉冲单描述,成功提示改为作废成功 2026-09-29 11:16:08 +08:00
刘泉佳 3700f5d82b ui: 收款流水页搜索区展开器改为与认领记录页一致的箭头图标按钮(tooltip 展开/折叠) 2026-09-29 10:35:56 +08:00
刘泉佳 05b16bbea0 ui: 收款流水页筛选重排——认领状态/交易时间提升为默认展示,低频条件收进展开区 2026-09-29 10:33:25 +08:00
刘泉佳 dc28bc2afa feat: 收款流水页筛选条件补充收款金额区间与认领人 2026-09-29 10:28:37 +08:00
b2894lxlx 2210e33643 🐛 修复组织类型字典非数字key导致下拉无数据,并调整用户弹窗布局
兼容 organization_type 的中文 dictKey 展示;用户弹窗所属组织与备注宽度按四列布局对齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 03:31:56 +08:00
b2894lxlx 1ed524f80f ✨ 组织类型接业务字典,并支持多所属组织与MK再提交
组织管理类型改为 organization_type;用户所属组织支持多选;提交MK前保留原审批状态以识别通过后再提交。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 03:15:27 +08:00
b2894lxlx 4418836fcd 🐛 审核通过后再提交强制新建MK表单实例
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 02:44:49 +08:00
b2894lxlx 4641d0a5dd ✨ 多业务接入MK审核流,提交改传bizId并由后端生成表单实例
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 02:23:24 +08:00
刘泉佳 730c4e2a6a fix: 认领结果回写/流水同步 0 条时提示 NaN——unwrapData 判空改 ?? 保留合法数字 0 2026-09-29 01:33:58 +08:00
刘泉佳 9b42551bcd feat: 认领记录页筛选条件补充推送状态 2026-09-29 01:32:15 +08:00
刘泉佳 66944bdaf9 feat: 收款流水页筛选条件补充推送状态/金蝶状态/明细流水号(推送状态默认展示) 2026-09-29 01:25:36 +08:00
刘泉佳 09da05c75e feat: 收款流水页新增推送金蝶按钮与推送状态/失败原因/金蝶单号列,认领口径切公告未认领金额
- 行按钮推送金蝶(receipt_flow_push 权限,认领完成且未推送可点),成功提示金蝶收款单号
- 列表/详情展示推送状态、推送失败原因、金蝶收款单号与金蝶状态,留痕表含推送记录
- 认领表单/批量认领上限校验切公告未认领金额口径,新增剩余可认领金额展示
2026-09-29 01:03:21 +08:00
刘泉佳 8bc312502f feat: 收款流水认领页面展示金蝶未认领金额(unclaimedAmount) 2026-09-28 20:04:58 +08:00
刘泉佳 9b40800aab Merge remote-tracking branch 'origin/feature/kingdee-attachment-sync' into feature/bfi-api-rate
# Conflicts:
#	src/api/payment/invoiceApplication.js
#	src/api/payment/invoiceReceipt.js
#	src/api/payment/paymentApplication.js
#	src/api/settlement/formalSettlement.js
#	src/views/payment/invoice-application.vue
#	src/views/payment/invoice-receipt.vue
#	src/views/payment/payment-application-form.vue
#	src/views/payment/payment-application.vue
#	src/views/settlement/formal-settlement.vue
2026-09-28 10:07:19 +08:00
b2894lxlx 8b9c908237 ⚡ 统一地图选址搜索为高德 place/text 并展示分页候选
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-28 00:19:17 +08:00
b2894lxlx b5043575e4 🐛 修复常用货物组织筛选,并调整列表字段与常用地址行政区划禁用
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-27 23:39:38 +08:00
b2894lxlx b92625528c ⚡ 优化区划国家入口、计划-1展示、路线地图点选与对账导出
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-27 23:16:18 +08:00
b2894lxlx f3a33b1a04 ✨ 完善运单导入编辑回填、分批校验与运费录入
导入编辑进入独立页面并回填数据,确认导入按每批100条校验后统一导出失败明细,完整录入运费信息按数量单位联动单价单位。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-27 22:54:33 +08:00
b2894lxlx 98fbf123f5 🐛 修复车务非必填-1展示,并完善总单调度与运单表单体验
调度页限制上一段到达后才可调度;司机候选项点击展开;车务多模块-1显示为空;同步配载/总单编辑与导入相关优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-27 21:27:14 +08:00
刘泉佳 f49f36fb60 ✨ 金蝶附件下载(A801-2)前端:票池弹窗/正式结算/付款申请三入口附件查看下载与类型分流 2026-09-27 21:17:59 +08:00
weicw-dshsa c268d2e64d 🔀 合并 fix/import-error-message-blob
修复导入失败时前端误报「系统错误」,根因有两层:
- axios 的 validateStatus 允许 200-500,HTTP 400 走的是成功分支而不是错误分支;
- 导入请求用 responseType='blob',res.data 是 Blob,读不到 code/msg,
  于是状态码退回 res.status、文案落到「系统错误」兜底。

改法:按 Content-Type 判断,声明为 JSON 的响应体才把 Blob 读成文本解析回
对象;真·文件流(Excel 明细)保持 Blob 原样走下载分支。
另统一 notifiedError() 给 Error 打 _notified 标记,业务层据此避免同一条
错误被弹两次。
2026-09-26 15:32:00 +08:00
weicw-dshsa fc7df95c1a 🐛 修复导入错误提示重复弹出并丢失业务文案
- axios 拦截器:blob 请求(导入/导出)在业务校验失败时后端返回的是
  JSON(Content-Type: application/json),而 axios 在 responseType='blob'
  下把响应体解析成 Blob,直接读 res.data.msg 取不到任何字段,
  status 退回 res.status、文案落到「系统错误」兜底,用户误以为系统崩溃。
  现按 Content-Type 判断:JSON 才解析回对象,真·文件流保持原样不动。
- 统一提示出口 notifiedError():弹窗后给 Error 打 _notified 标记,
  业务层据此避免同一条错误被弹两次。
- import-excel:拦截器已提示过的错误不再重复 $message.error;
  >500/网络异常等仍由 resolveErrorMessage 从 Blob 中取 msg 兜底。
2026-09-26 12:21:20 +08:00
weicw-dshsa 088c9cace0 🐛 导入失败时显示后端业务提示,不再误报系统错误
问题(用户实测反馈):导入只有一个表头的空文件时,前端提示「系统错误」,
用户误以为系统崩溃;后端其实已返回明确文案(400 + 「导入数据不能为空」)。

根因(两处叠加):
1) axios 错误分支 reject(new Error(error)) 把原始错误包装掉,
   error.response 丢失、message 退化成 [object Object];
2) 导入接口统一用 responseType:'blob',非 2xx 的响应体也是 Blob,
   而 axios 错误分支不做解析,拿不到后端 msg。

修复:
- src/axios.js:错误分支直接 reject 原始 axios 错误,保留 response;
- src/utils/import-excel.js:新增 resolveErrorMessage,从错误响应的 Blob 中
  解析后端 JSON 取 msg;解析不到时兜底为中文提示(不再暴露
  「Request failed with status code xxx」这类英文技术文案)。

验证:vite build 通过;模拟真实响应体验证 400 + 空表场景,
提示由「系统错误」变为后端原文「未读取到数据行,请确认表头下方已填写内容」。
2026-09-26 11:50:49 +08:00
b2894lxlx 079be78c2c ✨ 优化配载详情执行打卡与运单切换展示
执行详情按运单切换,补充打卡记录时间线与状态展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-25 17:45:21 +08:00
b2894lxlx ee7160e393 ✨ 完善客商MK审核提交、地区校验与流程实例校验
提交增加 loading,拆分所在地区/详细地址校验,支持 MK 审核跳转,并在已有流程实例时跳过重复提交。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-25 17:45:21 +08:00
刘泉佳 7fd8140761 feat: 收款认领页直连A301-2公告同步并支持A301-1认领结果查询回写 2026-09-24 22:21:38 +08:00
刘泉佳 aabe67e4d6 ✨ 金蝶附件同步(A902)前端:四页面附件同步列、重推补传提示与收票推送超时放宽 2026-09-24 13:27:41 +08:00
刘泉佳 e63c7fdd42 Merge remote-tracking branch 'origin/feature/bfi-api-rate' into feature/bfi-api-rate
# Conflicts:
#	src/api/payment/invoiceReceipt.js
2026-09-24 11:56:47 +08:00
刘泉佳 ec56ee07be feat: 正式结算、客商档案金蝶推送按钮增加防重复点击 2026-09-24 05:08:01 +08:00
刘泉佳 a5eeda6152 feat: 付款申请、开票申请、收票登记金蝶按钮增加防重复点击 2026-09-24 05:07:51 +08:00
刘泉佳 5c45e9b2c9 feat: 金蝶推送接口前端超时扩大至30秒 2026-09-24 05:07:39 +08:00
刘泉佳 331a39b37d Merge remote-tracking branch 'origin/master' into feature/bfi-api-rate
# Conflicts:
#	src/views/vehicle/customer-archive.vue
2026-09-24 03:15:23 +08:00
b2894lxlx aa26beda28 ✨ MK公开页、预结算体验优化、地图选址与认证密码规则完善 2026-09-23 18:36:18 +08:00
b2894lxlx a919781d4f ✨ 完善配载确认、承运商组织过滤司机与地址展示
配载草稿确认生成正式单并优化详情/列表地址市区展示;运单调度等按承运商联系人所属组织筛选司机车辆;统一地图搜索结果与登录默认账号清理。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 18:35:27 +08:00
刘泉佳 aa0aec3704 feat: 批量转结算弹窗新增单据号和合同编码查询条件 2026-09-23 15:39:32 +08:00
刘泉佳 622705325b feat: 选票弹窗新增剩余可用金额列并支持游标翻页与上一页缓存回显
- 下一页回传上页nextCursor续扫,新查询/换页容量/重新打开自动重置游标与页缓存
- 上一页直接回显已浏览页缓存,不再发起请求
- 查询表单删除单据使用状态条件(后端固定查未用),新增剩余可用金额展示列
2026-09-23 15:06:33 +08:00
b2894lxlx 694fe1a433 🔧 开发代理默认指向本地网关
补充本地网关端口说明,避免误把 Nacos 8080 当作业务网关。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 14:49:09 +08:00
b2894lxlx ae1f2fa09f 💄 调整临时额度弹窗字段宽度与 dialogWidth 配置
统一表单控件宽度,并修正 Avue dialogWidth 数值写法避免重复 px。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 14:49:09 +08:00
b2894lxlx fbfb563254 ✨ 项目管理客户信息增加是否广西百强列
在企业性质右侧展示客商档案的广西百强标识。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 14:49:09 +08:00
b2894lxlx 5352d799af ✨ 运单详情已完成隐藏变更路线并移除路线编辑
已完成运单不再展示变更运输路线入口,变更弹窗去掉拖拽路线编辑区。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 14:49:09 +08:00
b2894lxlx e2e2eb9a2f ✨ 完善合同附件上传回填、类型匹配与列表附件弹窗
修复上传成功后表格不回显,附件类型可切换,未匹配文件名默认其他文件;列表附件上传支持合同文件/其它附件。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 14:49:09 +08:00
刘泉佳 4657fc29bd ♻️ 收票管理仅保留推送金蝶收票单并统一命名为批量同步 2026-09-23 11:05:19 +08:00
刘泉佳 884de893db ✨ 收票单推送金蝶与票池实时游标分页前端适配:选票弹窗hasMore分页,下线镜像补查与票池同步入口,保存时按票号重查回填 2026-09-23 10:22:51 +08:00
刘泉佳 cc73d544ff 🔧 引入vue-tsc与typecheck脚本支持类型检查,pnpm-lock.yaml改为忽略不跟踪 2026-09-23 10:22:46 +08:00
b2894lxlx 6212d68a77 ✨ 付款申请表单优化与结算单过滤,并完善相关体验
进度预付仅可选预结算、尾款付款可选正式/预结算;同步收款账户、申请金额校验、关闭标签与费用明细残留等交互,并补充运单复制需求说明。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 00:17:07 +08:00
刘泉佳 10fecb5a54 ✨ 金蝶票池选票统一查镜像前端接线(新增统一选票弹窗组件,替换收票与结算认领弹窗并删除Mock) 2026-09-22 21:26:50 +08:00
b2894lxlx ce1ce02551 ✨ 完善变更记录、客商评分与车辆表单
变更记录详情统一为居中分页弹窗,并去掉经办人等信息行。
客商评分支持删除、提交前自评校验、复评后才能审批,以及 0 分提交。
车辆外廓尺寸和载质量单位改到输入框内,认证审核标签按四字换行,使用部门必填并默认当前部门。
临时额度的 -1 占位金额显示为空。新增项目编号可留空由系统生成。
修复缓存总单页在打开新增项目时误请求缺少 id 的详情接口。
详情页状态与运输方式标签样式统一。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 20:13:44 +08:00
刘泉佳 bab6bdba27 Merge remote-tracking branch 'origin/feature/bfi-api-rate' into feature/bfi-api-rate 2026-09-22 16:36:33 +08:00
b2894lxlx 3a8442e522 Merge remote-tracking branch 'websoft/master' 2026-09-22 12:06:53 +08:00
b2894lxlx ac87208ba9 ✨ MK公开页、预结算体验优化、地图选址与认证密码规则完善 2026-09-22 12:06:48 +08:00
刘泉佳 a554ccf38e 🐛 axios网络级失败(502/504/超时)补充统一错误提示,修复接口报错页面无感知 2026-09-21 21:20:57 +08:00
刘泉佳 a748e52551 ✨ 开票申请商品行选择开票内容时快照税收分类编码并随保存提交 2026-09-21 19:14:57 +08:00
weicw-dshsa 815810f841 ✨ 批量导入上传提示注明日期宽容写法
8 个 CRUD 导入上传 tip 与运单导入弹窗、运输计划导入页的提示文案
统一补充「日期支持 2026-08-02、2026-8-2、2026/8/2 等写法」,
配合后端宽容日期解析(weicw/tms-erp#1)。
2026-09-21 19:08:56 +08:00
weicw-dshsa 4e04cdbc5b ⚡ 统一运输方式兜底文案为公路运输
去掉计划单调度、运输计划展示中的「公路整车」旧文案
2026-09-21 19:08:55 +08:00
b2894lxlx 57368f0179 调试mk 2026-09-21 11:14:51 +08:00
刘泉佳 e89e78b527 ✨ 正式结算单认领查询对接发票云全票池(多条件搜索+服务端分页) 2026-09-20 19:41:37 +08:00
b2894lxlx 7078de6385 调试mk 2026-09-20 18:12:51 +08:00
刘泉佳 2a03991297 ⚡ 客商档案金蝶回填优化:放宽锁定字段、信用代码可重新输入并修复查询误判 2026-09-20 16:06:03 +08:00
b2894lxlx 4af9afda61 调试mk 2026-09-20 13:17:05 +08:00
刘泉佳 884cc4355f fix: 同步金蝶接口放宽请求超时,修复长耗时推送成功但页面无提示 2026-09-18 20:42:52 +08:00
刘泉佳 58aaf79445 ✨ 客商档案新增金蝶客商查询回填与同步金蝶重推入口 2026-09-18 20:01:29 +08:00
b2894lxlx 50ab33f369 Merge remote-tracking branch 'websoft/master'
# Conflicts:
#	src/views/business/project-apply.vue
2026-09-18 18:26:13 +08:00
b2894lxlx bcbc45bb03 调整 用户 2026-09-18 18:25:52 +08:00
gxwebsoft ff9fb9168b fix(router): 修复运单管理详情路由及合同详情路由重复定义问题
- 为 waybill-manage-page、transport-plan-page 等组件增加 routePathLocked 变量锁定路由路径
- 添加 isOwnedRouteActive 计算属性,防止缓存实例错误读取其它路由的 detailId 导致误请求
- watcher 和 openDetail 方法中添加路由守卫,避免多余的请求和误开详情标签
- 修改宿主视图绑定 detailId,避免直接绑定全局 $route.query 导致缓存污染
- 删除 router/views/index.js 中合同管理详情路由重复定义,防止内容区空白问题
- 优化 waybill-import-dialog 组件样式和结构,增加 section-card 包裹表单区域
- 调整相关组件 data、computed、watch 使用,确保第一次立即 watcher 正确读取路由数据
- 验证所有改动后相关页面 HTTP 200,标签页行为正常,解决「运单管理不存在」和合同详情空白问题
2026-09-18 12:22:25 +08:00
刘泉佳 f067518c0e fix: 付款申请批量同步金蝶失败时展示失败原因
- 批量同步后解析返回结果,按"同步失败"标识区分成功与失败条目
- 部分失败时以 warning 提示成功/失败条数及具体失败原因(停留8秒),全部成功仍提示已同步条数
2026-09-18 11:58:00 +08:00
刘泉佳 11460f3378 ✨ 付款管理新增同步付款结果按钮并扩展金蝶单据状态选项 2026-09-18 11:57:59 +08:00
gxwebsoft 986feff7b7 fix(waybill-manage): 修复运单详情弹窗残留问题
- 页面形态状态(列表/独立表单页/独立详情页)在实例创建时锁定,避免随路由变化切换导致弹窗残留
- 模板中形态相关所有判定改为使用锁定状态变量,保证渲染一致性
- 修改 detailContainer、crudContainer、pageFormOption 等方法使用锁定状态,避免缓存实例 option 变形
- 新增 closeInnerDialogs 方法,在组件 deactivated 和 beforeUnmount 生命周期关闭所有 append-to-body 弹窗
- 解决标签页 keep-alive 机制下弹窗 Teleport 持续遗留导致界面异常的问题
- 对运单管理页面多处弹窗和视图控制做统一修复,提升页面稳定性和体验
2026-09-18 11:29:52 +08:00
gxwebsoft 3e7110ca8c fix(waybill-manage): 修复运单详情弹窗残留问题
- 页面形态状态(列表/独立表单页/独立详情页)在实例创建时锁定,避免随路由变化切换导致弹窗残留
- 模板中形态相关所有判定改为使用锁定状态变量,保证渲染一致性
- 修改 detailContainer、crudContainer、pageFormOption 等方法使用锁定状态,避免缓存实例 option 变形
- 新增 closeInnerDialogs 方法,在组件 deactivated 和 beforeUnmount 生命周期关闭所有 append-to-body 弹窗
- 解决标签页 keep-alive 机制下弹窗 Teleport 持续遗留导致界面异常的问题
- 对运单管理页面多处弹窗和视图控制做统一修复,提升页面稳定性和体验
2026-09-18 11:17:50 +08:00
gxwebsoft b4e12a671f fix(views): 修复独立页切换时弹窗残留问题
- 独立整页固定使用普通 div 容器,取消动态切换 el-dialog 避免切换时弹窗 DOM 残留
- 删除相关 dialogVisible、dialogTitle、页脚弹窗动态分支代码以简化逻辑
- watch $route 调整,优化独立页初始化流程,避免不必要的关闭弹窗操作
- 新增 closeInnerDialogs 方法,页面卸载或 keep-alive deactivate 时关闭所有二级弹窗
- 对 loading-manage 与 project-apply 两个模块同步改造,统一处理弹窗状态和容器渲染
- 解决 keep-alive 缓存导致旧弹窗一直挂载在 body 上的问题,改善用户切换体验
2026-09-18 10:17:48 +08:00
gxwebsoft 5bb072223f feat(loading-manage): 配载管理详情及重新派单改用独立页面
- 新增 `/business/loading-manage/detail` 路由,详情页由独立页面承载
- 统一将新增、编辑、详情和重新派单均采用独立表单页/详情页显示
- 修改组件内逻辑,新增 `isStandalonePage` 计算属性统一控制页面与弹窗切换
- 修改列表页链接及操作行为,详情和重新派单按钮跳转至对应独立页面
- 去除旧详情弹窗模式,相关样式调整支持独立页面自然滚动
- 添加路由监听,支持页面刷新和切换时正确初始化详情或表单数据
- 兼容老链接 `/business/loading-manage?detailId=x` 自动跳详情新路由
- 修正弹窗底栏样式,去除写死间距,统一由全局规则控制
- 代码中禁止顶层 scoped style 使用 `//` 注释,改用 `/* */` 避免编译错误
2026-09-17 22:40:41 +08:00
gxwebsoft 3d6e297147 feat(vehicle): 归一化保险记录导入日期格式,修复设备台账批量删除按钮
- 保险记录批量导入时,前端读取 Excel 并规范日期格式为 YYYY-MM-DD,兼容多种日期写法
- 重写 `handleImport` 支持自定义上传逻辑,调用归一化处理后再上传文件
- 添加日期归一化相关方法,确保不合法或已标准格式保持原值
- 设备台账界面新增批量删除按钮及相关逻辑,支持多选数据批量删除并提示确认
- 补充导出含创建时间和更新时间的后端修复说明,避免导出时间字段为空问题
- 说明后端导出分配逻辑,强调导出字段由后端控制,前端修改导出列无效
2026-09-17 21:21:33 +08:00
gxwebsoft 82fbb99593 fix(payment): 优化发票申请及收据表单提示和样式
- 简化合并开票结算单的合同限制提示文案
- 调整发票申请表单搜索区域的内容左对齐样式
- 修正发票收据表单关键词输入框的占位符文字错误
2026-09-17 14:59:26 +08:00
gxwebsoft 17c482e21a feat(waybill): 优化打卡时间轴按打卡时间排序展示
- 新增 computed 属性 orderedWaybillPunchRecords,按打卡时间升序排序已打卡记录
- 未打卡记录保持后端返回的节点顺序,排在已打卡记录之后
- 引入方法 waybillPunchRecordTimestamp 解析打卡时间,支持多种日期格式
- 修正 el-timeline v-for 使用 orderedWaybillPunchRecords 避免 key 冲突
- 保持原有流程节点顺序不变,仅调整时间轴展示顺序
2026-09-17 14:47:29 +08:00
gxwebsoft 2684624979 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/views/business/contract-manage.vue
#	src/views/business/temporary-credit-limit.vue
#	src/views/transportCapacity/vehicle.vue
2026-09-17 14:42:33 +08:00
b2894lxlx 84d9e098ba 调整 业务 2026-09-16 23:43:12 +08:00
刘泉佳 7f543937fb ✨ 结算单新增时自动查询BFI汇率
- currency.js: 新增getBfiExchangeRate接口调用
- pre-settlement-editor.vue:
  - 选择合同后同步结算币种并自动查询当天BFI汇率
  - 汇率日期变更时自动查询BFI汇率并回填结算汇率
  - 新增时自动设置当天日期并查询汇率
- formal-settlement-editor.vue:
  - 选择合同后同步结算币种并自动查询当天BFI汇率
  - 汇率日期变更时自动查询BFI汇率并回填结算汇率
  - 新增时自动查询当天BFI汇率
2026-09-16 14:14:53 +08:00
b2894lxlx c1c4fa18c9 调整 iam 2026-09-16 06:30:30 +08:00
b2894lxlx 6a87066097 调整 合同、运力、客商、项目 2026-09-15 11:59:55 +08:00
b2894lxlx 79aa4f9fae 调整 合同、运力、基础配置、客商 2026-09-14 23:43:20 +08:00
b2894lxlx f1e2313e4c 调整 合同 2026-09-14 15:01:13 +08:00
b2894lxlx acbc9d3322 调整 基础数据、运力、车船务 2026-09-14 01:49:23 +08:00
b2894lxlx dbeb174ae2 调整项目 2026-09-13 01:01:24 +08:00
b2894lxlx 764c0f4581 运单调整 2026-09-11 14:39:30 +08:00
gxwebsoft 2131e99ec4 feat(contract-manage): 将合同详情由弹窗改为独立页面
- 新增路由 /business/contract-manage/detail,详情页共享合同管理组件
- 列表、表单、详情三分支用 isFormPage 和 isDetailPage 控制
- openDetail 方法改为跳转详情路由,initDetailPage 获取详情数据
- 新增 resetDetailState 和 closeDetailPage 方法,复用关闭逻辑
- 详情页去除弹窗高度限制,样式改为页面自滚动
- 详情页底栏使用固定位置关闭按钮,保持页面布局一致
- watch 和 created 中添加对 isDetailPage 的初始化调用和监听
2026-09-11 12:30:05 +08:00
gxwebsoft 6e5b15f203 Merge remote-tracking branch 'origin/master' 2026-09-11 10:41:13 +08:00
gxwebsoft fbfd5cde89 fix(api): 移除项目资金风险统计请求调用
- 由于后端/fund-risk-stats端点尚未实现,删除列表页中refreshFundRiskStats调用
- 从data中移除fundRiskStats相关数据,避免接口请求报错
- 列表头部风险筛选标签调整,仅显示“高风险/中风险”,去掉数量显示
- api/business/project-apply.js中保留getFundRiskStats定义,并添加注释说明后端就绪后恢复调用
2026-09-11 10:41:09 +08:00
b2894lxlx 1a9cd55d66 1、调整导入运单
2、调整对账
2026-09-10 22:50:18 +08:00
b2894lxlx 38e53e01af Merge remote-tracking branch 'websoft/master' 2026-09-10 02:01:42 +08:00
b2894lxlx 6ff672211f 1、调整导入运单
2、调整对账
2026-09-10 02:01:28 +08:00
gxwebsoft 1bbc97758d fix(contract-manage-change): 修正自动生成结算单问号提示图标位置
- 解决图标夹在开启/关闭 radio 按钮之间,开启按钮右侧多余间距问题
- 清除开启 radio 默认 margin-right,调整提示图标左右间距
- 提示图标紧靠开启按钮一侧,关闭按钮保持较大间距
- 优化样式,使布局更美观一致
2026-09-09 19:25:06 +08:00
gxwebsoft c1813e308f feat(vehicle): 重构车辆认证审核弹窗布局与验证规则
- 新增车辆认证审核弹窗,取消独立尺寸重量卡片,外廓尺寸改为长/宽/高组合布局
- 基础信息调整为3行4列,证件信息新增车辆资质图片行,所属组织标签更名为所有组织
- 行驶证有效期必填星号动态显示,长期有效时不显示且不校验,删除旧校验方法
- driver.vue中从业资格证有效期改为起止日期区间,新增起始日期字段,调整验证规则
- 驾驶证和从业资格证长期有效时禁用起止日期输入,校验逻辑同步调整,提交保留旧值
- contract-manage-change.vue合同文件批量下载按钮移入标题栏,与其它附件分区样式统一
- 样式更新,新增 dimension-group 用于外廓尺寸排版复用及其他局部样式调整
- 修复因后端未实现接口导致的降级方案预留,等待后端补充支持
2026-09-09 19:06:26 +08:00
gxwebsoft 716ce43862 feat(driver): 优化驾驶证信息必填规则和表单placeholder统一
- 驾驶证有效期必填加红星,长期有效去除必填红星,保持校验逻辑不变
- 统一司机弹窗所有输入框和选择框的placeholder为“请输入”或“请选择”,去除字段名
- 保留复合操作、日期范围起止及默认值说明的特例placeholder不变
- 新增3处自动生成结算单开关后悬浮问号图标和提示,增强用户理解
- customer-archive中新增customerKindOptions和映射,用于客户类别选择
- 提升archive-form label与控件垂直居中样式至全局,修正多页标签顶置问题
2026-09-09 17:35:19 +08:00
gxwebsoft 14c6ad0a31 feat(vehicle): 优化车辆管理相关表单布局和字段
- 车牌号组件改为单一输入框,去除省简称和颜色选择,实现完整车牌号输入
- 重写车牌号校验逻辑,校验完整车牌号格式,错误提示更加明确
- 车辆新增/编辑弹窗输入框宽度统一调整,基础信息和资质图片区按四列布局排列
- 修复所有组织字段宽度不一致问题,补充级联选择器样式支持
- 司机新增/编辑弹窗调整为四列布局,调整字段排列顺序,提升界面整洁度
- 客商档案中「客商类型」改名为「客商分类」,新增「客商类型」字段支持外部客商和内部组织区分
- 上传证件项 label 贴顶问题修复,统一提升到全局样式实现垂直居中
- 移除车牌颜色只读展示,审核弹窗同步删除车牌颜色字段
- 保持后端接口兼容,数据库字段未变,OCR同步调用链路无影响
2026-09-09 17:19:33 +08:00
gxwebsoft 1972115173 feat(transportCapacity): 优化车辆与司机弹窗布局并新增字段支持
- 车辆新增弹窗:所有组织移至基础车辆信息区末尾,新增使用部门字段,使用部门复用部门下拉
- 车牌号三段合并为整体输入框,优化样式为flex布局,去除多余边框及阴影,统一视觉
- 长期有效勾选时清空对应日期且禁用输入,新增handleLongTermChange方法,表单校验支持长期有效跳过必填
- 司机弹窗基础身份信息新增placeholder,调整布局,基础身份三列保持不变,其他信息行按需求拆分重排为一致三列
- 后端新增use_department字段,相关mapper、service及校验同步更新,新增增量迁移脚本支持
- 车辆表单相关输入控件宽度统一调整为6列布局,保持与其它区块一致,提升整体UI协调性
- 移除车辆车牌号拆分相关字段及逻辑,车牌号统一管理为单个字段,简化处理流程
- 代码中新增校验规则validateDrivingLicenseEndDate及validateRoadTransportCertEndDate,支持长期有效逻辑
- 优化图片上传区和证件信息区表单字段排列,提升表单整体易用性与规范性
2026-09-09 15:37:40 +08:00
gxwebsoft 006dc95bf0 Merge remote-tracking branch 'origin/master' 2026-09-09 15:11:56 +08:00
gxwebsoft d187da68d2 fix(contract-manage): 优化合同管理页面布局及字典数据读取
- 合同文件「批量下载」按钮调整为与标题同排且垂直居中
- 修改内联组件render函数结构及相关样式,统一合同文件和其它附件布局
- 合同格式下拉改为动态读取业务字典contractFormat,保留静态兜底
- 合同类别contractCategory下拉改为业务字典读取,替代硬编码选项
- 表单增加所属组织、签订日期、回款账期、开票周期、结算币种等字段
- 调整字段顺序,完善表单输入限制和控件属性
- 替换部分按钮类型,优化用户交互体验
- 修复detailValue字典映射,显示合同格式标签
- 修正样式`:deep`选择器,解决scoped样式对子组件无效问题
2026-09-09 15:11:53 +08:00
b2894lxlx 70bb94a2e6 Merge remote-tracking branch 'websoft/master' 2026-09-09 14:43:49 +08:00
b2894lxlx f78e61b433 1、调整导入运单
2、调整对账
2026-09-09 14:43:38 +08:00
gxwebsoft 508d73fc0d Merge remote-tracking branch 'origin/master' 2026-09-09 12:45:50 +08:00
gxwebsoft e73e1da877 feat(contract-manage): 将付款比例设置提示改为tooltip展示
- 付款比例设置区块标题后添加问号图标悬浮提示,替换原红色文字说明
- 编辑弹窗、详情弹窗、合同变更页统一使用el-tooltip + el-icon-question-filled组件
- 移除contract-manage.vue中原有.red tip提示及相应样式

feat(temporary-credit-limit): 优化临时额度申请界面及校验规则

- 增加新增/编辑/查看临时额度申请标题,覆盖默认菜单注入标题
- 所有表单字段统一设置placeholder为“请输入/请选择”无字段名
- 搜索栏占位符统一为“请输入/请选择”,与其他模块一致
- 临时额度申请有效期至字段增加“必须晚于当前日期”日期校验规则
- 增添futureDateRule通用校验规则,支持多种日期格式即时触发验证

feat(project-apply): 新增资金使用风险快速筛选及风险提示

- 项目申请列表增加资金使用风险列,显示高风险/中风险标签
- 新增资金使用风险快速筛选区,可点击标签筛选项目风险等级
- 提供资金使用风险阈值配置,支持中风险≥80%、高风险≥90%
- 调整页面样式及交互,优化风险筛选体验
- 新增后端接口getFundRiskStats,用于获取风险统计数据并实时刷新
2026-09-09 12:45:46 +08:00
gxwebsoft 4d5b99da82 feat(contract): 优化合同变更及车辆认证审核弹窗
- 合同变更页「其它附件」上传按钮样式调整,与变更材料区按钮统一
- 新增分区标题必填星号样式,改用 ::after 避免冲突
- 变更页 load() 强制清空表单关键字段,避免带入旧数据
- 实现 mergeChangeAttachments() 合并上次变更附件,去重并格式化大小
- 合同变更记录表格新增「变更内容」列,支持单行摘要显示与悬浮全量提示
- 操作列由单「流程」改为「详情 + 流程」按钮,详情文字统一
- 变更记录值格式化增强,支持方案名、支付比例、结算规则等摘要展示
- buildDetailChangeRecordRows 过滤条件更新,前后均为空时跳过展示
- 车辆认证审核弹窗样式及结构重构,使用 section-card 白卡分组,内容补全证件图片区
- 证件图片实现只读大图展示,复用全局样式,无需新增规则
- label 单位统一为中文全角括号,提升界面一致性
- 新增长期有效显示逻辑,避免日期未填「-」与长期有效混淆
- 删除无用计算属性 auditDetailItems,改用简洁 auditValue 方法返回展示值
- 全站弹窗标题统一增加 4px 主色竖条,符合设计规范,且对无标题弹窗进行排除处理
- 客户端临时额度申请期限字段拆分为两个独立字段,清晰明了
- 统一合同变更页变更原因必填标识及上传按钮样式
- 修正 EP 表单项标签高度和对齐,完善星号方案兼容性设计
2026-09-09 11:57:52 +08:00
gxwebsoft f3021d4bae style(ship): 调整航区字段布局及上传器宽度
- 将航区选择控件从宽度12调整为宽度6的列中展示
- 保留航区选择及提示信息功能不变,位置更紧凑
- 调整上传组件宽度从240px增加到248px以适配新布局
2026-09-09 11:20:52 +08:00
gxwebsoft 7eb6cce2db fix(project-apply): 修正项目简称输入框的必填校验
- 取消了项目简称表单项的 required 属性
- 在项目简称的验证规则中新增必填校验
- 使得项目简称必填校验逻辑与表单项显示一致
- 保持校验触发事件为失焦(blur)事件
2026-09-09 11:14:44 +08:00
gxwebsoft be2888d428 fix(styles): 修复必填星号首行对齐问题
- 调整 `.is-required .el-form-item__label` 采用 `align-items: normal` 实现星号首行对齐
- 覆盖 Element Plus 固定高度 `height: 32px` 为 `height: auto` 以支持内容自适应高度
- 保持 `align-self: center` 使单行 label 与输入框垂直居中,折行时星号贴首行
- 修改星号 `::before` 的高度为 `auto` 并继承行高,保证首行基线一致
- 修复多行 label 下星号因 flex 居中导致位置偏移的问题
- 兼容 label 多种位置(右、左、顶)及多行折行场景,影响 project-apply 等表单页
2026-09-09 11:11:39 +08:00
gxwebsoft 06f6950434 refactor(contract-manage): 优化结算货币标签显示
- 结算货币选项中仅显示字典值,去除字典键拼接
- 注释掉样式中 el-form-item 标签的行高设置,避免样式冲突
2026-09-09 10:39:30 +08:00
gxwebsoft 09f1af9a0a fix(styles): 调整必填星号与标签首行对齐样式
- 修改 .el-form-item__label 的对齐方式使必填星号与首行文字齐平
- 设置多标签布局下星号对齐,兼容单行和多行标签显示
- 调整 ::before 伪元素行高和外边距,确保星号基线一致

fix(project-apply): 更新表单字段“资金需求”为“履约保证金”

- 修改项目申请页面中“资金需求”标签为“履约保证金”
- 更新对应验证规则标签名称,保持校验一致性

chore(config): 更新开发服务器端口及代理设置

- 注释旧服务器配置,启用新端口 2889
- 更新 /api 代理目标地址为 172.16.203.228:8000
- 保持代理跨域设置不变
2026-09-09 10:31:22 +08:00
b2894lxlx 6d4961f404 1、调整凭证
2、调整对账
3、解决正式结算冲突bug
2026-09-09 10:21:19 +08:00
b2894lxlx cba2ad3b3b 1、调整凭证
2、调整运单
2026-09-08 17:50:34 +08:00
b2894lxlx a7315b4491 调整结算模块 2026-09-07 19:37:27 +08:00
b2894lxlx 0d070d4b34 调整业务模块 2026-09-07 10:05:32 +08:00
b2894lxlx 6a6d10dc79 调整业务模块 2026-09-05 23:26:37 +08:00
b2894lxlx 3bac9b6472 调整结算 2026-09-05 00:04:46 +08:00
b2894lxlx 96f2fa0eb5 Merge remote-tracking branch 'websoft/master'
# Conflicts:
#	src/views/business/contract-manage-change.vue
2026-09-04 05:24:02 +08:00
b2894lxlx e789f1647a 调整业务模块 2026-09-04 05:23:34 +08:00
b2894lxlx c63c7c3e33 调整基础数据、车船务、项目、合同 2026-09-03 22:40:11 +08:00
gxwebsoft 23b23dc8e5 style(element-ui): 区分 el-select 选中值与占位符颜色
- 修复 Element Plus el-select 组件选中值与占位符使用同一节点导致无法区分颜色的问题
- 只有未选值或已展开无搜索时节点带 .is-transparent,用此类排除选中颜色样式应用
- 更新
2026-09-03 17:03:08 +08:00
gxwebsoft a0a3d53927 style(region): 调整表单项底部外边距为9px
- 将 .el-form-item 的 margin-bottom 从8px修改为9px
- 保持样式其他部分不变
2026-09-03 16:58:34 +08:00
gxwebsoft 42e4925fd6 style(contract): 优化附件上传按钮样式及必填提示
- 为“上传附件”和“上传变更材料”按钮添加图标及主色调样式
- 在变更原因标题及输入框添加“必填”提示文字

style(customer-archive): 调整客户档案页面细节排版和格式
- 修复联系人列表相关代码缩进及注释格式
-
2026-09-03 16:06:43 +08:00
b2894lxlx 024e77801e 1、修复基础配置
2、修复业务模块
3、拆分页面代码
2026-09-03 15:59:34 +08:00
gxwebsoft 397490a165 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/views/business/contract-manage.vue
2026-09-03 15:45:29 +08:00
gxwebsoft 9e14f31604 fix(business): 修复下拉框定位及输入框宽度问题
- 统一 business-crud-page.vue 表单控件宽度,包含 el-autocomplete 及其内部元素
- 调整调度弹窗收发货地址行网格,使地址输入框宽度自适应避免溢出
- contract-manage.vue 中所有 el-select 加 :teleported="false" 和 :fit-input-width="true"
- 强制 contract-manage.vue 下拉框在输入框正下方显示,防止弹出位置漂移
- 禁用 contract-manage.vue 下拉弹窗自动翻转,确保下拉框始终在下方显示
2026-09-03 15:45:01 +08:00
b2894lxlx 40925cb21f Merge remote-tracking branch 'websoft/master' 2026-09-03 03:31:34 +08:00
b2894lxlx 6f0f1842cb 1、修复基础配置
2、修复业务模块
3、拆分页面代码
2026-09-03 03:31:30 +08:00
gxwebsoft 685a37b2df style(customer-archive): 统一评分详情表弹窗label宽度并对齐控件宽度
- 将评分详情表弹窗中el-form的label-width由"88"改为"88px"以严格固定宽度
- 强制score-detail-form中label样式flex、width、min-width均为88px,保留word-break实现长文本换行
- 调整单字段输入框、选择框、日期控件宽度由固定150px改为100%,确保各控件等宽撑满内容区域
- 保留临时额度申请使用期限双日期选择器flex布局,不受单字段宽度规则影响
- 客商档案页面“详细地址”输入框加入tooltip,仅文本溢出时显示完整地址,提升用户体验
- 通过ref、watch以及methods检测输入长度溢出以动态控制tooltip显示状态
2026-09-02 17:37:00 +08:00
gxwebsoft b83e3c358d Merge remote-tracking branch 'origin/master' 2026-09-02 16:50:52 +08:00
gxwebsoft 94da08e7a7 fix(login): 隐藏登录页租户栏并同步调整校验规则
- 删除登录页 userlogin 组件中租户输入框的 el-form-item 及相关 DOM
- 移除登录表单 loginRules 中 tenantId 的校验规则
- 保留 loginForm.tenantId 默认值和 getTenant 方法,保证后端兼容
- 说明接口正常时仍会覆盖默认 tenantId,方便后续启用租户栏另外开启配置
2026-09-02 16:50:49 +08:00
b2894lxlx 008ec7a6a8 微调合同 2026-09-02 15:14:05 +08:00
b2894lxlx 429b5548dd Merge remote-tracking branch 'websoft/master' 2026-09-02 15:11:34 +08:00
b2894lxlx 6d4434f5c9 修复付款的付款方式 2026-09-02 15:11:30 +08:00
gxwebsoft 4ffceb0413 fix(vehicle): 调整客户档案地区地址布局与宽度
- 将客户档案页面“所在地区”所在列从12改为6,新增“详细地址”列宽为6
- 修改“所在地区”级联选择器宽度由250px改为240px,匹配更合理的宽度设计
- 优化地址相关表单的布局,提高信息展示的清晰度和一致性
- transportCapacity/ship 组合控件宽度对齐至250px,解决两个date-picker和select宽度不一致的问题
- 修复项目表单中el-cascader宽度未统一的问题,确保级联选择器与其他输入控件同宽
2026-09-02 15:00:40 +08:00
gxwebsoft 6eff4b7383 style(ship): 优化日期选择器和下拉框宽度限制
- 为日期选择器添加最大宽度限制为250px
- 为下拉框添加最大宽度限制为250px
- 改进组件响应式布局适应性
2026-09-02 14:41:38 +08:00
gxwebsoft a8986690bb refactor(payment): 调整到期快捷筛选至账单台账页面独立展示
- 将快捷筛选标签及相关逻辑从搜索子组件提升至账单台账页面
- 标签置于搜索卡片下方、表格上方独立一行,满足用户需求
- 删除子组件中相关标签DOM及快捷筛选功能代码
- 统一选中态样式,采用 Element Plus 默认蓝底深字风格

fix(transportCapacity): 统一船舶管理弹窗表单控件宽度

- 删除异常的宽度强制规则,避免控件宽度不一致
- 采用固定宽度250px加最大宽度100%方案,实现多控件视觉宽度一致
- 清理无用label-width属性,解决label撑开布局异常问题
- 优化表单项label对齐与必填星号样式,确保多行label视觉协调

refactor(transportCapacity): 优化船舶证书分组布局及标签简化

- 证书有效期自标签统一更改为“有效期自”
- 证书上传区域由集中底部移至对应证书分组末尾,保持信息关联
- 清理无用样式,保留必需上传项间距样式,提升界面整洁度

fix(business): 修复项目申请表单级联选择器宽度问题

- 业务部门和承办部门el-cascader控件未设置宽度,导致收缩
- 补充级联选择器宽度规则,保证与其他控件宽度统一
- 支持热更新,无需重启开发环境
2026-09-02 14:36:42 +08:00
gxwebsoft 87ec01c8dd style(payment): 调整账单搜索组件操作栏布局
- 将操作栏从水平方向排列改为垂直方向排列
- 调整按钮和标签间距增大,标签左对齐,按钮右对齐
- 标签和按钮分成独立两行显示,提高界面清晰度

style(transportCapacity): 统一到期快捷筛选标签布局及样式

- 将到期快捷筛选标签由搜索菜单槽位移至头部槽位独立一行
- 移除原先纵向顺序和内联样式,增加标签上下间距
- 统一 driver、ship、vehicle 三个页面到期标签的外观和结构
- 优化标签容器的 margin 边距,取消排序和宽度限制

style(transportCapacity/ship): 统一弹窗内输入控件宽度

- 删除强制设置 .el-input__wrapper 宽度为 150px 的样式
- 增加输入框、选择框及级联选择器等组件宽度为 100% 的样式覆盖
- 保证新增和修改弹窗内所有表单输入控件宽度一致,提升界面整齐度
2026-09-02 12:11:18 +08:00
gxwebsoft f25e38d67e style(contract-manage): 优化过期筛选区域样式和结构调整
- 将时间段快捷筛选 slot 名称从 search-menu 修改为 header
- 调整过期筛选区域的 margin 样式,精细控制上下间距
- 移除多余的右侧 margin,优化布局显示顺序

feat(insurance-ocr-template): 添加 created 生命周期调用 onLoad 方法

- 在组件创建时调用 onLoad,确保初始化动作执行

style(project-apply): 统一项目金额字段后缀及标签显示

- 移除金额字段标签中的“(万元)”文本,改为标签内部统一显示单位后缀
- 为项目资金额度、应收账款额度、项目规模、预估利润、资金需求等输入框添加“万元”后缀显示
- 调整输入框结构,确保后缀模板正常渲染

style(ship): 调整输入框宽度提升表单布局一致性

- 为 .el-input__wrapper 添加固定宽度,保证表单输入框宽度统一
2026-09-02 11:39:44 +08:00
gxwebsoft 8a0db99572 Merge remote-tracking branch 'origin/master' 2026-09-02 11:12:36 +08:00
gxwebsoft 9c2a2ee6e6 style(region): 优化区域页面搜索表单布局与样式
- 使用 section-card 组件包裹搜索表单,提升 UI 结构清晰度
- 调整表单项为弹性盒子布局,实现横向均匀分布
- 为选择器添加固定宽度,统一控件尺寸
- 将搜索按钮和清空按钮放置于表单内右侧,方便操作
- 添加局部样式,优化表单间距和按钮组间距
- 引入 section-card 组件并在 components 中注册
- 合并合同类别初始值为空,避免默认值误导用户
2026-09-02 11:12:32 +08:00
b2894lxlx 252df72f87 1、调整业务模块 2026-09-02 03:22:51 +08:00
b2894lxlx 80a15fc626 1、调整收付款
2、调整基础配置
3、调整结算
2026-09-01 17:35:53 +08:00
b2894lxlx f858eb2bcd 调整收付款问题 2026-09-01 11:52:22 +08:00
b2894lxlx a9b4ab069b Merge remote-tracking branch 'websoft/master'
# Conflicts:
#	src/views/settlement/receivable-payable-detail.vue
2026-08-31 19:30:16 +08:00
b2894lxlx 449e4e1884 调整收付款问题 2026-08-31 19:29:57 +08:00
b2894lxlx dea760ea1c 调整收付款问题 2026-08-31 12:40:35 +08:00
gxwebsoft 9b9e652afd refactor(organization): 将所属组织搜索组件统一替换为级联选择器
- 全站搜索栏所属组织由树形下拉 (el-tree-select) 统一替换为级联选择器 (el-cascader)
- 修改多个视图和组件中所属组织相关表单与搜索逻辑,适配级联选择器数据结构
- 组织数据结构调整及新增 findOrgPath、findDeptPath 等方法支持路径反查与回显
- 表单模型与搜索参数中所属组织字段改用 ID 路径数组,提交时转换为名称
- 优化所属组织搜索UI,统一清除、过滤、提示等功能样式和行为
- 引入 organization-search mixin 支持组织数据加载、路径转换及搜索参数格式化
- 更新相关依赖组件 Element Plus 引入 ElCascader 替换 ElTreeSelect
- 修正搜索表单CSS,新增el-cascader全宽样式支持搜索栏布局一致性
- 兼容老数据,支持多种部门ID格式自动转换到单选路径数组格式
- 业务部门与承办部门表单同样应用级联选择器及路径转换逻辑
- 相关搜索配置项所属组织字段类型由 input 改为 cascader,提升搜索交互体验
2026-08-31 12:19:18 +08:00
b2894lxlx 2451efdfcb Merge remote-tracking branch 'websoft/master' 2026-08-31 10:38:25 +08:00
b2894lxlx a75ac1cfa4 fix 2026-08-31 10:38:21 +08:00
gxwebsoft 3a08e12c65 fix(project-apply): 隐藏操作栏中的“作废”按钮
- 从操作配置中移除“作废”按钮相关条目
- 保留“作废”接口方法,避免影响现有接口调用
- 仅通过配置修改完成,不涉及组件代码变更
- 确保剩余操作项正常显示且菜单宽度保持不变
2026-08-30 13:27:20 +08:00
b2894lxlx a10eea247f Merge remote-tracking branch 'websoft/master' 2026-08-28 18:22:20 +08:00
b2894lxlx 4fb8c74b5c fix 2026-08-28 18:22:17 +08:00
gxwebsoft 0a77fd2014 fix(vehicle): 修复客户性质筛选条件清空问题
- 将客户性质筛选条件的默认选项由固定值更改为空数组
- 修正筛选下拉列表的加载行为,避免显示错误选项
- 确保字典数据正确刷新,提升筛选准确性
2026-08-28 16:58:14 +08:00
gxwebsoft 16af750030 feat(project-apply): 优化表单提示与客户承运商表格交互
- 将“项目资金使用额度”和“项目应收账款额度”提示从placeholder移至label右侧问号tooltip
- 调整提示文案为“实际业务回款周期内付款”和“回款期内最大应收账款额”
- 客户信息和承运商信息表格中“客户”“承运商”列变为可点击链接,点击跳转查看客商档案
- 删除客户和承运商表格中的“联系人”“联系方式”列
- 表单输入框placeholder统一调整为“请选择”或“请输入”
- 新增openCustomerArchive方法支持查看客商档案,缺失ID时提示警告
- 项目申请页面用户查询条件由account改为phone,相关表格列同步调整
- 部门管理权限调整,操作列只显示“添加”按钮,删除查看、编辑、删除按钮相关代码及样式
- Vite配置中更新本地开发端口为2889并修改API代理目标地址为局域网地址http://172.16.203.228:8000
- 修正项目申请页面部分样式,使label问号图标与文字baseline对齐和换行显示
2026-08-28 16:44:06 +08:00
b2894lxlx a3ff00eaaf Merge remote-tracking branch 'websoft/master' 2026-08-28 16:01:49 +08:00
b2894lxlx d901d484fd fix 2026-08-28 16:01:42 +08:00
gxwebsoft b6fed6892a feat(organization): 全站所属组织搜索统一为树形下拉并修复项目部门宽度
- 新增复用 mixin organization-search.js,封装树形组织搜索功能,自动识别不同架构
- 替换合同管理、运输能力(司机/车辆/船舶)、费用模块的所属组织搜索使用树形下拉组件
- 将自定义搜索表单中所属组织 input 替换为 el-tree-select,支持树节点选中并写回名称
- 防止不同页面间组织数据相互覆盖,mixin 内部树数据命名为 organizationTreeOptions
- 修复项目补录页面业务部门错用全宽样式,恢复半宽显示,调整注释避免误读
- 各相关页面组件中引入并使用 mixin,调用接口加载组织树数据,保障搜索功能一致性
2026-08-28 16:00:53 +08:00
gxwebsoft d7ac4e4045 Merge remote-tracking branch 'origin/master' 2026-08-28 15:41:13 +08:00
gxwebsoft f7eea88945 feat(login): 更换登录验证码为图形字符验证码
- 配置文件 website.js 中将 captchaType 从 'behavior' 改为 'image'
- 修改 userlogin.vue,图形验证码图片由输入框内联改为输入框旁独立显示
- 重写 login.scss 样式,实现验证码图片区域带边框和高亮效果
- 确保验证码图片点击可刷新,且支持从后端接口获取数据
- 登录页仍默认显示 IAM 登录,需切换「账号密码登录」显示验证码区域

feat(contract-manage): 优化所属组织搜索栏为树形下拉选择框

- 引入并应用 organization-search mixin 统一管理所属组织搜索
- contract-manage.js、driver.js、transport-ship.js、transport-vehicle.js 中所属组织搜索改为 searchslot 模式
- contract-manage.vue 中新增 renderOrganizationSearch 方法,渲染基于 ElTreeSelect 的树形下拉组件
- 组织树数据加载时自动排除「外部组织」节点,保持搜索选项清晰
- 所有相关所属组织搜索统一样式与交互,提升用户体验

feat(system): 用户和部门视图样式及操作改进

- user.vue 和 userinfo.vue 中 el-tabs 组件添加 border-card 类型,提升标签页视觉效果
- dept.vue 中添加查看、编辑、删除操作入口,绑定对应方法,通过权限控制显示权限按钮
- dept.vue 菜单宽度、对话框宽度及行为配置优化

style(login): 调整登录样式及验证码布局细节

- 调整 .login-code-wrap 及内部元素的 flex 布局,输入框与验证码图片并排显示
- 设置验证码图片容器尺寸、边框及 hover 高亮样式
- 确保验证码图片撑满容器并禁止用户选中

refactor(mixins): 新增 organization-search 统一搜索 mixin

- 实现组织树数据加载、排除外部节点、归一化及扁平化处理
- 提供 renderOrganizationSearch 方法用于渲染统一树形选择搜索框
- 减少重复代码,方便各页面快速接入和统一维护

chore(vehicle): 车辆页面引入 organization-search mixin

- 在车辆视图 vehicle.vue 中混入 organization-search,增强所属组织搜索功能一致性
2026-08-28 15:41:01 +08:00
b2894lxlx 9a4e0f7c03 fix 2026-08-28 15:30:24 +08:00
gxwebsoft 99d6284a58 style(billing-plan-editor): 使用 SectionCard 包裹计费方案编辑区块
- 引入 SectionCard 组件包裹计费方案显示和编辑区域
- 为默认方案信息添加带提示的 Info 图标和 Tooltip
- 优化模板缩进和结构,提升可读性

feat(contract-manage): 优化合同管理搜索和操作栏体验

- 允许签约类型字段作为搜索条件且支持排序和提示
- 将签约类型从快捷搜索区移动到标准搜索表单中
- 废除冗余的 contractSignType 状态,使用表单绑定自动处理
- 调整搜索栏快捷筛选区域顺序,实现与按钮组的视觉统一

fix(contract-manage): 修正底部操作栏文案及按钮显示逻辑

- 底部操作栏将“关闭”文案修改为“取消”,符合统一规范
- 草稿编辑页底部按钮增加[暂存/提交临时/提交正式],一致新增页操作
- 按钮样式调整,采用右侧对齐并使用按键间距12px替代gap

style(contract-manage-change): 统一变更页底部操作栏样式和交互

- 底部操作栏使用固定悬浮布局,避免内容被遮挡
- 调整按钮顺序为[取消][提交],并修改“关闭”为“取消”
- 增加统一的按钮间距与操作栏背景和阴影样式
- 为页面容器添加底部内边距,保障悬浮栏空间

style(contract-manage): 修复“合同文件”标题样式加粗问题

- 补充 scoped 样式,利用 :deep 影响子组件中标题的字体加粗和颜色

fix(customer-archive): 调整文本行高以优化显示效果

- 将内容文本的行高从18px调整为16px,提升视觉紧凑度

style(customer-archive): 调整必填项标签布局

- 删除因padding造成的标签视觉错位,确保label与控件垂直居中对齐
2026-08-28 14:56:37 +08:00
b2894lxlx 17bbde81b2 fix 2026-08-28 02:10:54 +08:00
b2894lxlx 3482d03905 Merge remote-tracking branch 'websoft/master' 2026-08-27 23:52:21 +08:00
b2894lxlx b446e34c94 完善收付款管理 2026-08-27 23:52:17 +08:00
gxwebsoft 704c3093d7 feat(project-apply): 调整项目管理表单及搜索功能
- 项目编号放开编辑,新增时可输入,编辑时禁用
- 取消项目简称的必填校验,仅限制最大长度20
- 业务部门、项目由来说明表单项占满一行,优化布局
- 业务部门下拉增加过滤,重大项目补录模式不显示外部组织
- 新增变更编号及变更日期字段,关联变更类型显示
- 搜索栏将项目负责人由下拉切换为手动输入模糊查询
- 新增项目编号、客户名称、下游承运商三个搜索字段
- 清理无用的搜索工具函数和配置,优化代码结构
2026-08-27 20:05:51 +08:00
gxwebsoft 36cc9c751c style(ui): 优化必填星号与多行label的对齐方式
- 修复客商档案中必填项红色星号 * 与首行文字未垂直居中的问题
- 全站统一必填星号 * 的垂直居中规则,兼容单行与多行 label
- 临时额度管理弹窗多行 label 调整星号与首字对齐,避免视觉偏移
- 调整客户档案中信用评分表和附件描述布局,使字段间距合理且内容可编辑
- /business/project-apply 搜索栏项目负责人改为手动输入,优化模糊查询体验
- 新增项目编号、客户名称、下游承运商三个搜索字段,增加查询灵活性
- 调整项目申请按钮权限顺序,新增及重大项目补录按钮功能互换
- 注释隐藏tags.vue中avue-tags相关菜单按钮,简化界面
- 修改vite开发服务器端口和代理配置,指向内网环境方便调试
- 更新 pnpm-lock.yaml 应用 avue 相关补丁,保证依赖兼容性
2026-08-27 19:45:14 +08:00
b2894lxlx e98aa61cd2 Merge remote-tracking branch 'websoft/master'
# Conflicts:
#	src/views/business/contract-manage.vue
#	src/views/business/project-apply.vue
2026-08-27 18:21:24 +08:00
b2894lxlx cec4c323a7 完善付款管理 2026-08-27 18:21:07 +08:00
gxwebsoft 58abe5ebdf feat(vehicle): 将客商档案搜索区所属组织改为树形下拉组件
- 搜索区所属组织从扁平 select 切换到多选树形下拉,保持后端字段不变
- 使用 Vue3 h 函数动态渲染 element-plus ElTreeSelect 组件,支持多选和折叠标签
- 修复 Vue 渲染字符串组件名导致控件无法显示的问题,改为直接导入组件对象
- 解决多选模型值写为字符串导致选中失效的状态管理问题,保持 modelValue 为 ID 数组
- 在搜索变更回调中将部门 ID 数组转换为部门名称逗号拼接
2026-08-27 17:34:22 +08:00
gxwebsoft fc70aeb2af style(transportCapacity): 优化船舶表单布局和标签样式
- 船舶表单“安放龙骨日期/建造完工日期”label宽度收窄至88px并支持换行
- 船舶证书信息两块表单调整为4列布局,字段span由8改为6
- contract-manage和track-tracking页签名标签最大宽度限制为96px,防止过宽
- project-apply业务部门选择组件改用el-tree-select支持树形选择,辅助函数新增
- 全局样式调整el-tree-select及el-cascader去除圆圈,改用箭头和文字高亮展示
- 控制节点展开箭头、选中状态和悬浮效果统一视觉风格
2026-08-27 16:40:38 +08:00
gxwebsoft 9342e674c5 style(contract-manage): 优化描述组件布局和样式调整
- 将多个 el-descriptions 组件的 columns 属性从3调整为4,提升信息展示密度
- 扩展备注栏跨度以适应新列数布局
- 修改网格布局grid-template-columns为4列,调整列间距与行间距适配新样式
- 调整部分宽度属性,使界面自适应更合理
- 在customer-archive视图中移除多余的section-card标签,清理无用代码
- 修改全局样式element-ui.scss中的表单项底部间距由1px改为8px,提升控件间距协调性
- 优化输入框及下拉框占位符及选中值颜色,使视觉识别度更高且统一全站设计规范
2026-08-27 15:55:17 +08:00
b2894lxlx a176608019 Merge remote-tracking branch 'websoft/master' 2026-08-27 12:54:21 +08:00
b2894lxlx 3a262216c7 fix bug 2026-08-27 12:54:17 +08:00
gxwebsoft c9f673342a style(ui): 统一底部操作栏按钮间距,取消所有gap样式
- 移除各底部操作栏(footer/__actions/page-actions)内所有gap属性
- 按钮间距统一采用 Element Plus 全局默认的相邻按钮12px margin-left规则
- 修改 .el-dialog__footer 内.el-button间距由8px调整为12px,保持全站一致
- 确认 Avue 表单底部按钮无自带间距,去gap后间距由 Element Plus 提供
- 涉及模块覆盖 vehicle、payment、business、settlement,移除了相关组件样式中gap属性
- MEMORY.md规范更新,补充底栏按钮间距统一为无gap纯文字规则
2026-08-27 11:39:13 +08:00
gxwebsoft 2acdcb08a9 style(ui): 统一弹窗及独立表单页按钮文案和样式调整
- 将所有弹窗及独立表单页中的“返回”按钮文案统一改为“取消”
- 独立表单页footer的Avue提交与清空按钮去除内置图标,仅保留文字显示
- 更新相关页面和组件按钮文案,包括业务表单、支付表单、客户档案等
- 规范底部操作栏按钮顺序及样式,确保主次操作区分明显
- 修复部分组件页脚按钮文案,保持全站一致性
2026-08-27 11:26:40 +08:00
b2894lxlx 2c08d0c362 Merge remote-tracking branch 'websoft/master' 2026-08-26 23:18:19 +08:00
b2894lxlx b36aefc7c4 fix bug 2026-08-26 23:18:14 +08:00
gxwebsoft 7f70052afd fix(ui): 统一 payment 和 settlement 模块浮动底栏为 fixed 定位
- payment 模块所有表单页底栏按钮顺序及布局全量对齐,取消居中改为右对齐浮动
- settlement 模块弹窗及独立页 footer 添加 sticky 浮动,主操作按钮样式调整为 plain
- 彻底修复浮动底栏不吸底问题,所有相关 footer 由 position: sticky 改为 position: fixed
- 统一 fixed 底栏样式模板,包含左偏移支持侧栏折叠和横向布局切换
- 更新项目文档,明确浮动底栏必须用 fixed 定位,避免 overflow:hidden 祖先导致 sticky 失效
- 清理所有残留 sticky 样式,确保底栏始终固定在视口底部,提升用户体验
2026-08-26 18:49:13 +08:00
gxwebsoft 84c46b9312 style(footer): 统一多页面底栏按钮样式与排序规则
- 规范 master-order-editor.vue 底栏按钮类型及排序,按钮层级清晰
- 调整 payment-application-form.vue 底栏按钮样式为右对齐并浮动底部
- 统一 invoice-application/receipt-form 页底栏按钮顺序,返回按钮置首
- 所有相关表单页按钮底栏统一采用 sticky 浮动,背景及阴影样式一致
- 修复多个页面按钮缺少 plain 样式导致视觉层级不明
- 按钮间距统一为 12px,底栏内边距调整为 12px 24px
- 调整多个表单页面按钮组从居中对齐改为右对齐,提升操作一致性
- 保留弹窗 footer 按钮布局,不影响各页面独立弹窗交互体验
2026-08-26 18:43:03 +08:00
gxwebsoft 88c5f58548 fix(business-crud-page): 修正独立表单页关闭按钮位置顺序
- 独立表单页 shipping-template 中关闭按钮从 #menu-form 移动至 #menu-form-before 插槽
- 关闭按钮置于运费合计与保存草稿按钮之间,保证“次要→主操作”顺序一致
- 关闭按钮仅在独立表单页显示,弹窗模式保持原有顺序不变
- 清理了 #menu-form 槽中残留代码,避免影响渲染
- 修改 Element Plus 相关禁用输入框文字颜色,统一为 #909399,覆盖默认主题差异
2026-08-26 18:35:05 +08:00
gxwebsoft 1cf8abb162 style(styles): 统一全站 disabled 输入框文字颜色
- 解决客户档案编辑页不可编辑状态下不同电脑显示颜色不一致问题
- 统一覆盖 Element Plus 默认 disabled 文字色为 #a8abb2
- 防止主题或浏览器导致的颜色差异,保持全站一致性
- 样式修改添加至 element-ui.scss,紧跟表头颜色统一规则
- 提供便捷调整建议,如需更深色可改为 #606266
2026-08-26 18:23:35 +08:00
gxwebsoft 201ca245a7 style(customer-archive): 统一客户档案页及弹窗底部按钮样式与对齐规则
- 按新制定的项目模块按钮底栏标准调整 customer-archive 页面及相关弹窗的 footer 样式
- 修改按钮顺序及样式,确保返回/取消居左,保存按钮为蓝描边 plain,提交和主操作按钮为深蓝实心
- 将子弹窗按钮容器对齐方式由居中改为 flex-end,统一右对齐布局
- 删除底部按钮 success 绿样式,全部主操作使用深蓝实心 primary 样式
- 调整部分按钮显示逻辑及文本,增强阅读顺序和视觉层级感
- 更新 SCSS 样式以匹配新布局和对齐规则
- 调整全局输入框及下拉框 placeholder 颜色,加深至 #909399 保持视觉一致性
- 在 MEMORY.md
2026-08-26 18:16:28 +08:00
gxwebsoft bfb8659cd8 style(project-apply): 统一弹窗和独立表单底栏按钮风格
- 统一「保存+提交+返回」按钮的视觉层级和排序
- 按次要(默认)、中间(primary plain)、主操作(primary)三档区分按钮样式
- 调整按钮顺序为「辅助→返回/取消→保存草稿→提交」,主操作始终最右
- 修改project-apply、invoice-receipt-form、invoice-application-form三个页面的按钮实现
- 在 MEMORY.md 中新增底栏按钮统一规则说明
- 保留已合规页面按钮风格,避免重复修改
2026-08-26 18:05:58 +08:00
gxwebsoft ae45e88df9 fix(styles): 统一全站 el-table 表头文字颜色,修复 project-apply 表头色差问题
- 解决 project-apply 页面表头颜色在不同电脑显示不一致的问题
- 在 element-ui.scss 中新增高优先级规则,统一 el-table 表头文字颜色为 #374151
- 清理 project-apply.vue 中与全局样式冲突且无效的局部表头样式,保留字体加粗样式
- 修改选择器为 :deep(.el-table__header th .cell),确保样式生效且不影响背景色
2026-08-26 17:53:44 +08:00
b2894lxlx 9d512ebad4 Merge remote-tracking branch 'websoft/master' 2026-08-26 15:57:49 +08:00
b2894lxlx 5dc3c2d2c5 1、调整合同
2、调整项目
3、调整客商
2026-08-26 15:57:46 +08:00
b2894lxlx 70ebe31103 1、调整合同
2、调整项目
3、调整客商
2026-08-26 15:56:35 +08:00
gxwebsoft c3f05a342e style(forms): 全站表单项间距统一调整为14px
- 统一调整settlement、business、payment模块以及全局样式中.el-form-item的margin-bottom从16/18/20px改为14px
- 保留搜索栏相关的.el-form-item间距为8px和特殊分组risk-disposal的28px
- 修正多个页面和组件的局部样式覆盖问题,确保填写区表单项间距一致
- 更新element-ui.scss中.dialog-form--carded对应的margin-bottom为14px
- 维护部分表单组件原本使用的重要样式不变,保证视觉统一且不影响特殊布局
2026-08-26 15:38:39 +08:00
gxwebsoft dca137df8d style(voucher-manage): 调整操作列宽度优化布局
- 将表格中操作列宽度从120调整到160
- 进一步将另一操作列宽度从160调整至180
- 优化操作列的显示空间,提升界面可用性与美观度
2026-08-26 15:09:10 +08:00
gxwebsoft 8b1ac09592 style(voucher-manage): 调整操作列宽度优化布局
- 将表格中操作列宽度从120调整到160
- 进一步将另一操作列宽度从160调整至180
- 优化操作列的显示空间,提升界面可用性与美观度
2026-08-26 14:41:30 +08:00
gxwebsoft 35a43cce7a style(payment): 调整账单分类表单及搜索组件样式
- 调整账单分类表单内数字输入框文本左对齐
- 优化账单搜索组件快捷筛选标签布局位置
- 修改搜索区域按钮及标签对齐方式,按钮居右显示
- 统一快捷筛选标签的代码位置,提升组件结构清晰度
2026-08-26 14:36:31 +08:00
gxwebsoft 808a52ae25 style(payment): 优化票据搜索条件交互和标签宽度统一
- 票据账册搜索改用 el-check-tag 实现快捷筛选,提升用户交互体验
- 查询按钮增加图标,重置按钮同样添加图标及文字说明
- 调整票据搜索表单标签宽度由160px统一为88px,优化界面一致性
- 全站多处搜索栏表单标签宽度由160px改为88px,涵盖多页面及组件
- 修复业务组件内el-autocomplete输入框宽度适配,避免placeholder被裁剪
- 业务页面货物名称表格列宽由150调整为240,改善显示效果
- 修正系统部门操作列宽度避免按钮竖排溢出问题
- 修改多个表单页和搜索板块样式,确保标签宽度和对齐一致
- 调整表单按钮等样式,提升整体页面美观性和操作友好度
2026-08-26 12:09:10 +08:00
gxwebsoft 899a8c1c2b style(payment): 优化票据搜索条件交互和标签宽度统一
- 票据账册搜索改用 el-check-tag 实现快捷筛选,提升用户交互体验
- 查询按钮增加图标,重置按钮同样添加图标及文字说明
- 调整票据搜索表单标签宽度由160px统一为88px,优化界面一致性
- 全站多处搜索栏表单标签宽度由160px改为88px,涵盖多页面及组件
- 修复业务组件内el-autocomplete输入框宽度适配,避免placeholder被裁剪
- 业务页面货物名称表格列宽由150调整为240,改善显示效果
- 修正系统部门操作列宽度避免按钮竖排溢出问题
- 修改多个表单页和搜索板块样式,确保标签宽度和对齐一致
- 调整表单按钮等样式,提升整体页面美观性和操作友好度
2026-08-26 11:59:49 +08:00
b2894lxlx dc5a2235eb 1、调整合同
2、调整项目
3、调整付款管理
2026-08-26 02:31:12 +08:00
b2894lxlx 3da2d428f3 fix bug 2026-08-25 22:35:50 +08:00
b2894lxlx cb98355715 Merge remote-tracking branch 'websoft/master'
# Conflicts:
#	src/views/business/contract-manage-change.vue
2026-08-25 20:47:06 +08:00
b2894lxlx 335e45c3f1 fix bug 2026-08-25 20:46:37 +08:00
gxwebsoft 325e708ef1 style(business-crud-page): 调整地图标记和输入框图标字体大小
- 将地图标记后缀文字字体大小设置为18px
- 设置输入框图标的字体大小为18px
- 调整配送模板地图后缀的字体大小为18px
- 保持原有颜色和悬停样式不变
2026-08-25 20:42:11 +08:00
gxwebsoft 112acbbf34 refactor(business): 用el-button替换地址选择链接并统一图标样式
- 将business-crud-page.vue中发货地、收货地地址选择操作由el-link替换为带提示的el-button组件
- 所有地址选择按钮统一使用OfficeBuilding图标代替List图标
- master-order-editor.vue中的常用地址选择按钮图标由List改为OfficeBuilding
- 增加样式定义调整地址选择按钮图标大小和颜色
- 优化按钮交互体验,保持禁用状态时按钮不可点击
2026-08-25 20:37:47 +08:00
gxwebsoft d58f73ab50 style(loading-manage): 调整变更备注输入框行数及卡片最小高度
- 设置变更备注输入框默认显示2行,提升输入体验
- 为 section-card 添加最小高度限制,防止内容过少时布局过紧
- 注释掉无用样式,保持代码整洁
2026-08-25 20:18:19 +08:00
gxwebsoft fdd17c5878 style(loading-manage): 调整变更备注输入框行数及卡片最小高度
- 设置变更备注输入框默认显示2行,提升输入体验
- 为 section-card 添加最小高度限制,防止内容过少时布局过紧
- 注释掉无用样式,保持代码整洁
2026-08-25 20:07:16 +08:00
b2894lxlx 665503f43e Merge remote-tracking branch 'websoft/master' 2026-08-25 18:39:57 +08:00
b2894lxlx 4340c9fc59 fix bug 2026-08-25 18:39:51 +08:00
b2894lxlx d6b6be0e9b Merge remote-tracking branch 'websoft/master' 2026-08-25 16:14:02 +08:00
b2894lxlx 55583551b5 fix bug 2026-08-25 16:13:58 +08:00
292 changed files with 70971 additions and 10257 deletions
+3 -2
View File
@@ -3,8 +3,9 @@
VITE_APP_ENV = 'development'
#接口地址
# 开发环境建议填 [/api]:由下方 vite.config.mjs 的 proxy 同源转发到后端(172.16.203.228:8000),避免跨域(CORS)被浏览器拦截
# 如需直连线上绝对地址(如 http://172.16.203.228:8000/api),必须让后端 CORS 把 Allow-Origin 改为具体前端域名,不能用 `*`(配合 credentials 会被浏览器拒绝)
# 开发环境建议填 [/api]:由 vite.config.mjs 的 proxy 同源转发到本地网关 http://localhost(80)
# 本地需先启动:Nacos + blade-gateway(80) + blade-auth + blade-system 等;8080 端口一般是 Nacos 控制台不是网关
# 如需临时联调远程,把 vite proxy target 改为 http://172.16.203.228:8000 并去掉 rewrite
VITE_APP_API=/api
#调试参数
+1
View File
@@ -1,5 +1,6 @@
.DS_Store
node_modules
pnpm-lock.yaml
/dist
/tests/e2e/videos/
+343
View File
@@ -0,0 +1,343 @@
# 2026-08-26
## el-table 内嵌 el-autocomplete 撑满列宽(全局规则补充)
**问题**: `business/transport-plan/form?mode=add` 独立表单页"货物信息"表格,
「货物名称」列 placeholder `请输入货物名称` 只显示 `请输入货物名…`。
**根因**: `element-ui.scss` 兜底规则只覆盖 `.el-input`,而 `<el-autocomplete>` 在 Element Plus 中
根节点是 `.el-autocomplete` 包装层,内部才是 `.el-input`,中间多了一层导致选择器不命中,
autocomplete 没有拉满 cell,placeholder 被内边距 / clear 图标挤压截断。
**修复**:
- `src/styles/element-ui.scss` 第 663 行兜底规则追加:
- `.cell > .el-autocomplete` `width:100%`
- `.cell > .el-autocomplete > .el-input` `width:100%`
- `.cell > .el-autocomplete .el-input__wrapper` `width:100%`
- `src/views/business/components/business-crud-page.vue:1520` 独立表单页
"货物名称"列 `min-width="150"` → `240`(与 1544 行「货物类型」cascader 列一致)。
其它内嵌 `<el-select>` / `<el-input>` / `<el-cascader>` 不受影响(选择器依旧命中),
3626 行(调度明细「货物名称」用的是 el-select)未做改动。
## 全站搜索栏 label 宽度统一 88px
**问题**: 主人反馈 `business/master-order` 搜索区 label 宽度 160px,要求全站统一改成 88px,
参考 `business/shipping-template`(已经是 88px)。
**调研**:
- `src/styles/element-ui.scss:133-136` 已有 `.avue-crud__search .el-form-item__label { min-width: 88px; }`,
全站 Avue 自动搜索栏视觉上就是 88px。
- shipping-template 走 `<business-crud-page>`(Avue),所以 label 是 88px。
- 但是项目里有 18 个文件,22 处直接手写 `<el-form ... label-width="160px">`,不走 `.avue-crud__search`,
视觉就是 160px。这些都是**搜索/筛选** `<el-form>`(顶层 query 或子表单),不是表单 dialog 内填表。
**改动**: 全部 22 处 `label-width="160px"` → `label-width="88px"`。
涉及文件:
- business: master-order.vue, voucher-manage.vue(3 处), loading-manage.vue
- payment: invoice-application, invoice-receipt, receipt-flow, receipt-claim-record,
payment-application, components/bill-ledger-search, components/bill-payment-search
- settlement: settlement-adjustment, receivable-payable-detail, pre-settlement,
formal-settlement, transport-reconciliation, components/transport-reconciliation-editor,
components/formal-settlement-editor(3 处,都是子筛选)
- transit: risk-disposal, exception-disposal
**遗漏补丁**: `transit/exception-disposal.vue` 搜索区不走 Avue,而是在 scoped 样式里用
`:deep(.el-form-item__label) { min-width: 160px }` 写死,盖过了全局 88px。
已改为 `min-width: 88px`(文件第 443 行)。`risk-disposal.vue` 无此 scoped 覆盖,本来就是 88px。
**未做改动**: 表单 dialog 内填写项(`shipping-template-dialog` /
`transport-plan-dialog` / `waybill-manage-dialog` 等)走专门的 dialogCustomClass + labelWidth:100,
不受本规则影响。
其余残留 160px 均非搜索栏: project-apply.vue:2095(表单详情弹窗 label,故意留 6 汉字)、
customer-archive.vue:4980(按钮 min-width)、cargo-type.vue:604(选择器拼错不生效,设 50px)。
## payment-application 表单 input 文字居左
- `src/views/payment/payment-application-form.vue`:追加 scoped 样式,使 `el-input-number` 的输入文字居左对齐(付款比例、申请付款金额等数字字段),与 bill-ledger-form 保持一致。
## 正式结算查看页去除内容行高
**问题**: `settlement/formal-settlement/form?mode=view` 中只读字段(如项目名称、合同名称)
多行文本行高过大,Element Plus 默认 `.el-form-item__content` 的 `line-height` 导致显示松散。
**修复**: `src/views/settlement/components/formal-settlement-editor.vue` scoped 样式追加:
```scss
.formal-editor :deep(.el-form-item__content) {
line-height: normal;
}
```
## 全站表单项间距统一 14px
**诉求**: `settlement/pre-settlement/form?mode=add` 中 `.el-form-item` 间距偏大,要求统一为 14px 且全站一致(用户指"表单项间距")。
**排查**: 全站 `.el-form-item` 的 `margin-bottom` 取值不一致——表单填写区有 16px / 18px / 20px,
搜索栏统一 8px(保留),个别特殊分组 28px(risk-disposal,保留)。全局 `.el-form-item--default`
已是 14px,但各页面 scoped `:deep(.xxx .el-form-item)` 覆盖成了 16/18/20px。
**改动(表单填写区统一 14px,16/18/20 → 14)**:
- settlement: `components/pre-settlement-editor.vue:1637`(16→14)、
`components/formal-settlement-editor.vue:1560`(16→14)、
`receivable-payable-detail.vue:1860`(16→14)、
`components/transport-reconciliation-editor.vue:805`(16→14)、
`components/settlement-adjustment-editor.vue:560`(16→14)
- business: `voucher-manage.vue:1022`(16→14)、`loading-manage.vue:2751`(16→14)、
`project-apply.vue:2091`(16→14)、`contract-manage.vue:2253`(16→14)、
`components/business-crud-page.vue:13380/13894`(16→14)、
`components/master-order-dispatch.vue:700`(16→14)
- payment: `invoice-receipt-form.vue:755`(16→14)、`user.vue:1097`(16→14)、
`bill-ledger-form.vue:564`(16→14)、`payment-application-form.vue:1324`(16→14)、
`bill-payment-form.vue:382`(16→14)
- 全局: `components/cron-editor/main.vue:579`(18→14)、
`styles/element-ui.scss:430`(`.dialog-form--carded` 20→14)
**保留未动**: 所有 `__search` 搜索栏的 8px、risk-disposal 的 28px 特殊分组、transportCapacity
vehicle/driver/ship 与 customer-archive 等本就是 14px(含 !important)。
## project-apply 表头颜色在不同电脑不一致
**问题**: 客户反馈 `business/project-apply/form?mode=add` 页面中「客户信息」等表格表头颜色
在用户电脑是 `#374151`,客户电脑是 `#909399`。
**根因**:
- `src/styles/theme/go.scss` 中 `.theme-go .el-table tr th .cell { color: #374151 }` 把表头文字写死为 `#374151`。
- 客户电脑未使用 `theme-go`,表头文字走 Element Plus 默认 CSS 变量
`--el-table-header-text-color: #909399`。
- `src/views/business/project-apply.vue` 里的 `:deep(.el-table__header th) { color: #303133 }`
只作用在 `th` 上,颜色靠继承,优先级敌不过直接写在 `.cell` 上的规则和 Element Plus 默认值,
所以实际没生效。
**修复**:
- `src/styles/element-ui.scss` 在「统一全站 el-table 表头背景色」之后追加规则:
```scss
.el-table .el-table__header-wrapper th.el-table__cell .cell {
color: #374151;
}
```
直接作用于 `.cell`,特异性高于 `theme-go` 和 Element Plus 默认,全站统一为 `#374151`。
- `src/views/business/project-apply.vue` 清理两处与全局冲突且不生效的局部表头样式:
删除 `background: #f5f7fa; color: #303133;`,保留 `font-weight: 600`,
选择器改为 `:deep(.el-table__header th .cell)`。
## 弹窗 / 独立表单页底栏「保存+提交+返回」按钮统一风格
**问题**: 主人截图 `business/project-apply/form?mode=add` 页面底部 `[保存][提交][返回]` 三个按钮,
- 保存 + 提交 都是 `type="primary"` 蓝实心,视觉层级无区分。
- 排序「主→主→次」,主操作不靠右,与全站其它页面(business-crud-page 等)已用的「次→中→主」不一致。
**修复**: 统一定 3 档层级 + 「次→中→主」排序。
| 语义 | 写法 |
| --- | --- |
| 次要(返回/取消) | 默认(无 type)灰描边 |
| 中间步骤(保存草稿) | `type="primary" plain` 蓝描边 |
| 主操作(提交) | `type="primary"` 蓝实心 |
排序模板:`[可选辅助(如同步)][返回/取消][保存草稿][提交]`,主操作永远最右。
**改动文件**:
- `business/project-apply.vue:617-651` — 主 footer + 变更 footer 都重排,`保存` 加 plain。
- `payment/invoice-receipt-form.vue:242-252` — 保存加 plain,返回挪到保存前面。
- `payment/invoice-application-form.vue:419-429` — 同上。
**未改动(已合规)**:
- `settlement/components/pre-settlement-editor.vue` — 默认已是「默认保存 + primary 提交」。
- `settlement/components/transport-reconciliation-editor.vue` — 两个 plain 中间步骤 + 完成对账 实心,层级清晰。
**沉淀**: `MEMORY.md` 新增「弹窗 / 独立表单页底栏按钮统一规则」一节,作为新页面 footer 写法标准。
## customer-archive 全部 footer 同步按新增项目模块标准对齐
**问题**: 主人指出客户档案页面 `/vehicle/customer-archive/form?name=新增客商档案` 没有按统一规则调整,
并明确要求「浮动页面底部、按钮区域靠右对齐,返回在第一个,中间的用浅蓝色,最后一个是 primary 深蓝色」,
参考新增项目模块 `/business/project-apply/form?mode=add`。
### customer-archive.vue 共 5 处 footer 改动
- `archive-form__footer` (802-809) 原状「取消/返回 + 保存 primary + 提交 success 绿」混合 4 按钮。改为 `[返回/取消/关闭] + 保存 plain + 提交 primary`,并删掉「success 绿」换成 primary 深蓝。
- `contact-dialog` (867-870) 原状 `[保存 primary][返回]`,容器居中。改为 `[返回][保存 plain]`,容器 flex-end。
- `receipt-dialog` (965-969) 同上模式。
- `invoice-dialog` (1085-1089) 同上模式。
- `score-detail-dialog` (1343-1359) 原状 total 左 + `[保存 primary][复评确认 primary][返回]`。改为 total 左 + `[返回][保存 plain][复评确认 primary]`。
### customer-archive.vue SCSS 同步
- `.contact-dialog__footer-actions` `justify-content: center` 改 `flex-end`。
- `.receipt-dialog__footer-actions` 同上。
- `.invoice-dialog__footer-actions` 同上。
- `.archive-form__footer` 维持 flex-end 不变(独立页 `.archive-page-form &` 下 `position: fixed` 浮动底部已经写好)。
### 顺序小结(标准模板)
```vue
<div class="xxx-dialog__footer-actions">
<!-- 最左:信息聚合(如总分合计、统计)用 flex:1 自己推 -->
<!-- 次要:默认描边灰按钮 -->
<el-button @click="close">返回</el-button>
<!-- 中间:浅蓝实心 plain -->
<el-button type="primary" plain @click="saveDraft">保存</el-button>
<!-- 主操作:蓝实心 -->
<el-button type="primary" @click="submit">提交</el-button>
</div>
```
左→右阅读顺序:辅助信息聚合 → 次要 → 中间步骤 → 主操作。视觉权重递增,颜色从「灰 → 蓝描边 → 蓝实心」。
## 全站 disabled 输入框文字颜色统一
**问题**: 客户档案编辑页等「不可编辑」状态下,`el-input` / `el-textarea` / `el-input-number` 的文字颜色
在不同电脑显示不一致(有的深、有的浅)。
**根因**: Element Plus 默认 disabled 文字色由 `--el-disabled-text-color` 控制,不同主题/浏览器/变量覆盖下可能表现为 `#374151` / `#606266` / `#a8abb2` / `#909399` 等。
**修复**: `src/styles/element-ui.scss` 在表头颜色统一规则后追加:
```scss
.el-input.is-disabled .el-input__inner,
.el-textarea.is-disabled .el-textarea__inner,
.el-input-number.is-disabled .el-input__inner {
color: #a8abb2;
}
```
直接写死为 `#a8abb2`,覆盖 Element Plus 默认 CSS 变量及主题差异,保证全站一致。
**如需再深**,可把 `#a8abb2` 改为 `#606266`。
## shipping-template 独立表单页「关闭」按钮位置修正
**问题**: `/business/shipping-template/form?mode=add`(独立表单页)底部 `[提交][关闭]` 顺序,
主操作「提交」在左、次要「关闭」在右,与全站「次→主」规则不符。
**根因**: shipping-template 走 `business-crud-page` 复用的 PageAvueForm,footer 用的是 Avue-form 内置结构。
Avue-form 内置按钮默认在中间,自定义内容分两侧插槽:
- `#menu-form-before` slot → 在 Avue 内置按钮之左
- `#menu-form` slot → 在 Avue 内置按钮之右
之前「关闭」按钮写在 `#menu-form` slot(在 Avue 内置按钮之右),导致渲染成 `[Avue 提交][关闭]`,顺序反了。
**修复**: `src/views/business/components/business-crud-page.vue:2059-2090`
- 把「关闭」按钮从 `#menu-form` slot 移到 `#menu-form-before` slot 的「保存草稿」按钮之前。
- 触发条件 `v-if="isStandaloneFormPage && !showTransportPlanCreateActions"`,仅独立表单页生效;
弹窗模式(list 页内点新增/编辑)继续走 Avue 自带的 `[取消][提交]` 不动。
- 「关闭」按钮文字保持默认色(次要),无 type。
- 同步清空 `#menu-form` slot 中 shipping-template 残留(实际只剩 `showTransportPlanCreateActions` 块,
shipping-template 不会触发,留着不影响)。
**最终渲染**(独立表单页):
- 有运费合计 + 草稿:`[运费合计][关闭][保存草稿 plain][Avue 提交]`
- 仅有运费合计:`[运费合计][关闭][Avue 提交]`
- 无运费合计:`[关闭][Avue 提交]`(即主人截图场景)
**未触发场景**:
- transport-plan / waybill / loading-manage 等其它走 business-crud-page 但未声明 `standalone-form-page` 的页面,
本次未改。需要时可按相同模式补 slot。
## master-order 编辑页底栏按钮统一
**问题**: `/business/master-order?mode=editor`(多联总单编辑/新增页)底部按钮没统一。
**根因**: 编辑页是 `master-order.vue` 的 `mode==='editor'` 模板里内嵌 `<master-order-editor>` 组件,
主 footer 在 `src/views/business/components/master-order-editor.vue:534-539`。
**原状**:
```
[返回 default] [暂存 default] [确认创建 primary] [创建并调度 primary]
```
- 暂存 无 type,与返回同为灰描边,缺层级
- 确认创建 / 创建并调度 两个都 primary 实心,无视觉优先级
- footer 仅 `flex-end` + `padding:16px 0`,不浮动
**修复** (`master-order-editor.vue`):
- 按钮改:`[返回 default] [暂存 primary plain] [确认创建 primary plain] [创建并调度 primary]`
严格按「返回第一个、中间浅蓝 plain、最后深蓝实心」规则,仅最右一个深蓝。
- footer 样式改浮动底部:`position: sticky; bottom:0; z-index:10; background:#fff;
border-top:1px solid #eff1f7; box-shadow:0 -2px 8px rgba(0,0,0,.06); gap:12px; padding:12px 24px`。
**未动**: 页内各 el-dialog 的 footer(线路/地图/常用货物/货物导入等)已是 `[关闭 default][确定 primary]`,
符合「次→主」规则,无需调整。
## payment-application 表单页底栏按钮统一
**问题**: `/payment/payment-application/form?mode=add`(付款申请表单页)底部按钮没统一。
**根因**: 独立表单页 `src/views/payment/payment-application-form.vue` 的 `.payment-form-page__actions` footer(330 行)。
**原状**:
```
justify-content: center(居中)
[返回 default] [保存 default(缺 plain)] [提交 primary]
```
- 保存 无 type,与返回同为灰描边,缺层级
- footer 居中且不浮动
**修复** (`payment-application-form.vue`):
- 按钮:`[返回 default] [保存 primary plain] [提交 primary]`,仅最后深蓝。
- footer 样式:`justify-content: center` → `flex-end`;加 `position: sticky; bottom:0; z-index:10;
background:#fff; border-top:1px solid #eff1f7; box-shadow:0 -2px 8px rgba(0,0,0,.06); gap:12px; padding:12px 24px`。
**未动**: `payment-application.vue`(list 表格页,无独立 footer);`invoice-application-form.vue` / `invoice-receipt-form.vue` 上一轮已改。
## invoice-application / invoice-receipt 表单页底栏按钮统一
**问题**: `/payment/invoice-application/form?mode=add` 底部按钮没统一(用户反馈)。排查发现同族 `invoice-receipt-form.vue` 完全同款问题,一并修了。
**根因**: 两个发票表单页 footer 都是 `[同步 plain][返回 default][保存 plain][提交 primary]` + `justify-content: center`(居中),且未浮动。
- 同步 在 返回 之前,违反「返回在第一个」
- footer 居中,不靠右、不浮动
**修复**(`invoice-application-form.vue` 419-433 + 980;`invoice-receipt-form.vue` 242-256 + 777):
- 按钮重排为 `[返回 default][同步 plain][保存 plain][提交 primary]`,返回置首、最后仅提交深蓝。
- footer 样式:`justify-content: center` → `flex-end`;加 `position: sticky; bottom:0; z-index:10;
background:#fff; border-top:1px solid #eff1f7; box-shadow:0 -2px 8px rgba(0,0,0,.06); gap:12px; padding:12px 24px`。
**注意**: 之前首轮曾把这两个页「保存加 plain、返回挪到保存前面」,但当时没动 同步 的位置、没改居中/浮动,所以用户仍觉得没统一。本次彻底对齐。
## payment + settlement 模块底栏按钮全量统一
**背景**: 用户要求检查 payment 模块和 settlement 模块下所有页面底栏按钮是否统一。
### payment 模块(表单页 footer)
| 文件 | 原状 | 修复 |
| --- | --- | --- |
| `payment/bill-ledger-form.vue` | `[确认 primary][取消 default]` + 居中 | `[取消 default][确认 primary]`;footer flex-end + sticky 浮动 |
| `payment/bill-payment-form.vue` | `[保存 primary][提交 primary][取消 default]` | `[取消 default][保存 plain][提交 primary]`;footer 加 sticky 浮动(已 flex-end) |
| `payment/receipt-flow-form.vue` | `[确认 primary][取消 default]` + 居中(class 误用 `receipt-claim-form-page__actions`) | `[取消 default][确认 primary]`;footer flex-end + sticky 浮动 |
| `payment/receipt-claim-record-form.vue` | 单「关闭」按钮居中 | footer flex-end + sticky 浮动 |
| (此前已改)invoice/payment-application 各表单页 | — | 已合规 |
### settlement 模块
| 文件 | 原状 | 修复 |
| --- | --- | --- |
| `settlement/components/pre-settlement-editor.vue` | 两处 footer `[取消][保存 default][提交]`(弹窗 + pageMode) | 保存加 `type="primary" plain`;`&__page-actions` 加 sticky 浮动 |
| `settlement/components/formal-settlement-editor.vue` | footer 已 `[取消][提交]` 合规,独立页未浮动 | `.formal-editor__page-actions` 加 sticky 浮动 |
| `settlement/receivable-payable-detail.vue` | 生成费用弹窗 `[取消][上一步 primary][提交 primary]` | 上一步改 `type="primary" plain` |
| (已合规未动)`transport-reconciliation-editor.vue` | `[取消][保存草稿 plain][按匹配结果更新账单 plain][完成对账 primary]` | 已符合次→主 |
| (已合规未动)`settlement-adjustment-editor.vue` | 弹窗 `[取消][保存 primary]` | 合规 |
**注意**: settlement 的 `*_editor.vue` 多为「弹窗(!pageMode) + 独立页(pageMode)」双模式;弹窗 footer 走 `.el-dialog__footer` 全局规则(已右对齐/浮动),仅 pageMode 的 `.xxx__page-actions` 需手动加 sticky。
**注意**: payment 表单页剩余的 `justify-content: center` 全在 `__links` 链接行(非 action footer),属正常布局,未改。
## 浮动底栏根本修复:sticky → fixed
**问题**: 用户反馈 `/settlement/formal-settlement/form?mode=add`「有些页面还没能固定在底部」。
**根因(关键)**: 之前给 9 个独立表单页 footer 写的都是 `position: sticky; bottom:0`,但本项目布局
`.app-main` / `.basic-container` 祖先带 `overflow:hidden`,sticky 的滚动容器判断失效 → footer 永不吸底。
customer-archive 当初就是因此改用 `position:fixed`,且要带侧栏左偏移。
**修复(9 个文件全部 sticky→fixed)**:
- `business/components/master-order-editor.vue` `<footer>`(2050 行,含 `:global(.avue--collapse footer)`/`horizontal`)
- `payment/invoice-receipt-form.vue` / `bill-ledger-form.vue` / `receipt-claim-record-form.vue` /
`payment-application-form.vue` / `bill-payment-form.vue` / `invoice-application-form.vue` / `receipt-flow-form.vue`
各 `__actions`(均加 fixed + `:global(.avue--collapse .xxx)`/`horizontal` 左偏移 60px/0)
- `settlement/components/pre-settlement-editor.vue` `&__page-actions`(nested,全局覆盖放 style 末尾)
- `settlement/components/formal-settlement-editor.vue` `.formal-editor__page-actions`(上一轮已改 fixed,本轮复核确认)
**统一 fixed 模板**:
```scss
.xxx { position: fixed; right:0; left:230px; bottom:0; margin:0; z-index:10; flex-end + padding + border-top + 白底 + 上投影 }
:global(.avue--collapse .xxx) { left:60px; }
:global(.avue-layout--horizontal .xxx) { left:0; }
```
注意 `:global()` 要包整个选择器(customer-archive 写法),只包前缀会让 scoped 在中间选择器追加 data 属性而不匹配。
**验证**: 全仓 `position: sticky` 已清零(grep 无残留)。
+12
View File
@@ -0,0 +1,12 @@
# 2026-08-27 工作日志
## project-apply 表单 UI 调整(新增 / 重大项目补录)
文件:`src/views/business/project-apply.vue`
按截图需求修改项目管理表单(新增 + 重大项目补录同步):
1. 项目编号放开编辑(去掉 `disabled`),placeholder 改为"请输入项目编号,不填自动生成";编辑模式仍禁用。
2. 项目简称取消必填校验(仅保留 maxlength 20)。
3. 业务部门、项目由来说明单独占一行(新增 class `project-apply-form__item--full` + 样式 `grid-column: 1 / -1`)。
4. 业务部门下拉过滤"外部组织":原仅 `['add','edit']` 过滤,已补上 `majorSupplement`,重大项目补录不再显示外部组织。
待确认:后端 submit/saveDraft 在 `projectCode` 为空时是否自动生成编号(用户让后端同事补)。
+82
View File
@@ -0,0 +1,82 @@
# 2026-08-28 工作日志
## contract-manage 表单底部操作栏间距/文案修复
- 文件:`src/views/business/contract-manage.vue`
- 问题:底部浮动操作栏 `.contract-manage-page__footer` 用 `gap:8px`,与全站统一 12px 标准不一致("间距不一致不统一")。
- 修复:去掉 `gap:8px`,改用 `.el-button + .el-button { margin-left:12px }`,对齐全站"去 gap、12px"规范。
- 文案:底部"关闭"→"取消"(底部操作栏统一命名规范;详情弹窗/流程图弹窗的"关闭"为只读/纯关闭场景,按规范保留)。
## contract-manage 草稿编辑页按钮缺失修复
- 问题:编辑态(含草稿编辑)底部只有单个"提交"(submitEdit),缺"暂存/提交临时合同/提交正式合同",与新增页不一致。
- 修复:新增 computed `isDraftEdit`(`formMode==='edit' && form.approvalStatus==='draft'`);footer 三按钮条件由 `formMode==='add'` 改为 `formMode==='add' || isDraftEdit`,草稿编辑复用 `submitCreate`(按 id upsert + temporary/formal 提升),非草稿编辑仍走 submitEdit 单个"提交"。
## contract-manage-change 变更页底部操作栏统一改造
- 文件:`src/views/business/contract-manage-change.vue`
- 问题:`.page-footer` 左侧对齐、`gap:16px`、不悬浮,且按钮顺序/命名(提交→关闭)与新增页规范相反。
- 修复:
- DOM:按钮重排为 `[取消][提交]`(次要居左、主操作居右),"关闭"→"取消"。
- 样式:`.page-footer` 改为 `position:fixed` 悬浮吸底(left:230px / avue--collapse 60px / avue-layout--horizontal 0)、`justify-content:flex-end` 右对齐、去 gap 改用 `.el-button+.el-button{margin-left:12px}` 统一 12px、白底+上边框+上投影。
- 容器 `.contract-change-page` 加 `padding-bottom:72px`,避免内容被悬浮栏遮挡。
## contract-manage 新增页"合同文件"标题未加粗修复
- 文件:`src/views/business/contract-manage.vue`
- 问题:`attachment-section` 子组件内渲染的 `.dialog-section-title` 不受当前页面 scoped `> .dialog-section-title` 样式命中,导致"合同文件"标题字重未加粗(截图反馈)。
- 修复:在 `.contract-manage-form__section` 样式规则中补充 `:deep(.dialog-section-title)`,使直接子元素和子组件内部标题均统一为 16px/600/#303133。
## contract-manage 搜索栏"时间段快捷筛选"组位置前置
- 文件:`src/views/business/contract-manage.vue` + `src/option/business/contract-manage.js`
- 第一轮(理解偏差):把"签约类型"select 和 check-tag 组一起塞进 `#search-menu` slot。主人纠正:「签约类型」是普通搜索字段,应与第一行 8 个搜索项同排,不是 slot 内的辅助元素。
- 第二轮(终态):
- option 把 `signType` 加 `search:true / searchOrder:10 / searchPlaceholder:'请选择'`,作为 Avue 标准搜索字段,与其它 8 项同排(放在末尾)。
- 页面 `#search-menu` slot 移除「签约类型」select,仅留 5 个 el-check-tag。
- 页面 data 移除冗余的 `contractSignType:''`,onLoad/searchReset 不再手动维护 signType(Avue search form 自动绑定到 query)。
- 样式:`.el-select { width: 180px }` 已成 dead code,移除。
- `:deep(...)` 调整:去掉 `margin-right:auto`,只留 `order:-1`。`.avue-form__menu` 本身 `flex / flex-end`,check-tag 组排首项后,整体仍紧靠右侧,与 `[查询][重置][收起]` 形成 `[快捷筛选] [查询] [重置] [收起]` 右对齐序列。
## 登录验证码:滑块(behavior) → 图形字符验证码(image)
- 文件:`src/config/website.js`(第14行 `captchaType: 'behavior'` → `'image'`)、`src/page/login/userlogin.vue`、`src/styles/login.scss`
- 背景:登录页原 `captchaType:'behavior'`(滑块/旋转行为验证)。需求改成截图中的「输入框 + 右侧独立彩色字符验证码图片」。
- 改动:
1. `website.js` 改 `captchaType: 'image'` 启用图形验证码分支(代码原本就有 image 模式的 UI 与 `getCaptcha()` 逻辑,仅未启用)。
2. `userlogin.vue` image 分支模板:验证码图片从 `el-input` 的 `#append` 内联改为 form-item 内 `.login-code-wrap` 并排独立方块(输入框 flex:1 + 右侧 130px 图片块),点击刷新。
3. `login.scss` 重写 `.login-code/.login-code-wrap/.login-code-box/.login-code-img`:flex 并排、图片块带 1px 边框 + hover 主色高亮、`object-fit:fill` 撑满。
- 验证:`vite build --outDir /tmp/tms-verify-build` 全量编译通过(exit 0,无报错)。注意原 `pnpm build` 因 WorkBuddy safe-delete 钩子拦截清空 dist/(741 文件)而中断,非代码问题。
- 注意:登录页 `activeName` 默认 `'iam'`(IAM 统一登录),图形验证码在 `userlogin.vue`,需点「账号密码登录」才可见;验证码图片依赖后端 `/blade-auth/oauth/captcha` 返回的 `data.key` + `data.image`。
## 全站「所属组织」搜索统一为树形下拉(el-tree-select)
- 背景:合同管理页「所属组织」原为 cascader(radio 风格),客户档案页(vehicle/customer-archive)已用树形下拉。主人要求全站统一为树形。
- 统一方案:新建可复用 mixin `src/mixins/organization-search.js`(封装 organizationTreeOptions 树数据、loadOrganizationOptions()、renderOrganizationSearch(scope) 渲染函数,自动识别 organizationName / deptName 两种 prop 并挂 column.renderSearch;树节点 node-key='id')。
- 适用两种架构:
1. **avue-crud 搜索**(renderSearch 模式):option 列加 `searchslot:true`,视图混入 mixin + created/初始化调 loadOrganizationOptions。已改:contract-manage(option 去 cascader 改 searchslot,视图用 renderOrganizationSearch 渲染函数)、transportCapacity 的 driver/vehicle/ship 三页(option 去 type:select 改 searchslot,视图混 mixin)。
2. **自定义搜索表单**(query.deptName 文本框模式):替换 `<el-input v-model="query.deptName">` 为 `<el-tree-select>`,node-key='id',@update:model-value 用 findOrgById(val)?.rawLabel 把 id 转回部门名写入 query.deptName(保后端按名称过滤兼容)。已改 payment 模块 4 处:invoice-application / invoice-receipt / payment-application / components/bill-payment-search。
- 关键坑:mixin 内部树数据命名为 `organizationTreeOptions`(非 organizationOptions),与 transportCapacity 等页面表单自用的扁平 `organizationOptions` 隔离,避免相互覆盖破坏表单 select。
- 待办(同自定义表单模式,未改):settlement 模块 4 个视图(transport-reconciliation/formal-settlement/pre-settlement/settlement-adjustment,搜索用动态 v-for 渲染 searchFields,需给 deptName 加 `type:'tree'` 分支)、system/user.vue(deptName)。business 模块(waybill-manage/transport-plan 等)所属组织为 `hide:true` 不展示搜索,无需改。
## project-apply 业务部门栏占满宽度修复
- 文件:`src/views/business/project-apply.vue`
- 问题:「重大项目补录」(独立表单页 /business/project-apply/form?mode=majorSupplement)下"业务部门"项横跨整行,与上下两行半宽风格不一致。
- 根因:`<el-form-item label="业务部门">` 误加 `class="project-apply-form__item--full"`(该类对应样式 `:deep(.project-apply-form__item--full) { grid-column: 1 / -1; ... }` 是横跨整行的:本意只用于"项目由来说明"等多行文本字段)。
- 修复:移除该 class,业务部门恢复为半宽(一行两列网格的常规项),仅"项目由来说明"等需要全宽铺开的字段仍保留 full。同时把样式块上方注释由「占满整行的表单项(业务部门 / 项目由来说明)」改为「占满整行的表单项(如项目由来说明等多行文本字段)」,避免再误读。
## project-apply 字段 label 问号悬浮提示
- 文件:`src/views/business/project-apply.vue`
- 需求:把"项目资金使用额度"/"项目应收账款额度"两字段的提示从 placeholder 移到 label 右侧问号 tooltip。
- 实现:
- 两 `el-form-item` 加 `class="project-apply-form__tip-label"`,用 `#label` slot 包裹文字 + `<el-tooltip content=... placement="top"><el-icon class="project-apply-form__label-tip"><QuestionFilled /></el-icon></el-tooltip>`。
- placeholder 改回"请输入"。
- 提示文案按截图(去掉"额度"二字):资金使用额度→`实际业务回款周期内付款`;应收账款额度→`回款期内最大应收账款额`。
- scoped 样式:`__tip-label :deep(.el-form-item__label){vertical-align:top}` 保持默认 inline 布局,让文字按全局 `white-space: normal` 换行显示;`__label-tip{margin-left:4px;color:#a8abb2;cursor:help;vertical-align:-0.125em}` 问号与文字 baseline 对齐。
- 图标 `QuestionFilled` 已在 main.js 全局注册(@element-plus/icons-vue),模板可直接用。
- 可复用模式:其它表单字段如需 label 旁问号提示,套用此 slot 结构即可。
## project-apply 详情页客户/承运商表格改造
- 文件:`src/views/business/project-apply.vue`
- 需求:
1. 客户信息、承运商信息两个表格的「客户」「承运商」列改为可点击链接,跳转打开对应客商档案。
2. 删除两个表格的「联系人」「联系方式」列。
- 实现:
- 模板:两列的 `el-table-column` 用 `#default` slot 渲染 `<el-link type="primary" :underline="false" @click="openCustomerArchive(row)">`,显示 `row.customer` / `row.carrier`。
- 删除原 `prop="contact"` 和 `prop="phone"` 两列。
- methods 新增 `openCustomerArchive(row)`:取 `getCustomerRowId(row)`(`row.id || row.customerId || row.carrierId`),无 ID 时 `this.$message.warning`;有 ID 则 `this.$router.push({ path: '/vehicle/customer-archive/form', query: { id: String(id), name: '查看客商档案', view: '1' } })`。
- 注意:客商档案查看态由 `customer-archive.vue` 的 `this.$route.query.view === '1'` 识别,与现有查看逻辑一致。
+8
View File
@@ -0,0 +1,8 @@
# 2026-08-30
## 项目立项(/business/project-apply)操作栏隐藏「作废」按钮
- 需求:列表操作列不再展示「作废」。
- 实现:删除 `src/option/business/project-apply.js` 中 `config.operations` 的 `voidProject` 条目(原 status: ['approved','change_approved'],permission: project_apply_void)。
- 操作列按钮由 `customOperations(row)` 从 `config.operations` 动态渲染,故仅改 option 配置即可,无需动 `project-apply.vue`。
- 保留 `src/api/business/project-apply.js` 的 `voidProject` 接口方法(未被引用),如需彻底下线可一并删除。
- 遗留:剩余操作项为 提交 / 撤回 / 通过 / 审批驳回 / 变更;`menuWidth` 仍为 200,无需调整。
+12
View File
@@ -0,0 +1,12 @@
# 2026-08-31
## 客商档案「所属组织」改为单选级联(customer-archive.vue)
- 表单区 `el-tree-select`(多选勾选树)→ `el-cascader` 单选级联;`deptCascaderProps` 开 `checkStrictly: true`,可任意层级选中(不必选到叶子)。
- 引入 computed `deptCascaderValue`(get/set)做「部门 ID ↔ 根到节点路径」双向转换:
- `archiveForm.deptId` 仍为选中部门 ID 数组(单选只一个),后端字段 `deptIds` 逗号串 / `deptName` 不变,保存逻辑 `normalizeArchive` 无需改。
- getter 依赖 `deptTree`,部门树异步加载完成后详情回显自动刷新(解决 created 里 initDeptTree 与 openArchive 并发的时序问题)。
- `onDeptChange(path)` 改为取路径末级 → 写 `deptId / deptIds / deptName`;新增 `findDeptPath(id)`、`findDeptLabel(id)` 递归查找。
- `normalizeDeptIds(detail)` 单选只取第一个 ID(历史多选数据自动收敛)。
- 列表搜索区 `renderDeptSearch` 同步改为 `h(ElCascader, ...)`(import 从 ElTreeSelect 换 ElCascader),`searchChange` 取路径末级 ID 转部门名称。
</content>
</invoke>
+187
View File
@@ -0,0 +1,187 @@
# 2026-09-02 工作日志
## 合同/运力页「到期快捷筛选」标签统一改为独立一行
- 需求:把 avue-crud 搜索栏 `#search-menu` 里的 `<el-check-tag>` 到期快捷筛选组,移出搜索栏、单独成一行(搜索卡下方、表格上方),不加卡片包裹。
- 实现:改用 avue-crud 的 `#header` 插槽承载该组;CSS 由 `order:-1; margin-right:12px; min-width:0`(搜索按钮行内联)改为 `margin-bottom:12px`(独立行)。
- 涉及文件(4 处,均已改):
- `src/views/business/contract-manage.vue`(首改,用 `.contract-manage-page__expiry-search`)
- `src/views/transportCapacity/vehicle.vue`(`.transport-vehicle-page__expiry-tags`)
- `src/views/transportCapacity/driver.vue`(`.driver-page__expiry-tags`)
- `src/views/transportCapacity/ship.vue`(`.transport-ship-page__expiry-tags`)
- 各页变量名保留原样(contract-manage 用 contractExpiryScope/changeExpiryScope/contractExpiryStats;运力页用 query.expireStatus/handleExpireChange/expiryStat)。
- 已补改并修正:`payment/bill-ledger` 页。用户要求"放在卡片下面"(搜索白卡之外、表格之上独立一行),非卡片内分行。最终方案:把到期快捷筛选整组(标签 DOM + `shortcutOptions` 计算属性 + `handleShortcutChange`)从子组件 `bill-ledger-search.vue` 提升到页面 `bill-ledger.vue`,置于 `<bill-ledger-search>` 与 `<bill-ledger-table>` 之间(卡片下、表格上)。
- `bill-ledger-search.vue`:删除标签 DOM、`.bill-ledger-search__actions`/`.bill-ledger-search__actions-tags` CSS、`counts` prop、`shortcutOptions`/`handleShortcutChange`;按钮行 `.bill-ledger-search__actions-buttons` 恢复单行右对齐(`margin-bottom:8px`)。
- `bill-ledger.vue`:模板插入 `<div class="bill-ledger-page__expiry-tags">`(绑定 `query.expiryShortcut`,@change 调 `handleShortcutChange`→`handleSearch`);脚本加 `shortcutOptions` computed(用 `counts`)+ `handleShortcutChange` method;新增 `<style scoped>` 写入 `.bill-ledger-page__expiry-tags`(flex 行、`margin:8px 0`)。`emptyQuery()` 已含 `expiryShortcut:'all'`,无回归。
- 选中态样式统一:去掉 `.bill-ledger-page__expiry-tags :deep(.el-check-tag.is-checked)` 的 `background-color:#409eff` + `color:#fff` 蓝底白字覆盖,改用 Element Plus 默认浅蓝底深蓝字,与合同页一致。
## transportCapacity/ship 新增弹窗 input 宽度统一
- 问题:新增/修改弹窗内输入控件宽度不一致(文本框明显窄于下拉框、日期框)。
- 根因:`src/views/transportCapacity/ship.vue` 末尾 scoped 样式中独有 `.el-input__wrapper{width:150px !important}`,把文本框包裹层强制压成 150px,而 `el-select`(用 `.el-select__wrapper`)、日期/数字框不受影响 → 文本框与下拉框宽度不齐。同模块的 driver.vue / vehicle.vue 均无此规则。
- 修复:
1. 删除 `.el-input__wrapper{width:150px!important}` 异常规则。
2. 增强 `.ship-form` 内输入控件的 100% 宽度覆盖,补齐 `.el-select__wrapper` / `.el-cascader` / `.el-input__wrapper`,使文本框/下拉/日期/数字框均 100% 填满所在栅格列。
- 说明:弹窗仍保留不同栅格列宽(span6/8/12/16)的密集布局,同行字段已等宽;若需跨行全部 input 绝对像素等宽(统一栅格列宽),需进一步重排。
- 二次排查(用户反馈仍不一致):把 `.ship-form` 内控件 100% 覆盖从通用 `:deep(.el-input)` 改为更具体的 `:deep(.el-form-item__content > .el-input)` 并加 `!important`,同步覆盖 `.el-input__wrapper`/`.el-select__wrapper`,确保不会被 Element Plus 变量或更具体选择器压回默认/较窄宽度。请用户刷新浏览器验证。
- 三次修复(用户反馈第一行 span=6+6+12 三个字段宽度仍不齐):放弃「width: 100% 撑满 col」方案,改用「对齐 vehicle/customer-archive 的 `.archive-form` 风格」——label 固定 `calc(6em + 24px)` + 控件统一 `250px + max-width: 100%`。根因:`el-cascader` 默认 `display: inline-block`,即使 width: 100% 也不易撑满 col-content(与 input 视觉差 ~150px vs ~280px);span=6+6+12 下两列 form-item__content 实际不等宽(label-width=100 被长 label 撑开)。新方案固定 250px 后三字段视觉一致,参照客商档案。
- 同时清理模板中冗余 `label-width="88px"`(会被新 CSS 强制覆盖失效)。
## transportCapacity/ship 证书信息分组:附件下放到对应分组末尾 + label 缩短
- 需求:1. 「船舶国籍证书」「内河船舶最低安全配员证书」分组里 label 「证书有效期自」偏长,改短为「有效期自」(与下面统一)。2. 底部集中的 `.cert-upload-grid` 上传区调整到各证书分组末尾,让附件与字段同组。
- 修改:
- `src/views/transportCapacity/ship.vue` 模板 line 392/431:2 处 `label="证书有效期自"` → `label="有效期自"`。
- 模板结构重组:删除底部 `<el-row class="cert-upload-grid">...</el-row>`(原 line 472-548),把 6 个 `<image-upload-field>` 分别插入对应 `cert-block` 末尾(每个用 `class="ship-upload-item"`,独立 `<el-row :gutter="18"><el-col :span="8">`):
- 船舶所有权证书分组 → `ownershipCertImage`
- 内河船舶安全与环保证书分组 → `safetyCertImage`
- 船舶国籍证书分组 → `nationalityCertImage`
- 内河船舶最低安全配员证书分组 → `safeManningCertImage`
- 光船租赁登记证明书分组 → `leaseContractImage`(label="光船租赁登记证书")
- 船舶营业运输证书分组 → `businessTransportCertImage`(label="船舶营业运输证")
- 样式清理:删除无用的 `.cert-upload-grid { padding-top:8px }` / `.cert-upload-spacer { height:1px }`;保留 `.cert-block` / `.cert-block__title` / `.ship-upload-item`(`margin-bottom:14px`)。
- 验证:用户刷新浏览器看新布局。
## transportCapacity/ship 组合控件宽度对齐 250px 体系
- 问题:基础船舶信息里「安放龙骨日期/建造完工日期」的起止两个 date-picker、「航区」的 select 都是 `flex:1` 撑满所在 span=12 整列(约 498px),明显比单字段 250px 宽,视觉不协调。
- 修复(`src/views/transportCapacity/ship.vue` 末尾 scoped 样式):
- `.date-range-inline :deep(.el-date-editor)` 由 `flex:1; min-width:0` 增加 `max-width: 250px` → 两个 date-picker 在窗够宽时各对齐 250px、彼此等宽,窄窗时 flex:1 收缩不溢出。
- `.navigation-area-field :deep(.el-select)` 由 `flex:1; min-width:0` 增加 `max-width: 250px` → 航区 select 对齐 250px 单字段。
- 说明:`flex:1` 保底防溢出,`max-width:250px` 把组合控件拉回与单字段一致的 250px 体系(受所在列宽限制,安放龙骨 span=12 在常规窗宽下两个 date-picker 约 226px 等宽,更宽窗自动到 250px)。
## business/project-apply.vue 表单「业务部门/承办部门」级联选择器宽度统一
- 问题:项目新增/编辑表单(含独立表单页 `/business/project-apply/form`)中,业务部门、承办部门两个 `el-cascader` 的按钮宽度与同行的 select/input 不一致。
- 根因:scoped 样式里强制 100% 宽度的控件列表(`el-input` / `el-select` / `el-tree-select` / `el-date-editor`)漏了 `el-cascader`,导致级联选择器按内容宽度收缩,未填满栅格列。
- 修复:`src/views/business/project-apply.vue` 末尾 scoped 样式 `.project-apply-form` 的 100% 宽度规则补上 `:deep(.el-cascader)`。Vite HMR 已自动生效,无需重启 dev。
## 登录页隐藏租户栏
- 需求:登录页 `000000` 租户输入框隐藏。
- 根因:`src/page/login/userlogin.vue` 第 10 行 `el-form-item` 由 `tenantMode` 控制显示,初始值 `this.website.tenantMode`,`getTenant()` 请求 `/api/system/tenant/info` 成功时会把 `tenantMode=false` 自动隐藏并回填 `loginForm.tenantId`。截图里仍显示租户栏,说明该接口未正常返回(域名解析/后端/跨域),沿用了初始 `tenantMode`。
- 修复:
1. 删除 `userlogin.vue` 模板第 10-21 行整段租户 `el-form-item`(`v-if="tenantMode" prop="tenantId"`),不留 `v-if` 兜底,直接移除 DOM。
2. 同步删除 `loginRules.tenantId` 校验规则。
- 保留:`loginForm.tenantId: '000000'` 默认值、`getTenant()` 方法。提交登录时仍带 `tenantId` 字段,后端行为不变;接口正常时仍会用返回的 tenantId 覆盖默认值。
- 后续若要重新启用:恢复 `el-form-item` 即可(或改 `data()` 中 `tenantMode: this.website.tenantMode` 走配置开关)。
## 客商档案评分详情表控件/label 宽度对齐
- 问题:评分详情表弹窗内输入框、选择框、日期框宽度不一致;label 要求固定 88px 且超长换行。
- 修复(`src/views/vehicle/customer-archive.vue`):
- 模板 `label-width="88"` 改为 `label-width="88px"`。
- `.score-detail-form` 样式:
- `el-form-item__label` 强制 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important`,保留 `white-space: normal !important; word-break: break-all; line-height: 1.4` 实现超长换行。
- `el-form-item__content > .el-input / .el-select / .el-date-editor` 由固定 `width: 150px` 改为 `width: 100%`,单字段撑满等宽;`.score-detail-form__range` 内双日期框保持 `flex: 1; min-width: 0` 均分。
- 参考:客商档案 `.archive-form` 的 label 换行与控件等宽处理思路。
## 评分详情表弹窗输入框/选择框宽度对齐 + label 88px 换行
- 问题:`src/views/vehicle/customer-archive.vue` 中「评分详情表」弹窗表单的输入框、选择框、日期框视觉宽度不一致;label 区域未严格固定 88px。
- 修复:
1. `el-form` 的 `label-width="88"` 改为 `label-width="88px"`。
2. `.score-detail-form` 中 `.el-form-item__label` 强制 `flex:0 0 88px; width:88px; min-width:88px` 并保留 `white-space: normal` 换行。
3. 将单字段控件(`.el-form-item__content > .el-input/.el-select/.el-date-editor`)宽度由固定 `150px` 改为 `100%`,使其在固定 label 后自动等宽填满 content 区域。
- 保留:「临时额度申请使用期限」组合控件继续用 `.score-detail-form__range` 双 date-picker flex 均分布局。
## customer-archive 评分详情表:label 固定 88px 换行 + 控件宽度统一
- 问题:评分详情表弹窗内输入框与选择框宽度不一致,且 label 区域未按 88px 固定换行。
- 修复(`src/views/vehicle/customer-archive.vue`):
1. `score-detail-form` 的 `label-width="88"` 改为 `label-width="88px"`。
2. `.score-detail-form :deep(.el-form-item__label)` 强制 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important;`,并保留 `white-space: normal !important; word-break: break-all; line-height: 1.4;` 实现长 label 自动换行。
3. 单字段控件(`.el-form-item__content > .el-input / .el-select / .el-date-editor`)统一 `width: 100%`,使输入框与选择框在相同栅格列内等宽。
- 说明:「临时额度申请使用期限」双日期组合仍由 `.score-detail-form__range` 控制 `flex:1` 均分,不受单字段 100% 影响。
## 评分详情表 label 宽度与控件宽度对齐
- 需求:评分详情弹窗内输入框与选择框宽度一致;label 宽度固定 88px,超长自动换行。
- 修改 `src/views/vehicle/customer-archive.vue`:
1. `<el-form>` 的 `label-width="88"` 改为 `label-width="88px"`。
2. `.score-detail-form` 的 label 样式强制 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important;`,并保留 `white-space: normal !important; word-break: break-all; line-height: 1.4;` 实现换行。
3. 单字段控件(`.el-form-item__content > .el-input/.el-select/.el-date-editor`)由固定 `width: 150px` 改为 `width: 100%`,使输入框、下拉框、日期框均撑满同一 content 区,视觉宽度一致。
- 说明:「临时额度申请使用期限」两个 date-picker 仍由 `.score-detail-form__range` 的 `flex: 1; min-width: 0;` 控制,不受本次单字段 100% 影响。
## 客商档案「评分详情表」弹窗表单控件宽度对齐
- 问题:`src/views/vehicle/customer-archive.vue` 的「评分详情表」弹窗中,评定日期的 date-picker、拟申请额度等 input、评分量化表 select 等控件宽度不一致;label 需固定 88px 且长 label 换行。
- 修复:
1. `<el-form>` `label-width="88"` 改为 `label-width="88px"`。
2. `.score-detail-form` 样式:`.el-form-item__label` 强制 `flex:0 0 88px` + `width:88px` + `white-space:normal`,确保长 label 换行且宽度恒定。
3. 控件由固定 `width:150px` 改为 `.el-form-item__content > .el-input/.el-select/.el-date-editor { width:100% }`,单字段 input/select/date-picker 在 span=6 下等宽撑满 content 区。
- 备注:「临时额度申请使用期限」组合区保持 `.score-detail-form__range` 的 `flex:1` 双 date-picker 布局,不受单字段 100% 影响。
## customer-archive 评分详情表控件宽度与 label 换行对齐
- 问题:评分详情弹窗内 input、select、date-picker 宽度不一致;长 label(如「拟申请总资金使用额度(万元)」)未按 88px 换行。
- 修改 `src/views/vehicle/customer-archive.vue`:
1. `<el-form label-width="88">` → `label-width="88px"`,与全站风格一致。
2. `.score-detail-form` 样式:
- `el-form-item__label` 强制 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important;`,并保留 `white-space: normal` + `word-break: break-all` + `line-height: 1.4` 实现超长 label 换行。
- 单字段控件(`.el-form-item__content > .el-input/.el-select/.el-date-editor`)由固定 `width: 150px` 改为 `width: 100%`,使其在 88px label 下等宽撑满 content 区。
- 说明:双日期范围字段(临时额度申请使用期限)仍由 `.score-detail-form__range` 用 `flex:1; min-width:0` 均分,不受单字段 100% 规则影响。
## 评分详情表弹窗 label/控件宽度对齐
- 文件:`src/views/vehicle/customer-archive.vue`。
- 问题:评分详情表弹窗内 input 与 select/date-picker 宽度不一致;label 宽度未严格固定 88px、长 label 未换行。
- 修复:
1. `el-form` 的 `label-width="88"` 改为 `label-width="88px"`。
2. `.score-detail-form` 样式里 label 强制 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important;`,并保留 `white-space: normal`、`word-break: break-all` 换行。
3. 单字段控件(`.el-form-item__content > .el-input/.el-select/.el-date-editor`)统一 `width: 100%`,取代原先未生效/不齐的 `width: 150px`。
- 备注:「临时额度申请使用期限」两个 date-picker 仍由 `.score-detail-form__range` 的 `flex:1` 均分,不受 100% 宽度影响。
## 客商档案「评分详情表」弹窗控件宽度对齐与 label 换行
- 问题:评分详情表弹窗中 input、select、date-picker 宽度不一致;长 label 未换行。
- 修复 `src/views/vehicle/customer-archive.vue`:
1. `el-form` 的 `label-width="88"` 改为 `label-width="88px"`。
2. `.score-detail-form` 样式:label 强制 `flex:0 0 88px`、`width:88px`、`white-space:normal`,超出换行。
3. 单字段控件(`.el-form-item__content > .el-input/.el-select/.el-date-editor`)统一 `width:100%`,撑满 content 区,与选择框等宽。
- 保留 `.score-detail-form__range` 内双 date-picker `flex:1; min-width:0` 的「起—止」布局。
## 评分详情表弹窗 label 与输入控件宽度对齐
- 文件:`src/views/vehicle/customer-archive.vue`
- 问题:评分详情表弹窗内输入框、下拉框、日期框宽度不一致;长 label 未换行。
- 修改:
1. `<el-form label-width="88">` 改为 `label-width="88px"`。
2. `.score-detail-form` label 样式强制 `width: 88px !important; flex: 0 0 88px !important; min-width: 88px !important;`,并保留 `white-space: normal` / `word-break: break-all` 换行。
3. 单字段控件(`.el-form-item__content > .el-input / .el-select / .el-date-editor`)统一 `width: 100%`,填满 label 剩余区域,保证 input/select/date-picker 视觉等宽。
- 说明:组合日期区间(`.score-detail-form__range`)保持两个 date-picker `flex:1` 均分,不受单字段 100% 影响。
## customer-archive 评分详情表 label/控件宽度对齐
- 问题:评分详情弹窗表单的输入框与选择框宽度不一致;label 宽度要求 88px 且超长换行。
- 修复:`src/views/vehicle/customer-archive.vue`
- `label-width="88"` → `label-width="88px"`。
- `.score-detail-form` 样式:`.el-form-item__label` 强制 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important;`,并保留 `white-space: normal !important` 等换行样式。
- 控件由固定 `width: 150px` 改为 `.el-form-item__content > .el-input/.el-select/.el-date-editor { width: 100%; }`,使输入框、下拉框、日期框在各自列内等宽撑满。
## 评分详情表弹窗:label 固定 88px 换行 + 输入/选择框宽度一致
- 问题:`src/views/vehicle/customer-archive.vue` 评分详情弹窗内输入框与选择框宽度不一致,label 长文本未换行。
- 修复:
1. `label-width="88"` → `label-width="88px"`。
2. `.score-detail-form` 样式:`.el-form-item__label` 强制 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important; white-space: normal !important;` 确保 88px 固定宽度并自动换行。
3. 单字段控件 `.el-form-item__content > .el-input / .el-select / .el-date-editor` 由固定 `150px` 改为 `width: 100%`,使输入框与选择框在各自栅格列内等宽撑满。
- 备注:「临时额度申请使用期限」双日期组合仍走 `.score-detail-form__range` 的 `flex:1; min-width:0`,不受单字段 100% 影响。
## 客商档案评分详情表控件宽度统一 + label 88px 换行
- 问题:`src/views/vehicle/customer-archive.vue` 评分详情弹窗内输入框与选择框宽度不一致;label 区域未严格固定 88px,长 label 换行效果不稳定。
- 修复:
1. 模板 `el-form` 的 `label-width="88"` 明确为 `label-width="88px"`。
2. scoped 样式 `.score-detail-form`:
- `.el-form-item__label` 强制 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important;`,并保留 `white-space: normal !important` / `word-break: break-all` 让超长 label 自动换行。
- 控件统一由固定 `width: 150px` 改为 `.el-form-item__content > .el-input/.el-select/.el-date-editor { width: 100%; }`,使输入框、下拉框、日期框在各自栅格列内等宽撑满。
- 说明:「临时额度申请使用期限」双日期组合区仍由 `.score-detail-form__range` 控制 flex:1 均分,不受单控件 100% 规则影响。
## customer-archive 评分详情表控件宽度统一 + label 换行
- 需求:评分详情弹窗内输入框与选择框宽度不一致;label 宽度需固定 88px 且超长换行。
- 修改(`src/views/vehicle/customer-archive.vue`):
1. 模板 `score-detail-form` 的 `label-width="88"` → `label-width="88px"`。
2. scoped 样式 `.score-detail-form`:`.el-form-item__label` 固定 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important;`,并保留 `white-space: normal !important; word-break: break-all; line-height: 1.4;` 实现换行。
3. 控件宽度由固定 `150px` 改为 `.el-form-item__content > .el-input/.el-select/.el-date-editor { width: 100%; }`,使输入框、下拉框、日期框在同一 content 区域内等宽撑满。
- 说明:双日期字段(临时额度期限)仍走 `.score-detail-form__range` 的 `flex:1` 布局,保持两个日期框均分并对齐。
## 评分详情表输入框/选择框宽度对齐 + label 固定 88px 换行
- 文件:`src/views/vehicle/customer-archive.vue`
- 问题:评分详情弹窗内输入框、选择框、日期框宽度不一致;长 label 需要固定 88px 并允许换行。
- 修改:
1. `label-width="88"` → `label-width="88px"`。
2. `.score-detail-form :deep(.el-form-item__label)` 强制 `flex:0 0 88px !important; width:88px !important; min-width:88px !important`,保留 `white-space:normal !important; word-break:break-all; line-height:1.4`。
3. 控件宽度由固定 150px 改为 `.el-form-item__content > .el-input/.el-select/.el-date-editor { width:100%; }`,使输入框、选择框、日期框在固定 label 后自动等宽填满 content 区。
- 保留 `.score-detail-form__range` 内双 date-editor `flex:1; min-width:0` 的收缩行为。
## vehicle/customer-archive 两处「详细地址」hover 溢出提示完整地址
- 需求:编辑客商档案页,详细地址字段(窄栏 + 长文本易截断)鼠标移上去显示完整地址;仅文本溢出才显示。
- 实现:两处 `el-input` 用 `<el-tooltip>` 包裹,`content` 绑定字段值,`placement="top"`,`:disabled="!xxxOverflow"`(溢出才显示),`style="display:block; width:100%"`(el-tooltip 根 span 默认 inline,必须 block 才能撑满栅格列)。
- 主表单(line 317,`archiveForm.registeredDetailAddress`):`ref="registeredDetailInput"` + `@input` 实时重算。
- 发票弹窗(line 1008,`invoiceForm.registeredDetailAddress`):`ref="invoiceDetailInput"`,readonly 地图回填,靠 `watch` + `openInvoice` 打开后 `nextTick` 重算(弹窗按需挂载,预填数据打开后 watch 已不触发)。
- 新增:`data` 加 `registeredDetailOverflow/invoiceDetailOverflow` 标记;`watch` 监听两字段值变化→`checkOverflow`;`methods.checkOverflow(refName,flag)` 读 `inst.$el.querySelector('input')` 的 `scrollWidth > clientWidth` 判定溢出;`openInvoice` 打开后双 nextTick 重算发票框溢出。
- 验证:`vite build --outDir /tmp/tms-check-build` 零报错(常规 build 仅因安全删除保护拦截清空 dist,与代码无关)。
+24
View File
@@ -0,0 +1,24 @@
## 合同管理独立表单页下拉框定位修复
- `src/views/business/contract-manage.vue` 全部 12 处 `el-select` 加 `:teleported="false"` + `:fit-input-width="true"`:下拉保持在输入框正下方、宽度与输入框一致(原 teleport 到 body 导致下拉漂移且按内容撑宽)。
- 若后续新增独立表单页出现同样问题,照加这两个属性即可。
- 追加:teleported=false 后 popper 因下方空间不足自动上翻,再给 12 处 el-select 加 `placement="bottom-start"` + `:popper-options="{ modifiers: [{ name: 'flip', enabled: false }] }"` 强制始终在下方展示。
## 调度弹窗司机输入框宽度不一致修复
- 现象:「编辑调度信息(精简录入)」弹窗中,司机(el-autocomplete)输入框比同行 250px 的其他输入框宽。
- 根因:`business-crud-page.vue` 约 13434 行的「表单控件统一 250px」规则漏了 `.el-autocomplete`(autocomplete 根节点是包装层,内部才是 .el-input)。
- 修复:在该规则选择器列表追加 `> .el-autocomplete` 和 `> .el-autocomplete > .el-input`,与运单独立表单页(13658 行 business-crud-page--form-page 先例)一致。
- 经验:凡「控件统一 250px」类规则,选择器必须包含 `.el-autocomplete` 包装层。
## 调度弹窗收发货地址行溢出修复
- 现象:「编辑调度信息」弹窗收发货信息行,地址输入框过宽,右侧「联系方式」列被挤出弹窗。
- 根因:`__dispatch-shipping-form __route-address-row` 网格地址列为 `minmax(280px, 1fr)`,各列最小宽度合计 1030px,弹窗不够宽时 grid 不换行直接溢出。
- 修复:地址列改 `minmax(0, 1fr)`(宽屏仍 1fr 拉伸,窄窗自动收窄,总最小宽降到 750px)。
- 备注:基础规则 `__route-address-row`(13750 行,运单独立表单页等在用,7 列含 40px 按钮列)仍是 minmax(280px,1fr),同样窄窗有溢出风险,暂未动(用户未反馈该页问题)。
## el-select 选中值 / 占位符颜色区分(全局)
- 需求:已选值 #606266,未选值保持 placeholder 灰 #909399。
- 根因:Element Plus 的选中值文本与占位符**共用同一节点** `<div class="el-select__selected-item el-select__placeholder [is-transparent]">`,仅靠 `.el-select__selected-item` 无法区分,导致未选态也被染成选中色。
- 区分标志:只有「未选值」或「已展开但无搜索输入」时才带 `.is-transparent`。
- 修复(`src/styles/element-ui.scss`):选中规则改为 `.el-select__wrapper:not(.is-disabled) .el-select__selected-item:not(.is-transparent)`;多选 tag 文字另加 `.el-select__tags-text` 规则(tag 自身 color 优先级更高,父级规则盖不到)。
- 经验:以后写任何「区分 el-select 有值/无值」的 CSS,一律用 `:not(.is-transparent)`。
+215
View File
@@ -0,0 +1,215 @@
## 付款比例设置提示改为 tooltip(contract-manage / contract-manage-change)
- 需求:将「付款比例设置」区块原有的红色文字提示改为标题右侧的 `?` 图标悬停提示。
- 改动:编辑弹窗、详情弹窗、合同变更页三处标题后添加 `el-tooltip` + `el-icon-question-filled`,content 为「非必填;配置付款比例时,合计必须等于100%。」;同时移除 contract-manage.vue 中原 `.contract-manage-form__tip` 红色提示文字及对应样式。
## 必填星号与 label 首行齐平(全局)
- 根因:`element-ui.scss` 中 `.el-form-item--label-right .el-form-item__label { align-items:center }` 使 inline-flex label 的星号 `::before`(独立 flex 子项)在多行折行时被垂直居中。
- 修复:label 改 `align-self:center`(整体仍随行垂直居中)+ `align-items:flex-start`(星号/文字首行齐平),覆盖 label-right/left/top 三种位置;旧 inline-block 注释已修正。
- 影响页面:project-apply 独立表单页等所有多行 label 场景(弹窗内 `white-space:nowrap` 不受影响)。
## 必填星号首行对齐:改用 align-items: normal(stretch 方案)
- 最终写法(`src/styles/element-ui.scss`):`.is-required .el-form-item__label { align-self:center; align-items:normal; }` + `::before { display:inline-block; height:auto; line-height:inherit; margin-right:4px }`。
- 原理:stretch 让星号盒子在「换行时」被拉伸到 label 全高、内容从顶部排布 → 贴首行;单行时盒子=一行高,视觉不变(比 flex-start 更贴合「只换行才生效」)。
- 前提:::before 高度必须 auto 且继承行高;label 不能是 固定 height + 等高 line-height,否则退化成居中,需 JS 判行数加 class。
## 合同变更页(contract-manage-change.vue)
- 「其它附件」上传按钮改为 `type="primary" plain icon="el-icon-upload"`,与「变更材料」区 vehicle-attachment-upload 内置按钮一致。
- 分区标题必填星号:`.dialog-section-title.is-required::after { content:'*'; color:#f56c6c }`(`::before` 已被蓝色竖条占用,星号走 `::after`)。
- 再次变更逻辑:`load()` 里 form 强制重置 `changeContent/changeReason/changeAttachmentsJson = ''`、`changeMaterials = []`(不带上一次内容);新增 `mergeChangeAttachments()` 把上一次的 `changeAttachmentsJson` 归集进 `attachments`(其它附件),按 url/名去重,数字 size 转格式化文本,提交时随 attachmentsJson 持久化。
## 合同变更记录对齐客商变更记录(contract-manage.vue)
- 编辑页与详情弹窗的两处「变更记录」表格新增「变更内容」列:`formatContractChangeContent(row)` 复用 `buildDetailChangeRecordRows` 生成「字段:变更前:x → 变更后:y」摘要,单行省略 + `el-tooltip` 全量悬浮(样式 `.contract-change-record-content-cell/-tooltip` 照搬客商 `change-record-content-*`)。
- 编辑页操作列由单个「流程」改为「详情 + 流程」(`openDetailChangeRecord` + `openFlow`,宽度 120→150);详情弹窗表格「查看详情」改「详情」与客商一致。
- 值格式化增强:`billingPlanJson`→方案名(默认)列表、`paymentRatioJson`→「第N笔 xx%」、`settlementRuleJson/preSettlementConfigJson/formalSettlementConfigJson`→可读摘要(自动生成开关/结算类型/周期类型/截单日/周期天数,含预结算+正式结算双段)。
- `buildDetailChangeRecordRows` 过滤条件补「前后都为空则跳过」(原来只比 JSON 字符串,'' 与 null 会漏)。
## 星号方案补丁(关键)
- EP 的 `.el-form-item__label` 写死 `height:32px; line-height:32px`(项目只覆盖了 line-height:14px),导致 stretch 方案文字顶到 32px 盒子顶部、单行不居中。
- 修复:`.is-required .el-form-item__label { height:auto; align-self:center; align-items:normal }`——height:auto 收缩到内容高 + align-self:center 行内居中 + normal(stretch) 仅换行时星号贴首行。
## 车辆认证审核弹窗重构(transportCapacity/vehicle.vue)
- 背景:用户反馈「样式不统一、内容不全」。根因是审核弹窗用 `<el-descriptions :column="3" border>`(大白表 + 居中 label),与同页编辑弹窗的 `<section-card>` 白卡分组风格不一致。
- 改造:弹窗主体改为 4 个 `section-card` 分组 —— 基础信息 / 尺寸重量 / 证件信息 / 证件图片;内部 `el-form label-position="right" label-width="auto"` + `el-row/el-col`(3 列 `:span="8"`,所属组织与备注占 `:span="24"`)。
- 内容补全:新增「证件图片」分组,6 张证件照用 `<image-upload-field readonly large class-prefix="vehicle">` 只读展示(可点击放大);证件图片尺寸 240×151 复用已有全局 `:deep(.vehicle-uploader--large)` 规则,无需新写。
- label 单位统一为中文全角括号:`外廓长度(毫米)` / `核定载质量(KG)`(原为半角 `外廓长度(mm)`)。
- 方法重构:`formatAuditValue(item)` → `auditValue(prop)`,签名更简洁;新增 longTermMap,对 drivingLicenseEndDate / roadTransportCertEndDate / annualReviewEndDate / compulsoryScrapDate 在勾选长期有效时返回「长期有效」,避免与未填写的「-」混淆(与列表页 formatEndDate 行为一致)。
- 删除已无引用的 `auditDetailItems` computed。
## 全站弹窗标题补 4px 主色竖条(全局 element-ui.scss)
- 发现:`.el-dialog__title` 全局只配了 `inline-flex + gap:8px`,**没有竖条**(竖条此前只在 `.dialog-section-title::before`),导致全站 el-dialog 标题都不带竖条,不符合 AGENTS.md 4.8。
- 修复:新增 `.el-dialog__title:not(:empty)::before { width:4px; height:16px; border-radius:2px; background:var(--el-color-primary) }`。
- 注意:必须加 `:not(:empty)`,否则无标题弹窗会出现孤立蓝条。
- customer-archive.vue 评分详情弹窗:将「临时额度申请使用期限」区间行(span=12 + 至 连接)拆分为两个 span=6 独立字段「临时申请额度有效期起 / 止」,字段名 tempCreditStartDate/End 不变,删除废弃样式 .score-detail-form__range。
- temporary-credit-limit 文案调整:option 增加 addTitle/editTitle/viewTitle=「新增/编辑/查看临时额度申请」(覆盖 crud-dialog-title mixin 按菜单名注入的「临时额度管理」);remark label 改「临时额度信息」(hide:true 无表格副作用,rules 同步);projectName 表单 label 用 `#projectName-label` 插槽改「项目额度信息」,列 label 保持「项目名称」不动表格列头/搜索。Avue 支持-{prop}-label / -header / -search 插槽分离表单/表头/搜索文案。
- temporary-credit-limit「申请有效期至」增加「必须晚于当前日期」校验:common.js 新增通用 `futureDateRule(label)`(trigger: ['change','blur'],空值/非法值放行,按 0 点比较,当天也算过期),temporary-credit-limit.js 的 validUntil rules 追加该规则。已验证 Element Plus 通用 picker 在选中日期时调用 formItem.validate('change'),故选择即刻提示;form.validate() 不带 trigger 时会跑全部规则,提交仍拦截。
- temporary-credit-limit 搜索占位文案统一:createCrudOption(withSearchPlaceholders([...])),搜索栏 placeholder 变为「请输入/请选择」不带字段名,与 waybill-manage / transport-plan / shipping-template 同一模式。
- temporary-credit-limit 弹窗表单 placeholder 统一为「请输入/请选择」不带字段名:option 各列加 placeholder(projectCode/undertakeDeptName/projectFundLimit/usedFundLimit/remainingFundLimit/remark=请输入,validUntil=请选择),视图 formslot 自定义控件(projectName select、applyLimit input)placeholder 同步改。
## 【项目管理】列表增加「项目资金使用预警」(project-apply)
- 需求:列表新增「资金使用风险」列,高风险红色、中风险黄色;列表头部增加「资金使用风险」快速筛选标签(高风险(数)/中风险(数)),点击筛选。规则 tooltip:中风险≥80%、高风险≥90%,使用率=已使用资金/项目资金使用额度×100%。
- 前端(已落地,src/views/business/project-apply.vue + src/option/business/project-apply.js + src/api/business/project-apply.js):
- option/project-apply.js 新增列 `fundUseRisk`(slot:true, hide 列表显示) + 导出 `fundUseRiskOptions`(high/medium/none) 与 `fundUseRiskThreshold`{high:90, medium:80}。
- view 新增 `#header` 插槽:`el-check-tag` 快速筛选(复用 contract-manage 到期标签交互),带数量(fundRiskStats[statKey])与 `?` tooltip 规则;新增 `#fundUseRisk` 插槽渲染彩色 `el-tag`(high→type=danger 红,medium→type=warning 黄,无→"-")。
- data 加 fundRiskScope/fundRiskStats{high,medium,none}/fundRiskTagOptions;onLoad 追加 `fundUseRisk` 筛选参数 + 成功后 refreshFundRiskStats;新增 changeFundRiskScope(再次点击取消)、refreshFundRiskStats;searchReset 清空风险筛选。
- api 新增 `getFundRiskStats` → GET /blade-transport/project-apply/fund-risk-stats。
- 后端契约(未改,待确认数据来源):ProjectApplyVO 补齐 `fundUseRisk`(high/medium/none)、`fundUseRiskName`、`fundUseRate`、`usedFundLimit`/`remainingFundLimit`;列表 selectProjectApplyPage 填充每行风险 + buildQuery 支持 `fundUseRisk` 过滤;新增 `/fund-risk-stats` 返回 {high,medium,none}。
- ⚠️ 关键阻塞:系统里项目与任何费用表(运输计划/运单/账单/客商)均无关联字段,`usedFundLimit`(已使用资金) 无真实取数来源;临时额度模块的 project-apply/list 返回里 usedFundLimit 实际为空。需业务方确认「已使用资金」口径后才能算风险等级,否则列表风险列全空、统计为 0。
## 合同文件「批量下载」按钮与标题同排(contract-manage.vue 内联 AttachmentSection)
- 需求:编辑页「合同文件」分组里「批量下载」按钮目前独立一行、靠右偏上,高度与标题区错位;要放到「合同文件」标题右侧同排、垂直居中。
- 修复:`const AttachmentSection = defineComponent(...)` 内联组件 render() —— 原结构是 `div.dialog-section-title`(标题) 与 `div.contract-manage-form__attachment-head`(批量下载按钮) 两个并列 div;改为把标题与按钮放进同一个 `contract-manage-form__attachment-head` flex 容器(标题左、按钮右)。
- 样式:`:deep(.contract-manage-form__attachment-head)` 由 `justify-content:flex-end` 改为 `space-between` + `align-items:center`,并 `.dialog-section-title { margin-bottom:0 }` 抵消标题自身 20px 下边距(2674 行 `:deep(.dialog-section-title)` 会命中子组件渲染的标题)。
- 注意:AttachmentSection 用 render 函数产出 DOM,不带当前组件 scoped data 属性,因此样式必须走 `:deep()`;scoped 的 `&__billing-head, &__attachment-head`(2723) 只作用于当前组件模板内(计费信息),对子组件 DOM 无效。
- 影响:4 处使用(编辑/详情 × 合同文件/其它附件)布局统一变为「标题 + 批量下载」同排。
- 主人二次反馈「下面的其它附件也一样」——确认 L463(编辑/其它附件 description) / L765(详情/其它附件 description+readonly) 与 L313(编辑/合同文件) / L671(详情/合同文件) 都走同一份 render+样式,无需新代码;指挥浏览器 Cmd+Shift+R 硬刷即可看到效果(render 函数组件的 :deep() 样式改 HMR 经常不传播)。
## 合同格式下拉改读业务字典 contractFormat(contract-manage.vue)
- 需求:新增/编辑合同管理页「合同格式」下拉由硬编码改为读取业务字典 `contractFormat`。
- 原实现:`option/business/contract-manage.js` 里 `contractFormat` 列 `dicData` 硬编码 `[{label:'电子合同',value:'电子合同'},{label:'纸质合同',value:'纸质合同'}]`,view 里 `contractFormatOptions` computed 直接 `columnOptions('contractFormat')` 读 column.dicData。
- 修复(4 处):
1. data 加 `contractFormatDictOptions: []`(与 settlementModeDictOptions 并列)。
2. computed `contractFormatOptions` 改为优先 `contractFormatDictOptions`,为空回退 `columnOptions('contractFormat')`(保留静态兜底)。
3. `loadSettlementDictionaries` 的 `Promise.all` 追加 `getBizDictionary({ code: 'contractFormat' })`,用 `records()` 映射 `{label:dictValue,value:dictKey}` 填充 `contractFormatDictOptions`;再**条件覆盖** column.dicData(仅当字典非空时),避免字典未配时清掉静态兜底。
4. `detailValue` 的字典映射分支加入 `prop === 'contractFormat'`,详情页「合同格式」也走 `displayStatus` 显示 label。
- 后端契约:`getBizDictionary` → `GET /blade-system/dict-biz/dictionary?code=contractFormat`,返回 `List<DictBiz>{code,dictKey,dictValue,remark}`(DictBizController#dictionary 按 code 精确匹配)。字典 code 按用户口述用驼峰 `contractFormat`(现有 settle_method 为下划线,若后台实际是 `contract_format` 需调整 code)。
- 参考模式:与 `settlementMode`(settle_method)完全一致。
## 合同类别 contractCategory 也读业务字典(contract-manage.vue,续上)
- 需求:模板里 label 实为「合同类型」(prop `contractCategory`),下拉由硬编码 `common.js#contractCategoryOptions`(客户合同/承运商合同/补充协议,value 中文)改为读业务字典 `contractCategory`。
- 修复(3 处,与 contractFormat 同模式):
1. data 加 `contractCategoryDictOptions: []`。
2. computed `contractCategoryOptions` 改为优先 `contractCategoryDictOptions`,空回退 `columnOptions('contractCategory')`(静态兜底)。
3. `loadSettlementDictionaries` 的 `Promise.all` 追加 `getBizDictionary({ code: 'contractCategory' })`,映射填充后**条件覆盖** `contractCategory` 列 dicData(仅非空时)。
- 列表页列显示/搜索下拉、详情页(detailValue 已有 `prop==='contractCategory'` 分支走 displayStatus)均因 column.dicData 覆盖而自动跟随业务字典。
- 注意:`config.defaultForm.contractCategory: '客户合同'`(option/contract-manage.js)为硬编码中文默认值,若业务字典 dictKey 非中文需同步调整。
## 车辆新增弹窗:所有组织移位 + 使用部门 + 长期有效清空(vehicle.vue + 后端)
- 需求(transportCapacity/vehicle):①所有组织移到「基础车辆信息」区末尾;②新增「使用部门」字段(后端 transport_vehicle 无该字段,需加);③车牌号三段(省/号码/颜色)包成整体输入框+减宽;④勾选「长期有效」清空对应日期且不可编辑。
- 用户确认:所有组织+使用部门放「基础车辆信息」区末尾;使用部门用部门下拉(复用 getDeptTree→organizationOptions,存部门名,仅部门名非级联);车牌号包成整体输入框+减宽;长期有效 4 组全部统一清空(强制报废/行驶证/道路运输证/年审)。
- 前端(vehicle.vue):
1. emptyForm 加 `useDepartment: ''`。
2. 删「基础车辆信息」首行的「所有组织」(原 span24),在区末新增一行:`使用部门`(span8, el-select+organizationOptions, 存 rawLabel) + `所有组织`(span8, el-cascader)。organizationOptions 由 initDeptTree 填充。
3. `.plate-group` 由 grid(112px 1fr 112px) 改为 flex 整体输入框:外层 border+radius+focus-within 变化;内部 el-select/el-input 去 box-shadow、无边框,第一/最后 select flex 0 0 30%、中间 input flex 1,子项间用 `::before` 竖线分隔。
4. 4 组 date-picker 加 `:disabled="xxxLongTerm === 1"`(行驶证/道路运输证原缺,补上),checkbox 加 `@change="handleLongTermChange(日期prop, longterm prop)"`;新增方法 handleLongTermChange(勾选时清空日期并 validateField)。
5. formRules:drivingLicenseEndDate / roadTransportCertEndDate 由 required 改为 validator(新增 validateDrivingLicenseEndDate / validateRoadTransportCertEndDate,longTerm===1 时跳过必填),annualReview 原 validator 已支持。
- 后端(tms-erp-api-ws,新增 use_department 列):
- `TransportVehicle.java` 加 `private String useDepartment`。
- `blade_transport_vehicle.sql` 建表加 `use_department varchar(50)`。
- `TransportVehicleExcel.java` 加 `使用部门` 列。
- `TransportVehicleMapper.xml` resultMap + BaseColumn + QueryCondition(useDepartment LIKE) 各加。
- `TransportVehicleServiceImpl.prepare()` trim useDepartment;validate() 加长度≤50 校验。
- 新增增量迁移 `doc/sql/transport/blade_transport_vehicle_use_department_patch_20260909.sql`(ALTER ADD use_department AFTER organization_name)。
- 注意:VO 继承 entity 无需改;Wrapper BeanUtil 同名透传。已上线库须执行 patch SQL(建表脚本是 DROP+CREATE,勿直接跑)。
- 后续补充(vehicle.vue「车辆资质图片」区输入框对齐):第一行原 span `6/8/10` 不等宽(行驶证档案编号 6 / 行驶证有效期 8 / 道路运输证号 10)导致输入框宽度不齐,统一改为 `8/8/8`,与下方各行 span=8 一致。仅改 2 处 span,无逻辑改动。
## 车辆车牌号:改为单一 input 输入框(vehicle.vue,3 段 → 1 段)
- 需求:基础车辆信息区「车牌号」原本是「省简称 select + 号码 input + 车牌颜色 select」三段拼装(plateProvince/plateNoBody/plateColor),改为**单个 el-input** 直接输入完整车牌号(如 京A12345)。
- 改动(vehicle.vue):
1. 模板:`plate-group` 三段 → `<el-input v-model="vehicleForm.plateNo" maxlength="8" show-word-limit placeholder="如:京A12345" @input="handlePlateNoInput" @blur="validatePlateFormat">`。删除 `.plate-group` 样式(含 focus-within、竖线分隔、child flex 占比等整段)。
2. emptyForm:删 `plateProvince`、`plateNoBody`,保留 `plateNo`、`plateColor`(后端字段继续存历史车牌颜色)。
3. data:删 `plateProvinceOptions`、`plateColorOptions` 暴露;常量 `plateProvinceOptions`(31 省简称数组)整段删除;`plateColorOptions` 常量删除。
4. validatePlateNo 重写:直接校验 `value`(完整车牌号),正则 `/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼港澳]{1}[A-Z][A-Z0-9挂学警港澳]{5,6}$/`,错误提示改为「请输入标准车牌号(如:京A12345)」。
5. syncPlateNo / splitPlateNo 重写:splitPlateNo 改为 noop(保留以兼容 OCR 后回调链路);syncPlateNo 改为 trim+upperCase 单一字段。
6. handlePlateBodyInput 改名为 handlePlateNoInput,新增 validatePlateFormat 失焦校验。
7. 车辆认证审核弹窗只读详情同步删「车牌颜色」展示(与录入表单一致)。
- 后端不动:`plate_no` / `plate_color` 字段保留(数据库兼容历史车牌颜色),Service 无强制校验。
- 受影响:OCR 自动识别(行驶证/机动车销售统一发票)后续调用链路无需改,因 syncPlateNo 仍对 plateNo 做归一化。
## 车辆新增/编辑弹窗:输入框宽度统一(vehicle.vue,前后两区)
- 需求(transportCapacity/vehicle):新增/编辑弹窗「基础车辆信息」+「车辆资质图片」两个 section 各输入框宽度要一致;截图红框重点为车牌号、外廓尺寸、所有组织/使用部门。
- 根因:基础区原本列宽混乱(车牌号 span10 / 车辆类型 span6 / 外廓尺寸 span6,其余 span6),行内 3 列与 2 列混排;图片区文本字段全 span6,与基础区不一致。
- 改动(vehicle.vue 仅模板 el-col span):
1. 「基础车辆信息」区所有字段统一为 **span=8(3 列布局)**:车牌号、车辆类型、外廓尺寸、核定载质量、准牵引总质量、业务关系、能源类型、强制报废日期、海关备案号、所有组织、使用部门。
2. 「车辆资质图片」区文本/日期字段(行驶证档案编号、行驶证有效期、道路运输证号、道路运输证有效期、道路运输年审有效期、机动车登记编号、机动车登记日期)统一为 **span=8**。
- 不改:只读「车辆认证审核」弹窗(L140 起的 span6 只读详情 + large 图片上传)与新增弹窗内「证件图片」上传控件(固定尺寸)保持 span6。外廓尺寸为复合控件(长/宽/高三 input),口径同 span8。
- ⚠️ 注意:操作中曾误删核定载质量 el-col 标签(453-460 行开始时少 el-col、尾部多一个 `</el-col>`),已修复为完整 `<el-col :span="8">...`。
- 后续纠偏(2026-09-09 傍晚):主人反馈「为什么不是按4列显示」——上一步把"基础车辆信息/车辆资质图片"两区误改成 3 列(span8)。同项目惯例 driver.vue/customer-archive.vue 全为 **4 列(span6,仅复合长字段 span12)**,故统一重排为 4 列:
- 基础车辆信息:`车牌号(6)+车辆类型(6)+外廓尺寸(12)` → `核定载质量/准牵引总质量/业务关系/能源类型(4×6)` → `强制报废日期/海关备案号/所有组织/使用部门(4×6)`
- 车辆资质图片:`行驶证有效期/道路运输证号/道路运输证有效期/道路运输年审有效期(4×6)` → `机动车登记编号/机动车登记日期/行驶证档案编号(3×6)`,图片上传控件仍 span6
- 该区 span8 已全部移除,编辑表单区仅剩外廓尺寸 span12(复合控件长/宽/高)+ 图片上传 span6。
- 宽度微调(2026-09-09 傍晚2):主人圈出「所有组织」字段宽度与同行不一致。根因:`.vehicle-form` 里的 `:deep(...){width:100%}` 选择器只列了 `el-select / el-input / el-input-number / el-date-editor`,**漏了 `el-cascader`**(所有组织用的就是级联选择器,默认不撑满整列)。已补 `:deep(.el-cascader)` 进该规则,全表单仅此一处用到 el-cascader,故所有组织与同行等宽。
## 司机弹窗:补 placeholder + 每行统一 3 列(transportCapacity/driver.vue)
- 需求:新增/编辑司机弹窗 ①输入框无提示文案;②基础身份信息 3 列,其它 4 列 / 6 列行不统一。
- 改动(仅 1819 行 driver.vue 的 126-540 行 `<driver-dialog>` 模板,script/style 不动):
1. **基础身份信息**:行结构不动,7 个 el-form-item 控件补 placeholder(司机姓名/身份证号/出生年月/性别/民族/学历/详细地址)。住址已有「请选择省市区」、驾驶车辆已有「请输入或选择车辆」,不动。
2. **驾驶证信息**:第一行 4 列(:span=6)拆为两行 — 行 1(准驾车型 / 档案编号 / 有效期 date-range,:span=8)+ 行 2(长期有效,:span=8)。起/止 placeholder 保留简短「起」「止」,符合主人 1.B 偏好(保留原 date-range 二联结构,不改 daterange)。
3. **从业资格证信息**:第一行 4 列拆为两行 — 行 1(从业资格认证类型 / 资格证号 / 有效期,:span=8)+ 行 2(长期有效,:span=8)。资格证号 placeholder 保留「默认身份证号」不动。
4. **司机联系信息**:一行 6 列拆为两行 — 行 1(司机类型 / 手机号 / 紧急联系人姓名,:span=8)+ 行 2(紧急联系人手机号 / 与联系人关系 / 所属组织,:span=8)。所属组织已有 placeholder,不动。
- 决策点(主人确认):1.B(保留原 date-range 二联 + 第三列留空后拆行的方案);2 保留;3.A(驾驶证主页/副页、封面页/内容页两组上传图仍 :span=12,与占整行宽的原貌一致)。
- 校验:node 脚本统计 4 个 section-card / 13 个 el-row / 31 个 el-col / 24 个 el-form-item,全平衡。改动只在 driver.vue 一文件,~25 处 Edit 调用。
## 新增/修改司机弹窗改为 4 列布局(driver.vue,16:xx)
- 主人要求 /transportCapacity/driver 新增司机弹窗改 4 列显示。原文件各分组已是 span=6(4 列栅格)但每行只放 3 个字段,第 4 列空置。
- 重排结果(仅删行断 + 移动字段顺序,字段/校验/逻辑零改动):
1. 基础身份信息:行1 司机姓名/身份证号/出生年月/性别;行2 民族/学历/住址/详细地址;行3 驾驶车辆/岗位;行4 三张证件照(不变)。
2. 驾驶证信息:行1 准驾车型/档案编号/有效期/长期有效;上传主页/副页 span 12→6。
3. 从业资格证信息:行1 类型/资格证号/有效期/长期有效;上传封面页/内容页 span 12→6。
4. 司机联系信息:行1 司机类型/手机号/紧急联系人姓名/紧急联系人手机号;行2 与联系人关系/所属组织。
- 注意:此前日志记录的「3 列 span=8」方案已被后续改动覆盖为 span=6,本次以当前文件实际状态为准。
## 客商档案:客商类型→客商分类 + 新增客商类型(customer-archive.vue)
- ⚠️ 补救(17:34):data 里的 `customerKindOptions`/`customerKindMap` 定义一度丢失(模板 v-for 空 → 下拉显示「无数据」,主人截图反馈),已补回 data(customerTypeOptions 之后)。其余 8 处改动经 grep 确认完好。
- 需求:原「客商类型」(prop `customerType`,字典 customer_type:客户/承运商,多选)改名「客商分类」;另新增「客商类型」字段,取值为 外部客商 / 内部组织。
- 约定:prop `customerType` 不动(其它页面 loading-manage / dept / business-crud-page 都以 `customerType:'承运商'` 作为后端查询参数),只改中文标签。
- 新增字段 prop = **`customerKind`**(前端自定义命名,后端尚无对应字段,需后端在客商表加列并透传),值 `external`(外部客商) / `internal`(内部组织),单选下拉,默认 `external`。
- 改动点(8 处,全在 `src/views/vehicle/customer-archive.vue`):
1. 表单:227 行 label 改「客商分类」;其后新增一个 `el-col :span="6"` 表单项「客商类型」prop `customerKind`(el-select + `customerKindOptions`)。
2. data 新增 `customerKindOptions` / `customerKindMap`(external/internal ↔ 中文)。
3. formRules:`customerType` 提示文案改「请选择客商分类」。
4. 列表列:「客户类型」label 改「客商分类」;新增「客商类型」列(prop customerKind,type select,dicData 内外部,minWidth 120,**未加 search**)。
5. `emptyArchive()` 加 `customerKind: 'external'`。
6. `normalizeDetail()` 加 `customerKind: detail.customerKind || 'external'`。
7. 变更记录 `getChangeFieldLabel`:`customerType:'客商分类'`,新增 `customerKind:'客商类型'`。
8. `formatChangeFieldValue`:别名映射加 `客商分类/客户类型 → customerType`、`客商类型 → customerKind`;valueMap 加 `customerKind:{external:'外部客商',internal:'内部组织'}`。
- 校验:用 @vue/compiler-sfc 编译模板,0 错误。
## 上传证件项 label 贴顶修复(全局 archive-form 垂直居中提升)
- 症状:driver.vue 司机弹窗「驾驶证主页/副页」「封面页/内容页」等证件上传项(el-form-item 内容高 151px)label 文字贴顶,普通 32px 控件行看不出来。
- 根因:`archive-form` class 被 driver / vehicle / ship / credit-score-quantification / loading-manage / waybill-import-dialog 等 8+ 页面复用,但「label 与控件垂直居中」规则 `.archive-form :deep(.el-form-item){align-items:center}` 锁在 customer-archive.vue 的 scoped 样式里,其它页面拿不到;flex 容器默认 stretch,label(32px) 在高控件行内贴顶。
- 修复:规则提升到 `src/styles/element-ui.scss` 全局(紧跟 is-required 星号配套规则之后):`.archive-form .el-form-item { align-items:center }`。全站 archive-form 表单一并生效,label 与控件垂直居中。SCSS 编译通过。
## 司机弹窗「驾驶证信息」:有效期必填、长期有效不必填(transportCapacity/driver.vue)
- 需求:驾驶证信息区「有效期」(起/止,prop `drivingLicenseEndDate`)要必填(显示红*),「长期有效」复选框(prop `drivingLicenseLongTerm`)不必填(去掉红*)。
- 改动(formRules 两处):
1. `drivingLicenseEndDate` 规则加 `required: true`(`{ required: true, validator: this.validateDrivingLicenseEndDate, trigger: 'change' }`)——仅用于渲染红*,实际校验仍走自定义 validator。
2. 删除 `drivingLicenseLongTerm` 的 `{ required: true, message: '请选择驾驶证是否长期有效' }` 规则块。
- 关键机制(Element Plus 2.14 + async-validator 4.2.5 已核实):
- 红* 由 `form-item` 的 `isRequired = normalizedRules.some(rule => rule.required)` 决定,只要规则对象带 `required` 就显示。
- async-validator 的 `rule.validate` 中,若存在 `rule.validator`,直接走自定义 validator 并 return,**不会**再执行 required 空值检查 → 勾「长期有效」时止日被清空/disabled 也不会误报必填。
- 校验逻辑保持原样:起始日期恒必填;`longTerm !== 1` 时止日必填。
- 未同步:「从业资格证信息」区仍是「有效期无红* + 长期有效有红*」的旧形态(待主人确认是否一并统一)。
## 司机弹窗 placeholder 统一为「请输入/请选择」(transportCapacity/driver.vue)
- 需求:新增/修改司机弹窗输入框 placeholder 去掉后面的字段名称,只保留「请输入」「请选择」。
- 改动:18 处 placeholder 简化(7 处 → `请输入`,10 处 → `请选择`,1 处动态 cascader `driverForm.addressRegion || '请选择省市区'` → `|| '请选择'`)。
- **保留 4 处**(主人明确确认):
1. 驾驶车辆 `请输入或选择车辆` —— 复合操作(既可输入又可点按钮选车),去掉会丢语义。
2. 日期范围 `起` / `止` —— 起止标记,非字段提示。
3. 资格证号 `默认身份证号` —— 默认值说明,非「请输入」格式。
- 未动:表单**校验错误提示 message 仍带字段名**(如「请选择准驾车型」),因报错需指明具体字段,与 placeholder 定位不同;主人未要求,暂保持。
- 做法:Python 脚本按完整 placeholder 字符串精确替换(18 规则全命中,未匹配即 exit 1 中断),比逐个 Edit 高效且不会误伤。
- 校验:`@vue/compiler-sfc` parse + compileTemplate + compileScript 三项全通过。注意脚本必须放在**项目根目录**下运行(放 /tmp 解析不到 node_modules 里的 @vue/compiler-sfc)。
- ⚠️ git diff 显示的 `drivingLicenseLongTerm` 校验规则变动是**本次之前工作区已有的未提交改动**(见上一条日志),非本次引入,勿混淆。
## 「自动生成结算单」开关加 ? 悬浮提示(3 处)
- 需求:开关「开启」后面加问号图标,hover 提示「开启时,系统根据配置规则归集运单,定时生成结算单」。
- 改动文件(同一文案,为保持全站一致):
1. `src/views/business/components/business-crud-page.vue`(1884 行附近,`.business-crud-page__settlement-switch`)
2. `src/views/business/contract-manage.vue`(357 行附近,`.contract-manage-form__settlement-switch`)
3. `src/views/business/contract-manage-change.vue`(55 行,`.settlement-switch`,原为单行长标签已拆行)
- 写法:`<el-radio :label="1">开启</el-radio>` 后插 `<el-tooltip content="..." placement="top"><el-icon class="settlement-switch-tip"><QuestionFilled /></el-icon></el-tooltip>`,再接「关闭」radio。
- 样式:`.settlement-switch-tip { margin:0 4px; color:#a8abb2; cursor:help; font-size:14px }`,各文件 scoped 样式内一份(contract-manage 写在 `&__settlement-switch` 内)。
- 注意:图标走 main.js 全局注册(@element-plus/icons-vue 全量注册),无需 import;写法与 `project-apply.vue` 的 `__label-tip` 一致。
- 这三个大文件本身已存在 prettier 格式问题(改动前 git HEAD 版本同样 fail),非本次引入。
## 车辆认证审核弹窗排版对齐新增弹窗(vehicle.vue)
- 诊断 `/project-apply/fund-risk-stats` No endpoint:前端已定义 getFundRiskStats(api/business/project-apply.js:13),后端未实现;方案已给(后端补 R<{high,medium}> / 前端 catch 降级),待主人选。
- vehicle.vue 认证审核弹窗重排对齐新增弹窗:取消独立「尺寸重量」卡片,外廓尺寸改长/宽/高组合(span12,dimension-group);基础信息 3 行×4 列(车牌号/车辆类型/外廓尺寸;载质量/牵引质量/业务关系/能源类型;强制报废日期/海关备案号/所有组织/使用部门);证件信息按新增「车辆资质图片」行分布(止+运输证号+止+年审 / 登记编号+登记日期+档案编号+行驶证起 / 运输证起 / 备注整行);label「所属组织」改「所有组织」对齐新增;dimension-group 样式复制进 .certification-audit 作用域。@vue/compiler-sfc 编译校验通过。
- vehicle.vue 行驶证有效期必填星号:原规则是纯 validator(Element Plus 不渲染星号),改为 data 内 baseRules + computed formRules,drivingLicenseEndDate 动态返回 required 规则(长期有效=1 时返回 [],星号与校验同步消失);删除 validateDrivingLicenseEndDate。经验:Element Plus 星号只在规则含 required:true 时渲染,且 el-form-item 的 :required 属性会被注入为 {required} 规则并产生英文默认提示,不宜用它控制星号。
- 备注:车辆保存的「车牌颜色不能为空」「行驶证有效期:必填」均为后端校验(前端无对应规则),需后端改。
- driver.vue 从业资格证有效期对齐驾驶证:单日期改 date-range 起/止(列宽 6+6+6+6 → 6+6+8+4),新增 qualificationStartDate 字段(emptyForm + handleSubmit normalize);qualificationEndDate 规则加 required:true(渲染星号,validator 豁免长期);删除 qualificationLongTerm 的 required 规则(长期有效不必填);勾长期后起止均 disabled(比驾驶证更严,驾驶证起仍可编辑)。⚠️ 后端需新增 qualification_start_date 字段否则起日期无法存取。
- driver.vue 补充:驾驶证「有效期起」加 :disabled(长期=1 时禁用);驾驶证与从业资格证两个 validator 统一在「长期有效=1」时直接 callback() 放行(起止都不校验、不提示)。禁用不清空旧值,提交时保留原日期(后端以 longTerm 为准)。
- contract-manage-change.vue「合同文件」批量下载按钮上移:标题与按钮合并进 .section-head(flex space-between 同排),与「其它附件/计费信息」分区一致;删除无引用的 .attachment-head 样式。
- contract-manage-change.vue「自动生成结算单」问号提示图标位置:原 icon 夹在开启/关闭 radio 之间,因 .el-radio 默认 margin-right:32px 导致离开启远、贴关闭;修复 = 开启 radio margin-right 清零 + icon margin 改 0 32px 0 8px。
+6
View File
@@ -0,0 +1,6 @@
# 2026-09-10
- 移除 project-apply 的 fund-risk-stats 请求(后端未实现端点,每次列表加载都弹 No endpoint 报错):删除 onLoad 内 refreshFundRiskStats 调用与该方法、data 中 fundRiskStats;头部风险标签去掉「(数量)」只留「高风险/中风险」;api/business/project-apply.js 保留 getFundRiskStats 定义并注释「后端就绪后恢复」。
EOF2
}
cat >> /Users/gxwebsoft/VUE/tms-erp-web-ws/.workbuddy/memory/2026-09-10.md <<'EOF'
+28
View File
@@ -0,0 +1,28 @@
# 2026-09-11
## 上传证件区域固定宽度导致跨机器错位(transportCapacity)
- 现象:driver.vue 新增弹窗的身份证/大头照上传框在本机对齐,他人电脑不对齐。
- 根因:`--large` 上传框写死 `width:240px`(ship 写 248px),而弹窗 `width:90%` + `el-col :span=6` 列宽是弹性的、上方输入框 `width:100%`;列宽随分辨率 / Windows 系统缩放 / 浏览器缩放变化,240px 只在特定宽度下恰好等于列宽。叠加 `labelWidth:auto` 靠 JS 实测字体(苹方 vs 微软雅黑)宽度,label 列宽也不同 → 起始位置漂移。
- 修复:三个文件改为 `width:100%; max-width:240px; flex:none`(ship 保留 248px 上限),窄屏跟随列宽、宽屏保持比例上限,左边缘恒与输入框对齐。
- `src/views/transportCapacity/driver.vue`(.driver-uploader--large)
- `src/views/transportCapacity/vehicle.vue`(.vehicle-uploader--large)
- `src/views/transportCapacity/ship.vue`(.ship-uploader--large)
- 用 @vue/compiler-sfc(脚本放项目根目录)验证三个 SFC 编译通过。
- 教训:本项目所有「固定 px 宽度的上传/控件」都要配 `max-width` + `width:100%`,否则高分屏/低分屏下必然错位。
## 合同详情由弹窗改为独立页面(contract-manage)
- 路由:新增 `/business/contract-manage/detail`(`src/router/views/index.js`,name『合同详情』,meta `keepAlive:false` + `activeMenu:'/business/contract-manage'`),组件仍复用 `contract-manage.vue`(与 form 页同一套路)。
- 页面内三分支:`v-if="!isFormPage && !isDetailPage"`(列表)/ `v-else-if="isFormPage"`(表单)/ `v-else`(详情)。**v-else 必须紧邻 form 分支的 `</template>`**,中间不能插其它组件,否则 Vue 编译报「v-else 无相邻 v-if」——弹窗类组件(billing-plan-editor / 附件预览 / 变更记录详情 / 流程)放在三个分支之后。
- 关键改动:`openDetail` 改 `router.push({ path:'/business/contract-manage/detail', query:{ id, name } })`;新增 `initDetailPage()`(按 query.id 拉详情,`resetDetailState()` 先清空)、`closeDetailPage()`(closeTag + 回列表)、`isDetailPage` / `detailPageTitle` computed;`created` 与 `$route.fullPath` watch 都要加 `else if (this.isDetailPage)`。
- 样式:`.contract-manage-detail--page { max-height:none; overflow:visible }`,去掉弹窗的 74vh 限制;底栏复用 `.contract-manage-page__footer`(fixed 浮动),只读详情页按钮保留「关闭」。
- 校验方式:dev server 已运行时 `curl "http://localhost:2889/src/views/business/contract-manage.vue"` 返回 200 即 Vite 编译通过(比整包 build 快)。
## 只给「查看弹窗」加宽 label(temporary-credit-limit)
- 场景:详情页长 label(「剩余项目资金使用额度(万元)」)换行超 2 行撑坏详情表格,但又不能影响新增/编辑页。
- **关键发现:Avue 的 `avue-form` 在 detail 模式下根节点会挂 `.avue--detail` 类**(源码 `node_modules/@smallwei/avue/lib/packages/element-plus/form/index3.js` 中 `class: normalizeClass([b(), { 'avue--detail': isDetail }])`)。所以「只针对详情态」的选择器就是:
```scss
.temporary-credit-limit-dialog .avue--detail .el-form-item__label { width:210px !important; flex:0 0 210px !important; }
```
新增/编辑弹窗无 `.avue--detail`,天然不受影响,不需要在 option 里做 mode 判断。
- 校验样式块编译:dev server 请求 `?vue&type=style&index=1&lang.scss`(**lang 必须写 scss,写 css 会 500**),grep 规则确认产物。
- 结论备忘:长 label 优先「加宽 + 换行 ≤2 行」,不要用单行省略 + tooltip —— 详情是纯阅读场景,hover 才能看全字段名的可发现性差。
+63
View File
@@ -0,0 +1,63 @@
# 2026-09-17 工作日志
## 配载管理(loading-manage):弹窗全部改为独立表单页/详情页
- 需求:`/business/loading-manage` 不再用弹窗,改成与新增合同(`/business/contract-manage/form?mode=add&name=新增合同管理`)一致的独立页面。
- 现状:新增/编辑**已经**是独立页(组件内 `isStandaloneFormPage` + `/business/loading-manage/form` 路由),但「详情」和「重新派单」仍是 el-dialog(用户截图即详情弹窗)。
- 改动(3 个文件):
1. `src/router/views/index.js`:新增 `/business/loading-manage/detail`(name 配载单详情,`meta.keepAlive:false, activeMenu:'/business/loading-manage'`),组件仍是 `loading-manage.vue`。
2. `src/views/business/loading-manage.vue`:
- computed 新增 `isStandaloneDetailPage`(path==='/business/loading-manage/detail')、`isStandalonePage`(form|detail 并集,作为隐藏列表容器的统一开关)、`detailPageTitle`、`formPageDefaultTitle`(add/edit/reassign 映射);`isStandaloneFormPage` 放行 `mode=reassign`。
- 模板中所有控制「是否独立页/是否弹窗」的 `isStandaloneFormPage` 改为 `isStandalonePage`;顶部标题按详情/表单取 `detailPageTitle`/`formPageTitle`。
- 新增 `gotoLoadingDetail(row)`(push `/detail?id=&name=配载单详情`,替换「配载单号」链接与操作列「详情」)、`gotoLoadingReassign(row)`(push `/form?mode=reassign&id=&name=重新派单`)、`initStandaloneDetailPage()`;`initStandaloneFormPage` 模式透传 edit/reassign;`openLoadingDialog` 对 add/edit/reassign 一律转独立页;`closeLoadingDialog` 判据改 `isStandalonePage`。
- `mounted` / `watch $route` 增加详情页分支(直接打开与页内切换都要 init);detail 分支除 id 变化外,`dialogMode !== 'view'` 也重新 init(防止从表单页切详情时残留表单态)。
- 兼容旧链接:`/business/loading-manage?detailId=x` 仍可用,会 replace 到详情页。
- 样式:底栏去掉写死的 `gap:8px`(全站统一 12px);页面模式的内容区解除 `max-height:78vh` 内嵌滚动盒 —— 原 `:global(.loading-manage-form-page .loading-manage-dialog__body)` 权重 (0,2,0) 低于 scoped 规则 (0,3,0) 一直没生效,改为 `.loading-manage-page .loading-manage-form-dialog.loading-manage-form-page .loading-manage-dialog__body` (0,4,0),内容改随 `#avue-view` 自然滚动(与合同页一致)。
3. `src/views/business/components/waybill-manage-page.vue`:`openLoadingDetail` 由 `/business/loading-manage?detailId=` 改为 push `/business/loading-manage/detail?id=&name=配载单详情`。
- 校验:dev server(2889)单独编译 4 个模块均 HTTP 200(含 scoped scss);`vite build` 仍会在无关文件 `src/page/login/facelogin.vue` 被沙箱敏感内容保护中断。
- 踩坑:**`<style scoped lang="scss">` 顶层写 `//` 注释**会让该样式模块编译 500(`Unexpected '/'. Escaping special characters with \ may help.`),已实测确认;顶层必须用 `/* */`。
- 未做:登录态无法在浏览器实测(无测试账号),需用户点 `/business/loading-manage` → 详情/重新派单 自测;`voucher-manage-detail` 跳配载时带的 `?loadingNo=` 参数本来就未被列表消费(历史遗留)。
## 运单管理详情:打卡时间轴改为按打卡顺序展示
- 文件:`src/views/business/components/waybill-manage-page.vue`(详情页「执行详情 → 打卡详情」tab)
- 需求:时间轴不再固定按流程节点顺序(发货/在途/到货/卸货/签收/回单),改为按打卡时间先后展示。
- 实现(不改后端、不改原始数据):
1. 新增 computed `orderedWaybillPunchRecords`:映射出 `punchedAt`(毫秒时间戳)后排序 —— **已打卡记录按打卡时间升序排在前**,未打卡记录保持后端返回的节点顺序排在其后;时间相同用原索引稳定排序。
2. 新增 method `waybillPunchRecordTimestamp(record)`:`punched === false` 或 `statusName === '未打卡'` 直接返回 null(防止后端返回计划时间造成误序);优先 `this.$dayjs(text).valueOf()`,失败兜底 `new Date(text.replace(/-/g,'/'))`(兼容 Safari)。
3. 模板 `el-timeline` 的 `v-for` 改用 `orderedWaybillPunchRecords`;`key` 由 `record.nodeCode || record.id || index` 改为 `${nodeCode||id||'punch'}-${index}`,避免同节点多次打卡(在途)时 key 重复。
- 校验:项目根目录临时脚本 `_verify-sfc.mjs`(@vue/compiler-sfc parse + compileScript + compileTemplate)通过,已删除脚本。
- 注意:`getPunchRecords`(`src/api/business/waybill-manage.js` → `/punch-records`)后端未提供节点排序字段,排序只能在前端做。
## 保险记录批量导入:前端归一化日期格式
- 文件:`src/views/vehicle/insurance-record.vue`(仅此一个文件)
- 背景:保险记录批量导入是**后端驱动**——前端 `handleImport` 经 `importBlob` 把原始 Excel 直接 POST 到 `/blade-transport/insurance-record/import-insurance-record`,前端不解析行。后端只认 `2026-09-01`,`2026-9-1` 导入失败。
- 决策:用户选择**前端归一化**(不动后端)。把 `openImportDialog` 改为自写 `httpRequest`:用 `xlsx` 读取 Excel → 把日期列(开始日期/结束日期/开票日期)`2026-9-1` 归一为 `2026-09-01` → 重写成新 File 再交给 `handleImportExcel` 上传;复用其失败明细下载与成功刷新逻辑。无日期列时回退为原文件(行为不变)。
- 关键方法:`normalizeExcelDates` / `readExcelRows` / `writeExcelFile` / `normalizeDateString`(正则 `^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$`,用 `new Date` 校验合法性后补零;非法/已标准格式返回 null 保持原值)。
- 校验:本机 dev(localhost:2889)待用户部署自测。注意所有单元格经 `sheet_to_json(raw:false)` 后均为字符串,与 transport-plan-import 的客户端解析模式一致。
## 车/船务模块导出 xlsx「创建时间/更新时间」为空(后端 tms-api 修复)
- 现象:`/vehicle/insurance-record` 批量导出的 xlsx 里「创建时间」「更新时间」两列空白,「更新人」有值。用户要求车/船务模块所有页面统一修。
- 定位:导出由**后端**生成(前端只传查询参数,`exportColumns` 参数后端未使用)。实测 `~/Downloads/保险记录2026-09-17 20_46_08.xlsx`:K/M 列单元格无 `<v>` 节点(值 null),E/F/J 日期列是数值 46266.0(带 numFmt `yyyy-MM-dd`,WPS 显示为日期)。
- 根因:Service 用 BladeX `org.springblade.core.tool.utils.BeanUtil`(**继承 Spring `BeanUtils`**)做 `copyProperties`。实体 `TenantEntity.createTime/updateTime` 是 `java.util.Date`,导出类字段是 `java.time.LocalDateTime` → Spring BeanUtils **类型不兼容静默跳过**,值为 null。而「更新人」是手动 `UserCache.getUserRealName(...)` 赋值的,所以有值。(Hutool 的 BeanUtil 能转 Date→LocalDateTime,但本项目用的是 BladeX 版本,不能。)
- 佐证:项目里正常的导出(Waybill / LoadingManage / CustomerArchive / CommonCargo)时间字段一律用 `java.util.Date`,并在 Service 里手动 `excel.setCreateTime(record.getCreateTime())`。
- 修复(后端仓库 `/Users/gxwebsoft/JAVA/tms-api`,blade-transport 模块):在 13 个导出方法里补显式赋值(Date→LocalDateTime 用 BladeX `DateUtil.fromDate`,`Func.isEmpty` 判空),不改 Excel 类字段类型(避免影响自定义 converter 与导入模板)。
```java
excel.setCreateTime(Func.isEmpty(x.getCreateTime()) ? null : DateUtil.fromDate(x.getCreateTime()));
excel.setUpdateTime(Func.isEmpty(x.getUpdateTime()) ? null : DateUtil.fromDate(x.getUpdateTime()));
```
- 涉及文件(13 个 Service + 1 新建 Excel 类 + 设备台账 controller/接口):InsuranceRecord / ViolationRecord / MaintenanceRecord / MaintenancePlan / TireReplacementRecord / AccidentRecord / AnnualInspectionRecord / MileageRecord / TransportChangeRecord / EtcRecord / OilElectricRecord / OtherExpenseRecord / EquipmentLedger。
- 设备台账特殊:原 `EquipmentLedgerExcel` 根本没有审计列(前端列表有),新建 `EquipmentLedgerExportExcel extends EquipmentLedgerExcel`(创建时间/更新人/更新时间),导出链路改用新类,**导入模板仍用旧类不受影响**。
- 未验证:本机无 mvn(仅 IDEA 内置),且离线模式缺 `blade-bom` 无法编译;用 javap 确认 `DateUtil.fromDate(Date)` 存在 + 逐行 grep 复核代替。需用户在 IDEA 里编译并重新部署后自测。
- 同类隐患(本次未改,其他模块):TemporaryCreditLimit、ProcessConfig、CommonRoute、CommonAddress、ContractManage、ProjectApply、ShippingTemplate 的导出 Excel 也是 LocalDateTime 且未手动 set 时间。
## 保险记录保额 -1 问题排查(未结案,待用户验证 DB 环境)
- 症状:新增弹窗保额不填 → 再开编辑弹窗显示 -1,触发「不能小于 0」校验。
- 已排除:前端(源码/dist 构建/git 全历史)、后端 tms-api + tms-erp-api-ws(源码/编译产物 javap/git 历史)均无任何 -1 赋值逻辑;保额为 formslot el-input,空值传 ''。
- 关键链路:编辑弹窗 beforeOpen 走 `/detail` 接口(getOne 原生查询),**不经过**列表 SQL 的 `CASE WHEN insured_amount<0 THEN 0` 兜底——所以表格显示 0、弹窗显示原始值。
- 数据库截图(用户 Navicat):该行 insured_amount=NULL,全表无负数 → 说明用户浏览的库与后端实际连接的库**不是同一个/DDL 不一致**。系统存在「空数值=-1」惯例(MaintenanceRecordMapper 里程 `CASE WHEN mileage=-1 THEN NULL`;前端 waybill-manage.js/process-config.js 均有 `Number(v)===-1?'':v` 掩码)→ 后端真实库的 insured_amount 列默认值很可能是 -1,MP 插入排除 null 列 → 默认值 -1 入库。
- 待验证:在后端 Nacos 数据源指向的库执行 `SHOW CREATE TABLE blade_insurance_record` + `SELECT insured_amount FROM blade_insurance_record WHERE policy_no='11111' AND vehicle_no='沪AE31312'`。
- 修复方案(待确认):1) ALTER 列默认值改 NULL + UPDATE 存量负数为 NULL;2) 前端 beforeOpen 对 insuredAmount/premium 做 null/-1 → '' 归一(参照 waybill-manage normalizeNumericDisplayValue);3) 列表 SQL 的 CASE 兜底与 detail 不一致,可顺手统一。
+122
View File
@@ -0,0 +1,122 @@
# 2026-09-18
## 修复:改成独立整页后点左侧菜单仍弹出旧弹窗(loading-manage / project-apply)
**现象**:配载单管理、项目管理已改为「新增/编辑/详情走独立整页」,但从独立页返回或直接点左侧菜单时,页面上仍会盖出一个老的 `el-dialog`(新增项目管理 / 配载单)。
**根因**(已读 Vue runtime-core 源码确认):
- `src/router/tab.js` 按 `tabKey`(默认 fullPath)为每个标签建一个具名 wrapper 组件放进 `wrapperMap`,`src/page/index/layout.vue` 用 `<keep-alive :include="$store.getters.tagsKeep">` 缓存 —— 每个标签页一个实例,全部缓存不销毁。
- 组件被 deactivate 后**仍会随 `$route`(全局响应式)重新渲染**;此时 `isStandalonePage` 变 false,`<component :is="isStandalonePage ? 'div' : 'el-dialog'">` 由 div 翻回 `el-dialog`,而 `v-model`(`projectBox`/`dialogVisible`)仍是 true,`append-to-body` 使弹窗 Teleport 到 body。
- `KeepAlive.deactivate` 调 `move(vnode, storageContainer, ..., moveType=OUT)`,但 `TeleportImpl.move`(`moveTeleport`)忽略传入的 moveType、一律按 REORDER 处理 → **teleport 出去的 DOM 不会被搬回 storage container**,永久残留在 `document.body` 上并显示出来。
**修法**(不回退 tab.js 的 `return wrapper` 提交 c1c4fa1,改修根因):
- 独立页容器固定为 `<div v-if="isXxxPage">`,不再按路由回退成 `el-dialog`。
- 页内其它 `append-to-body` 二级弹窗加 `deactivated()` / `beforeUnmount()` → `closeInnerDialogs()` 把 v-model 置 false。
**改动文件**:
- `src/views/business/loading-manage.vue`:容器改 `<div v-if="isStandalonePage">`;删 `dialogVisible`、`dialogTitle`、`resetLoadingDialog`、`clearLoadingDialog`、页脚弹窗分支(含「清空」);`watch.$route` 简化;新增 `closeInnerDialogs()`(routeChange / commonAddress / candidateSearch)+ `deactivated`/`beforeUnmount`。
- `src/views/business/project-apply.vue`:容器改 `<div v-if="isProjectFormPage">`;删 `projectFormContainer`/`projectFormContainerProps`、`projectBox`、`resetProjectDialog`、`handleCancelProject`;`closeProjectForm` 简化为直接 push 列表;新增 `closeInnerDialogs()`(changeRecordDetail / 附件文档预览 / 图片预览 / 选人)+ `deactivated`/`beforeUnmount`。
**校验**:dev server(2889) curl 两个 .vue 及其 scoped style 模块,均 HTTP 200(500 才是编译失败)。`vite build` 因沙箱敏感内容保护会在 facelogin.vue 中断,未用。
**遗留同类风险(未改,已告知用户)**:`business/components/waybill-manage-page.vue`(`detailContainer` 在 PageDetail / el-dialog 间切)、`settlement/components/{pre,formal}-settlement-editor.vue`、`transport-reconciliation-editor.vue`(`pageMode ? 'div':'el-dialog'`)、`business/components/waybill-import-dialog.vue`(`standalone`/`createPage`)。统一修法:容器类型在 `created()` 锁定,不随路由/prop 翻转。
---
## 追加修复:运单详情弹窗残留(waybill-manage-page.vue)
用户复现路径:打开 `/business/waybill-manage/detail?id=xxx` → 点「配载管理」菜单 → 页面上又冒出运单详情弹窗。与上面同一根因:`detailContainer()` 随 `isStandaloneWaybillDetailPage` 在 PageDetail / el-dialog 间翻转,`:append-to-body="!isStandaloneWaybillDetailPage"` 随之为 true,`v-model="detailBox"` 仍 true → Teleport 进 body 搬不回来。
**改动**(`src/views/business/components/waybill-manage-page.vue`):
- data 新增三个锁定标志 `formPageLocked` / `detailPageLocked` / `formModeLocked`,在 `created()` 一次性从 `isStandaloneWaybillFormPage` / `isStandaloneWaybillDetailPage` / `$route.query.mode` 取值。
- 模板里所有形态判定(根 class、`v-if="!detailPageLocked"` 的列表容器、`form-page-title`、`status`、独立表单页按钮块、分页 `v-show`、`:append-to-body`、detail-page class、内容 `v-if="detailBox || detailPageLocked"`、独立页 footer / 弹窗 footer slot)全部改读锁定标志。
- `detailContainer()` / `crudContainer()` / `pageFormOption()`(含 `boxType`)改读锁定标志,避免缓存实例的 option 变形。
- 新增 `closeInnerDialogs()`(关 detailBox、里程补录、附件文档/图片预览、变更路线、常用地址、流程图、运输路线/地址/站点/地图、常用货物、货物导入、过程配置、Excel 导入)+ `deactivated()` / `beforeUnmount()`。
**关键验证**:`onDeactivated` 在子组件里也会触发 —— apiLifecycle 的 `injectHook` 会向上遍历父链,发现 KeepAlive 父级就把钩子注入到根实例(runtime-core 的 `injectToKeepAliveRoot`),所以写在子组件 waybill-manage-page 上的 `deactivated` 有效。
**校验**:dev server curl 该 .vue 与 scoped style 均 200。仍未 commit。
**未提交**:上述改动 + 上一轮 loading-manage 独立页改造均未 commit。
---
## 排查(未改码):配载单详情页反复弹「运单管理不存在」
用户截图:打开 `/business/loading-manage/detail?id=2097607162718441474&name=配载单详情`,连续弹出 6-7 条红色「运单管理不存在」,标签栏堆了一串「运单管理详情」。
**文案来源**:前端 src 里没有这句话,是后端 `BusinessException` 的 msg,被 `src/axios.js` 的响应拦截器(`status !== 200` → `ElMessage.error(message)`)统一弹出。
**主因(通道 A)**:`views/business/waybill-manage-detail.vue` 只有 23 行,写了 `:detail-id="$route.query.id"` —— 直接绑全局 `$route`,**没有路径守卫**。`waybill-manage-page.vue` 的 `detailId` watcher 是 `immediate` 且对任何非空 id 都 `openDetail({id})`。于是:
1. 之前打开过的每个运单详情标签(`/business/waybill-manage/detail?id=A`、`?id=B`…)各是一个被 keep-alive 缓存的独立实例;
2. 跳到配载单详情后,这些实例被 deactivate 但仍随 `$route` 重渲染 → `$route.query.id` 变成配载单 id;
3. prop 变化 → `openDetail({id: 配载单id})` → `getDetail(配载单id)` 去查运单 → 后端「运单管理不存在」→ 拦截器弹一次。**几个旧标签就几条提示**;
4. 且被污染实例的 `isStandaloneWaybillDetailPage` 仍是 live computed(false) → `openDetail` 走 `$router.push` 分支 → 又生成新的「运单管理详情」标签(标签栏那串的来源)。
同理风险:`waybill-manage.vue:6` 与 `transport-plan.vue:6` 的 `:detail-id="$route.query.detailId"`(`business-crud-page.vue:7356` 会 push `/business/loading-manage?detailId=id`,正好会污染运单列表实例)。
**次因(通道 B)**:`loading-manage.vue` 的 `restoreWaybillRows` 会用 `waybillIdsJson` 里的每个运单 id 并发 `getWaybillDetail(id)`,若历史运单已被删/失效,也会报同一句文案且并发多条。单条 `.catch(() => null)` 挡不住拦截器已弹出的提示。
**区分方法**:Network 里看报错请求的 `id` —— 等于 `2097607162718441474`(配载单 id)→ 通道 A;是别的数字(运单 id)→ 通道 B。或先关掉所有「运单管理详情」标签再复现,通道 A 会消失。
**待确认的修法**(用户要求先报不改):
1. `waybill-manage-page.vue`:`created()` 锁 `routePathLocked = this.$route.path`,`detailId` watcher 首行加 `if (this.$route.path !== this.routePathLocked) return;`。
2. `openDetail()` 的 push 分支加同样守卫(防其它调用路径)。
3. 宿主 view 绑定加路径守卫(`waybill-manage-detail.vue` / `waybill-manage.vue` / `transport-plan.vue`)双保险。
4. 可选:`getWaybillDetail` 支持静默模式,供 `restoreWaybillRows` 用,避免历史脏数据刷屏。
## 修复(已执行):「运单管理不存在」误报 + 误开标签
用户确认后按上方案落地,5 个文件:
1. `src/views/business/components/waybill-manage-page.vue`
- `data()` 新增 `routePathLocked: this.$route.path` —— **必须放 data,不能放 created**:Vue Options API 顺序是 data → computed → watch(immediate) → created(已在 runtime-core `applyOptions` 源码中核实)。放 created 会让首个 immediate watcher 读到空值。
- 新增 computed `isOwnedRouteActive() { return this.$route.path === this.routePathLocked; }`
- `detailId` watcher 首行加 `if (!this.isOwnedRouteActive) return;`
- `openDetail` 的 `$router.push` 分支同样加守卫。
2. `src/views/business/components/transport-plan-page.vue`:完全同构的写法(`:detail-id` host 一致),加同样的 `routePathLocked` + `isOwnedRouteActive` + watcher 守卫。
3. `src/views/business/waybill-manage-detail.vue`:`:detail-id="$route.query.id"` → `:detail-id="detailId"`,computed 做 `this.$route.path === '/business/waybill-manage/detail'` 守卫。
4. `src/views/business/waybill-manage.vue`:同上,守卫 `/business/waybill-manage`(保留旧链接 `?detailId=` 兼容)。
5. `src/views/business/transport-plan.vue`:同上,守卫 `/business/transport-plan`。
**验证**:
- dev server curl 5 个 .vue + 2 个 scoped style 全部 HTTP 200。
- 用 `@vue/server-renderer` 写了临时 mjs(已删)模拟带 `$route` 的 app,确认 **data() 里访问 `this.$route` 可行**:immediate watcher 触发时 `routePathLocked` 已是正确路径、首次加载不被拦截。
- `$route` 是 vue-router 挂在 `app.config.globalProperties` 上的 getter(vue-router.mjs:1498),组件实例化时已就绪。
**未做(第 4 条可选)**:给 `getWaybillDetail` 加静默模式供 `restoreWaybillRows` 使用(历史失效运单仍会刷多条红字),待用户定夺。
**另注**:`business-crud-page.vue:6651` 有同构的 `detailId` immediate watcher,但全项目只有上述 3 个宿主 view 传 `:detail-id`,它暂无调用方,未动。
**仍未 commit。**
## 修复(已执行):合同详情页整页空白 —— 路由表重复定义
**现象**:主订单详情点合同编号 → `/business/contract-manage/detail?id=...&name=合同详情` 内容区空白(侧栏/标签正常)。
**定位过程(关键:真实浏览器复现,不猜)**
1. 静态排查全部排除:`contract-manage.vue` 及其 26 个依赖 HTTP 200、模块 `import()` 成功、无循环依赖、详情分支引用的 24 个成员全部有定义。
2. dev 环境登录态无法直接拿(验证码 + `admin/admin` 报 `error_code 2004 用户密码强度过低`),改为**注入式复现**:
- `agent-browser open <目标 URL>`(落 `/login`);
- eval 取 `document.getElementById('app').__vue_app__.config.globalProperties` 拿 `$router` / `$store`;
- 写 cookie `saber3-access-token`(注意 token 走 **js-cookie**,不是 localStorage)+ `$store.state.user.token`;
- 覆写 `XMLHttpRequest.prototype.open/send` 让所有 `/api/**` 返回 `{code:200,data:{...}}`(defineProperty 伪造 readyState/status/responseText 后手动触发 `onloadend`)。
- 再 `$router.push(...)` 做 SPA 跳转。
3. 复现出空白,并拿到决定性证据:
- `/business/contract-manage/detail` → `$route.matched.length === 1`(只有 Layout,name undefined),`#avue-view.children.length === 0`,innerHTML 为 `<!---->`。
- 对照 `/business/contract-manage/change`(结构相同但未重复定义)→ `matched.length === 2`(Layout + 合同变更),内容完整渲染。
- `$router.getRoutes()` 里 detail 有 **3 条**记录:2 个 Layout 父记录 + 1 个子记录(`合同详情`)。
**根因**:`src/router/views/index.js` 中 `/business/contract-manage/detail` **被复制粘贴定义了两次**,两次的子路由都叫 `合同详情`。vue-router 的 `addRoute` 遇到重名会 `removeRoute(name)` 把先注册的子记录删掉,但**不会把它从父记录的 `children` 数组里摘除**;于是第一条 Layout 壳记录留在 matcher 列表里且排在前面,`resolve('/business/contract-manage/detail')` 命中这个"无有效子路由"的壳 → matched 只有 1 层 → `page/index/index.vue` 里 `#avue-view` 的二级 `<router-view>` 无匹配 → 内容区空白。
(`getRoutes()` 计数是判据:form / change 都是 2 条 = 父 + 子(健康),detail 是 3 条 = 2 父 + 1 子(坏)。)
**修复**:删除重复块(原 314-325 行),只保留 159-169 行那一份。
**验证**:
- 重复扫描脚本(正则匹配「顶层 path + `component: Layout` + 首个子路由 name」):views/index.js 27 条 Layout 子路由中仅 `合同详情` 重复;page/index.js 无重复 → 已清零。
- 修复后浏览器复验:detail → `matched.length === 2`(Layout + 合同详情)、`#avue-view.children.length === 1`,正文完整渲染「基本信息 / 签约类型 / 合同编号 HT-2026-001 / … / 变更记录」。
- 全量回归:48 条 business/vehicle/payment/settlement 路由 `resolve()` 后 `matched.length` 全部 ≥ 2,`bad: []`。
- `curl` 校验:`views/index.js`、`contract-manage.vue` 均 200。
- 残留报错均为 mock 数据形状不符所致(所有接口都返回同一个合同对象,导致 `tree.reduce` / `feeCategories.find` 之类失败),非真实缺陷。
**未 commit**(连同 1、2 轮改动一起待确认)。
+11
View File
@@ -0,0 +1,11 @@
# 2026-09-30
## /wel/index 首页重构为运营数据看板(按蓝湖效果图)
- 新增依赖:echarts ^6.1.0(corepack pnpm add echarts)。
- 重构 `src/views/wel/index.vue`:改为「核心经营指标 / 运输结构 / 业务单元情况 / 项目情况 / 财务情况 / 车队与线路 / 车务管理 / 底部占比与趋势」8 大板块 + 版权页脚,删除原欢迎横幅/快捷入口。
- 新增文件:
- `src/views/wel/chart-options.js`:排行条形/盈亏双向/环形/分组柱/面积折线/堆叠柱 6 类 option 生成器(fmt 千分位、红涨绿跌 rich label)。
- `src/views/wel/dashboard.scss`:看板公共样式,各组件 scoped `@import '../dashboard.scss'`(子目录注意路径)。
- `src/views/wel/components/`:wel-chart.vue(ECharts 封装:ResizeObserver + activated resize + dispose)、wel-filters.vue(统计时间/条件筛选 mock)、rank-panel.vue(排行/盈亏通用面板)、section-kpi / section-structure / section-business / section-project / section-finance / section-fleet / section-vehicle / section-summary.vue。
- 数据全部为 Mock,组件内已注释标注对接点;蓝湖红字标注的指标口径未定,待后端统计接口(估 12~18 个)确定后替换。
- 自检:dev server(2889) 下所有新文件及 `?vue&type=style&...&lang.scss` 均 200;`/node_modules/.vite/deps/echarts.js` 200。vite build 因 facelogin.vue 沙箱中断不可用,未跑。
+19 -44
View File
@@ -1,52 +1,27 @@
# 项目长期记忆(tms-erp-web-ws / Saber3 客商模块)
# 项目长期记忆(tms-erp-web-ws / Saber3)
## 设计约定(已确认)
后端:`/Users/gxwebsoft/JAVA/tms-api`(`tms-erp-api-ws` 滞后,别看错仓库)。
### 弹窗分组:统一用全局 `<section-card>`
- 弹窗 body 背景由 `src/styles/element-ui.scss` 全局 `.el-dialog__body { background:#f5f6fa; padding:16px }` 统一灰底。
- 每组 = 白底卡片:4px 主色竖条 + 8px 间距 + 圆角 6px + 淡阴影。组件全局注册 `<section-card title="...">`,支持 `#title` / `#extra` slot。
- 仅 3 项的小分组用 `el-col :span="8"` 均分一行。
## 列表 / 表单 / 详情三态同组件分流
`/xxx`、`/xxx/form`、`/xxx/detail` 指向同一 `.vue`;form/detail 在 `src/router/views/index.js` 是 `component: Layout` + 空 `children.path` + `meta:{keepAlive:false}`(detail 另带 `activeMenu`)。组件内按 `$route.path` 分流,必须 `watch:{$route}` 重新 init;标签标题取 `query.name`;独立页容器固定 `<div v-if>`,不用 `el-dialog`。
### Avue 内置弹窗用 section-card:dialogCustomClass 透明化 avue-form
- option 顶层加 `dialogCustomClass:'xxx-dialog'`,再在 `element-ui.scss` 写三条:
1. `.xxx-dialog .avue-form { background:transparent; box-shadow:none; padding:0 }`
2. `.xxx-dialog .avue-form__group > .el-col > .el-form-item { margin-bottom:0 }`
3. `.xxx-dialog .avue-dialog__footer, .avue-crud__dialog .xxx-dialog .avue-dialog__footer { margin-top:0 !important }`
## ⚠️ keep-alive 三坑
背景(`router/tab.js` + `page/index/index.vue`):按 `fullPath` 建 wrapper,`<keep-alive :include="tagsKeep">` 缓存,**每标签一实例、永不销毁**,deactivate 后**仍随全局 `$route` 重渲染**。
### 弹窗底部操作栏浮动(全局 CSS)
- `.el-dialog__body { max-height:calc(100vh - 200px); overflow-y:auto; overflow-x:hidden }`
- `.el-dialog__footer { margin:0; padding:12px 20px; background:#fff; border-top:1px solid #ebeef5; box-shadow:0 -2px 8px rgba(0,0,0,.04) }`
- Avue 内置弹窗 footer 真实 class 是 `.avue-dialog__footer`,须 `position:sticky; bottom:0; flex:none; background:#fff` 贴底。
**坑一:独立页容器禁用 `<component :is>` 在 div / el-dialog 间切换。** `append-to-body` 走 Teleport,`TeleportImpl.move` 忽略 `moveType`,不会把 DOM 搬回缓存容器 → 切菜单冒出旧弹窗。修法:固定 `<div v-if>` + `deactivated(){closeInnerDialogs()}` + `beforeUnmount()`(写在子组件也生效)。已修 loading-manage / project-apply / waybill-manage-page;未修 `settlement/components/{pre,formal}-settlement-editor.vue`、`transport-reconciliation-editor.vue`、`waybill-import-dialog.vue`。
### 上传证件区域尺寸(transportCapacity)
- 身份证/行驶证/道路运输证等固定 **240×151px**(1.586:1)。
- scoped 覆盖 `.xxx-uploader--large { width:240px }` 与 `--large .el-upload { width:100%; height:151px }`。
**坑二:宿主 view 直绑全局 `$route.query.*` 给子组件 prop。** 缓存实例被灌入当前页 id → 拿错 id 查后端(批量弹「运单管理不存在」),live computed 还会误 push 标签。修法:`routePathLocked` 锁路径 + watcher / push 前判 `isOwnedRouteActive` + 宿主绑定加路径守卫。⚠️ `routePathLocked` **必须放 `data()`**(Options API 顺序 data → computed → watch(immediate) → created;放 created 会读到空值、挡掉首次加载)。已修 waybill-manage-page / transport-plan-page + 三个宿主 view。
### 上传图片点击放大预览
- `src/components/image-upload-field/main.vue` 用 `<el-image preview-src-list fit="contain" preview-teleported :z-index="3000" @click.stop>`,阻止冒泡到 el-upload。
**坑三:路由表重复定义 → 内容区整页空白(2026-09-18)。** `/business/contract-manage/detail` 在 `router/views/index.js` 定义两次、子路由同名 `合同详情`。重名 `addRoute` 会 `removeRoute(name)` 移除旧子记录,但**不从父记录 children 里摘除**,第一条 Layout 壳记录留在 matcher 且靠前 → `resolve()` 命中它 → `$route.matched.length === 1` → 二级 router-view 无匹配 → 侧栏/标签正常但内容空白。修法:删重复定义。排查:扫「顶层 path + 首个子路由 name」重复项。
### 独立表单页标题 `archive-page-form__title`
- 样式全局化到 `src/styles/element-ui.scss`:18px/600/#303133 + margin-bottom:20px + 4px 主色竖条 + 8px 间距。
- business-crud-page 系列通过 `formPageTitle` prop 注入;loading-manage / master-order 手写前置标题。
## 样式硬规则
- 底栏 `flex; justify-content:flex-end`,**不写 gap**(靠 EP 默认 `.el-button+.el-button{margin-left:12px}`);浮动底栏必须 `position:fixed`(祖先 overflow:hidden 使 sticky 失效)+ `:global(.avue--collapse .x){left:60px}`、`:global(.avue-layout--horizontal .x){left:0}`,`:global()` 要包整个选择器。
- 按钮层级:次要=无 type;中间步骤=`primary plain`;主操作=`primary`(禁绿、禁两个蓝实心)。顺序 `[辅助][取消][保存草稿][提交]`。
- 独立页标题 `.archive-page-form__title`;分组用全局 `<section-card>`;上传证件区 `width:100%; max-width:240px` + `.el-upload{height:151px}`。
- ⚠️ `<style scoped lang="scss">` 顶层禁 `//` 注释(Vite5 + sass `Unexpected '/'` → 500),顶层用 `/* */`。其余见 `src/styles/element-ui.scss` 与 `AGENTS.md` 4.4。
### 表格列内嵌输入控件全宽
- `element-ui.scss` 全局兜底:`.el-table .el-table__body td.el-table__cell .cell > .el-input-number/.el-input/.el-select/.el-date-editor/.el-cascader { width:100% }`。
### 业务状态标签文字化
- 全站"状态语义"的 `el-tag` 加 `class="status-text"`,转为普通文字,不带颜色/背景/边框/圆点。
- 实现:`element-ui.scss` 末尾 `.el-tag.status-text { ... }`。
## 关键文件
- `src/components/section-card/main.vue`
- `src/styles/element-ui.scss`
- `src/views/vehicle/customer-archive.vue`
- `src/views/transportCapacity/{driver,vehicle,ship}.vue`
- `src/views/business/project-apply.vue`
## 环境注意
- Vite 代理跨域:`axios.withCredentials=true`,改 `.env`/`vite.config.mjs` 须重启 dev。
## business-crud-page 复用模式
- option 由 prop `crudOption` 经 `cloneOption` 克隆。
- 独立表单页走 `PageAvueForm`,不包 el-dialog。
- 自定义详情弹窗:`config.detailButton=true` + `detailSections:[{title, fields}]`。
## 表单 / 自检 / 导出
- placeholder 只写 `请输入`/`请选择`;例外:`请输入或选择车辆`、日期区间 `起`/`止`、示例值。校验 message 须带字段名。
- `business-crud-page.vue`:option 经 `cloneOption` 克隆,独立表单页走 `PageAvueForm`。
- 自检:`curl "localhost:2889/src/xxx.vue"` 与 `"...?vue&type=style&index=0&scoped=true&lang.scss"`(200 / 500)。`vite build` 在 `facelogin.vue` 被沙箱中断,不能作唯一校验。dev `VITE_APP_API=/api` 代理 `172.16.203.228:8000`。
- 导出:前端 `exportColumns` 后端未用,以 `XxxExportExcel.java` 为准;BladeX `BeanUtil` 类型不兼容静默跳过(`Date` → `LocalDateTime` 丢值,用 `DateUtil.fromDate`)。
+2
View File
@@ -13,6 +13,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="format-detection" content="telephone=no" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<link rel="stylesheet" href="/iconfont/index.css" />
<link rel="stylesheet" href="/iconfont/avue/iconfont.css" />
<link rel="stylesheet" href="/iconfont/saber/iconfont.css" />
+52
View File
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>oa登录</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.10.2/jquery.min.js">
</script>
<script>
// 获取URL查询字符串
const queryString = window.location.search;
// 使用URLSearchParams解析查询字符串
const urlParams = new URLSearchParams(queryString);
// 获取特定的参数值
var userid = urlParams.get('userid'); // 用户id
var username = urlParams.get('username'); // 用户名
var redirectUrl = urlParams.get('redirectUrl'); // 重定向地址
if(userid==null){
userid="";
}
if(username==null){
username="";
}
$.get("/oaApi/getToken?userid="+userid+"&username="+username,function(result){
debugger
//成功返回token
if(result.data.token){
//设置系统的cookie
document.cookie = "X-AUTH-TOKEN="+result.data.token;
if(redirectUrl==null){
//如果是登录就跳转到系统首页
window.location.href = "./";
}else {
//如果是待办有重定向就跳转
window.location.href = redirectUrl;
}
}else {
alert("该业务系统未同步当前oa用户");
}
});
</script>
</head>
<body>
</body>
</html>
+7 -2
View File
@@ -6,7 +6,8 @@
"prod": "vite --mode production",
"build": "vite build",
"build:prod": "vite build --mode production",
"serve": "vite preview --host"
"serve": "vite preview --host",
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
@@ -23,6 +24,7 @@
"crypto-js": "^4.1.1",
"dayjs": "^1.10.6",
"disable-devtool": "^0.3.8",
"echarts": "^6.1.0",
"element-plus": "^2.14.2",
"exceljs": "^4.4.0",
"highlight.js": "^11.9.0",
@@ -34,6 +36,7 @@
"sm-crypto": "^0.4.0",
"vue": "^3.5.13",
"vue-i18n": "^11.4.2",
"vue-pdf-embed": "^2.1.6",
"vue-router": "^4.3.2",
"vue3-clipboard": "^1.0.0",
"vuex": "^4.1.0",
@@ -45,9 +48,11 @@
"prettier": "^2.8.7",
"sass": "^1.85.1",
"terser": "^5.31.1",
"typescript": "^7.0.2",
"unplugin-auto-import": "^0.11.2",
"vite": "^5.4.19",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-setup-extend": "^0.4.0"
"vite-plugin-vue-setup-extend": "^0.4.0",
"vue-tsc": "^3.3.11"
}
}
+81 -55
View File
@@ -5,9 +5,7 @@ settings:
excludeLinksFromLockfile: false
patchedDependencies:
'@smallwei/avue':
hash: 5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db
path: patches/@smallwei__avue.patch
'@smallwei/avue': 5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db
importers:
@@ -33,16 +31,16 @@ importers:
version: 1.7.0
'@smallwei/avue':
specifier: ~3.9.2
version: 3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)
version: 3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)
animate.css:
specifier: ^4.1.1
version: 4.1.1
avue-plugin-ueditor:
specifier: ^1.0.4
version: 1.0.4(axios@1.16.0)(vue@3.5.34)
version: 1.0.4(axios@1.16.0(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34)
axios:
specifier: ^1.13.5
version: 1.16.0
version: 1.16.0(debug@4.4.3(supports-color@7.2.0))
codemirror:
specifier: ^5.65.21
version: 5.65.21
@@ -55,6 +53,9 @@ importers:
disable-devtool:
specifier: ^0.3.8
version: 0.3.9
echarts:
specifier: ^6.1.0
version: 6.1.0
element-plus:
specifier: ^2.14.2
version: 2.14.2(vue@3.5.34)
@@ -63,13 +64,13 @@ importers:
version: 4.4.0
highlight.js:
specifier: ^11.9.0
version: 11.11.1
version: 11.12.0
js-base64:
specifier: ^3.7.4
version: 3.7.8
version: 3.9.4
js-cookie:
specifier: ^3.0.0
version: 3.0.5
version: 3.0.8
js-md5:
specifier: ^0.7.3
version: 0.7.3
@@ -88,6 +89,9 @@ importers:
vue-i18n:
specifier: ^11.4.2
version: 11.4.2(vue@3.5.34)
vue-pdf-embed:
specifier: ^2.1.6
version: 2.1.6(vue@3.5.34)
vue-router:
specifier: ^4.3.2
version: 4.6.4(vue@3.5.34)
@@ -124,7 +128,7 @@ importers:
version: 5.4.21(sass@1.99.0)(terser@5.46.2)
vite-plugin-compression:
specifier: ^0.5.1
version: 0.5.1(vite@5.4.21(sass@1.99.0)(terser@5.46.2))
version: 0.5.1(supports-color@7.2.0)(vite@5.4.21(sass@1.99.0)(terser@5.46.2))
vite-plugin-vue-setup-extend:
specifier: ^0.4.0
version: 0.4.0(vite@5.4.21(sass@1.99.0)(terser@5.46.2))
@@ -1205,6 +1209,7 @@ packages:
crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
deprecated: Active development of CryptoJS has been discontinued. This library is no longer maintained.
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -1450,8 +1455,8 @@ packages:
heic2any@0.0.4:
resolution: {integrity: sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA==}
highlight.js@11.11.1:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
highlight.js@11.12.0:
resolution: {integrity: sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==}
engines: {node: '>=12.0.0'}
hls.js@1.6.16:
@@ -1529,12 +1534,11 @@ packages:
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
js-base64@3.7.8:
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
js-base64@3.9.4:
resolution: {integrity: sha512-PtOMXpEGuP0RRiRXsjzHzl44dMHxSu2CPvAhinupR1tBa88me+1DPqsobl7eupn5UukjLkln1ZnAOAOXOrYG0Q==}
js-cookie@3.0.5:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
js-cookie@3.0.8:
resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==}
js-md5@0.7.3:
resolution: {integrity: sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==}
@@ -1791,6 +1795,10 @@ packages:
resolution: {integrity: sha512-o8MolyzirkkLrcdsae/HEOiIcXWI7DS5zGpvqW8xTC2YUsW30rltFw2bDGvw/fskUdEMrQm2br68jzDS5BH2vw==}
engines: {node: '>=22.13.0 || >=24'}
pdfjs-dist@6.2.108:
resolution: {integrity: sha512-YxFb+SQcodN2rnX9Tn3dHYlqfb7NjlzzfONPpJd+AKoKtUjEdevTfbC07d5TcczzOK6261auRkP/M8OBHs9vFQ==}
engines: {node: '>=22.13.0 || >=24'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -2123,6 +2131,11 @@ packages:
peerDependencies:
vue: ^3.0.0
vue-pdf-embed@2.1.6:
resolution: {integrity: sha512-cNmclL9jGuRIqgm13fx/3ZZd1nW/Nn0zXfQUo/yd8AQU9GTFLLYRgFc5HM41v1+bMoj1wW59mZoB/cNQx8Y2Rw==}
peerDependencies:
vue: ^3.3.0
vue-router@4.6.4:
resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
peerDependencies:
@@ -2641,10 +2654,10 @@ snapshots:
countup.js: 2.10.0
signature_pad: 5.1.3
'@smallwei/avue@3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)':
'@smallwei/avue@3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)':
dependencies:
'@element-plus/icons-vue': 2.3.2(vue@3.5.34)
axios: 0.21.4
axios: 0.21.4(debug@4.4.3(supports-color@7.2.0))
countup.js: 1.9.3
dayjs: 1.11.20
element-plus: 2.14.2(vue@3.5.34)
@@ -2780,30 +2793,30 @@ snapshots:
dependencies:
vue: 3.5.34
'@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
'@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 4.0.6
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 3.0.0
is-url: 1.2.4
lodash.throttle: 4.1.1
nanoid: 5.1.11
slate: 0.123.0
snabbdom: 3.6.3
'@wangeditor-next/code-highlight@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
'@wangeditor-next/code-highlight@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 4.0.6
prismjs: 1.30.0
slate: 0.123.0
snabbdom: 3.6.3
'@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
'@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@types/event-emitter': 0.3.5
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
dom7: 4.0.6
dom7: 3.0.0
event-emitter: 0.3.5
html-void-elements: 3.0.0
i18next: 23.16.8
@@ -2824,13 +2837,13 @@ snapshots:
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/code-highlight': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/list-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/table-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/upload-image-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/video-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/code-highlight': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/list-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/table-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/upload-image-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/video-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 4.0.6
is-hotkey: 0.2.0
lodash.camelcase: 4.3.0
@@ -2843,16 +2856,16 @@ snapshots:
slate: 0.123.0
snabbdom: 3.6.3
'@wangeditor-next/list-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
'@wangeditor-next/list-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 4.0.6
slate: 0.123.0
snabbdom: 3.6.3
'@wangeditor-next/table-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
'@wangeditor-next/table-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 4.0.6
lodash.debounce: 4.0.8
lodash.throttle: 4.1.1
@@ -2860,22 +2873,22 @@ snapshots:
slate: 0.123.0
snabbdom: 3.6.3
'@wangeditor-next/upload-image-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)':
'@wangeditor-next/upload-image-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 4.0.6
lodash.foreach: 4.5.0
slate: 0.123.0
snabbdom: 3.6.3
'@wangeditor-next/video-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
'@wangeditor-next/video-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 4.0.6
nanoid: 5.1.11
slate: 0.123.0
@@ -3052,22 +3065,22 @@ snapshots:
asynckit@0.4.0: {}
avue-plugin-ueditor@1.0.4(axios@1.16.0)(vue@3.5.34):
avue-plugin-ueditor@1.0.4(axios@1.16.0(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34):
dependencies:
'@wangeditor/editor': 5.1.23
'@wangeditor/editor-for-vue': 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34)
axios: 1.16.0
axios: 1.16.0(debug@4.4.3(supports-color@7.2.0))
vue: 3.5.34
axios@0.21.4:
axios@0.21.4(debug@4.4.3(supports-color@7.2.0)):
dependencies:
follow-redirects: 1.16.0
follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0))
transitivePeerDependencies:
- debug
axios@1.16.0:
axios@1.16.0(debug@4.4.3(supports-color@7.2.0)):
dependencies:
follow-redirects: 1.16.0
follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0))
form-data: 4.0.5
proxy-from-env: 2.1.0
transitivePeerDependencies:
@@ -3235,9 +3248,11 @@ snapshots:
dayjs@1.11.20: {}
debug@4.4.3:
debug@4.4.3(supports-color@7.2.0):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 7.2.0
delayed-stream@1.0.0: {}
@@ -3445,7 +3460,9 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
follow-redirects@1.16.0: {}
follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)):
optionalDependencies:
debug: 4.4.3(supports-color@7.2.0)
form-data@4.0.5:
dependencies:
@@ -3534,7 +3551,7 @@ snapshots:
heic2any@0.0.4: {}
highlight.js@11.11.1: {}
highlight.js@11.12.0: {}
hls.js@1.6.16: {}
@@ -3593,9 +3610,9 @@ snapshots:
isarray@1.0.0: {}
js-base64@3.7.8: {}
js-base64@3.9.4: {}
js-cookie@3.0.5: {}
js-cookie@3.0.8: {}
js-md5@0.7.3: {}
@@ -3822,6 +3839,10 @@ snapshots:
optionalDependencies:
'@napi-rs/canvas': 1.0.2
pdfjs-dist@6.2.108:
optionalDependencies:
'@napi-rs/canvas': 1.0.2
picocolors@1.1.1: {}
picomatch@2.3.2: {}
@@ -4136,10 +4157,10 @@ snapshots:
uuid@8.3.2: {}
vite-plugin-compression@0.5.1(vite@5.4.21(sass@1.99.0)(terser@5.46.2)):
vite-plugin-compression@0.5.1(supports-color@7.2.0)(vite@5.4.21(sass@1.99.0)(terser@5.46.2)):
dependencies:
chalk: 4.1.2
debug: 4.4.3
debug: 4.4.3(supports-color@7.2.0)
fs-extra: 10.1.0
vite: 5.4.21(sass@1.99.0)(terser@5.46.2)
transitivePeerDependencies:
@@ -4171,6 +4192,11 @@ snapshots:
'@vue/devtools-api': 6.6.4
vue: 3.5.34
vue-pdf-embed@2.1.6(vue@3.5.34):
dependencies:
pdfjs-dist: 6.2.108
vue: 3.5.34
vue-router@4.6.4(vue@3.5.34):
dependencies:
'@vue/devtools-api': 6.6.4
+5
View File
@@ -1,5 +1,10 @@
packages:
- '.'
allowBuilds:
'@parcel/watcher': set this to true or false
es5-ext: set this to true or false
esbuild: set this to true or false
patchedDependencies:
'@smallwei/avue': patches/@smallwei__avue.patch
Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+11
View File
@@ -58,3 +58,14 @@ export const changeStatus = (id, status) => {
},
});
};
export const getBfiExchangeRate = (currencyCode, effectdate) => {
return request({
url: '/blade-transport/bfi/exchange-rate',
method: 'get',
params: {
currencyCode,
effectdate,
},
});
};
+31
View File
@@ -0,0 +1,31 @@
import request from '@/axios';
export const getList = (current, size, params) =>
request({
url: '/blade-system/invoice-item/list',
method: 'get',
params: { ...params, current, size },
});
export const getAll = () => getList(1, 1000, {});
export const getDetail = id =>
request({
url: '/blade-system/invoice-item/detail',
method: 'get',
params: { id },
});
export const submit = row =>
request({
url: '/blade-system/invoice-item/submit',
method: 'post',
data: row,
});
export const remove = ids =>
request({
url: '/blade-system/invoice-item/remove',
method: 'post',
params: { ids },
});
+52
View File
@@ -0,0 +1,52 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/measurement-unit/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-system/measurement-unit/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-system/measurement-unit/remove',
method: 'post',
params: {
ids,
},
});
};
export const submit = row => {
return request({
url: '/blade-system/measurement-unit/submit',
method: 'post',
data: row,
});
};
export const changeStatus = (id, status) => {
return request({
url: '/blade-system/measurement-unit/status',
method: 'post',
params: {
id,
status,
},
});
};
+3 -4
View File
@@ -92,13 +92,12 @@ export const createCrudApi = baseUrl => ({
data,
});
},
reassign(id) {
reassign(data) {
const payload = data && typeof data === 'object' ? data : { id: data };
return request({
url: `${baseUrl}/reassign`,
method: 'post',
params: {
id,
},
data: payload,
});
},
batchComplete(ids) {
+3 -3
View File
@@ -59,14 +59,14 @@ export const withdraw = id =>
params: { id },
});
export const startChange = (id, reason) =>
export const startChange = (id, changeContent, changeReason = changeContent) =>
request({
url: `${baseUrl}/start-change`,
method: 'post',
params: {
id,
changeContent: reason,
changeReason: reason,
changeContent: changeContent || '',
changeReason,
},
});
+11 -1
View File
@@ -24,10 +24,11 @@ export const getDetail = id =>
},
});
export const getCarrierContracts = () =>
export const getCarrierContracts = projectIds =>
request({
url: `${baseUrl}/carrier-contracts`,
method: 'get',
params: { projectIds: (projectIds || []).join(',') },
});
export const saveDraft = row =>
@@ -78,6 +79,15 @@ export const cancel = id =>
},
});
export const start = id =>
request({
url: `${baseUrl}/start`,
method: 'post',
params: {
id,
},
});
export const complete = id =>
request({
url: `${baseUrl}/complete`,
+16 -1
View File
@@ -13,12 +13,27 @@ export const getDetail = (id, waybillId) =>
...(waybillId === undefined ? {} : { waybillId }),
},
});
export const getVoucherImages = waybillId =>
export const getVoucherImages = (waybillId, params = {}) =>
request({
url: '/blade-transport/process-config/voucher-images',
method: 'get',
params: {
waybillId,
...params,
},
});
export const getVoucherFolders = waybillId =>
request({
url: '/blade-transport/process-config/voucher-folders',
method: 'get',
params: { waybillId },
});
export const getVoucherFolderDetail = (voucherId, plateNo) =>
request({
url: '/blade-transport/process-config/voucher-folder-detail',
method: 'get',
params: { voucherId, plateNo },
});
export const submit = api.submit;
export const remove = api.remove;
export const copy = api.copy;
+15
View File
@@ -6,9 +6,24 @@ const api = createCrudApi(baseUrl);
export const getList = api.getList;
export const getDetail = api.getDetail;
export const getChangeRecordDetail = (id, recordIndex) =>
request({
url: `${baseUrl}/change-record/detail`,
method: 'get',
params: { id, recordIndex },
});
export const submit = api.submit;
export const remove = api.remove;
// 项目资金使用风险统计(高风险 / 中风险数量),供列表头部快速筛选标签使用
// 暂未启用:后端 /fund-risk-stats 端点尚未实现,列表页已移除调用,等后端就绪后恢复
export const getFundRiskStats = params =>
request({
url: `${baseUrl}/fund-risk-stats`,
method: 'get',
params,
});
export const saveDraft = row =>
request({
url: `${baseUrl}/save-draft`,
+24 -1
View File
@@ -10,7 +10,6 @@ export const submit = api.submit;
export const dispatch = api.dispatch;
export const remove = api.remove;
export const copy = api.copy;
export const cancel = api.cancel;
export const complete = api.complete;
export const importTransportPlan = ({
@@ -36,3 +35,27 @@ export const importTransportPlan = ({
timeout: 60000,
});
};
export const validateTransportPlan = ({
file,
projectId,
projectName,
customerName,
contractId,
contractName,
}) => {
const data = new FormData();
data.append('file', file);
data.append('projectId', projectId || '');
data.append('projectName', projectName || '');
data.append('customerName', customerName || '');
data.append('contractId', contractId || '');
data.append('contractName', contractName || '');
return request({
url: `${baseUrl}/validate-transport-plan`,
method: 'post',
data,
responseType: 'blob',
timeout: 60000,
});
};
+25
View File
@@ -5,7 +5,29 @@ const baseUrl = '/blade-transport/voucher-manage';
export const getList = (current, size, params) =>
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
export const getFolderPage = (voucherId, current, size, params) =>
request({ url: `${baseUrl}/folder-page`, method: 'get', params: { voucherId, current, size, ...params } });
export const getFolderDetail = (voucherId, plateNo) =>
request({ url: `${baseUrl}/folder-detail`, method: 'get', params: { voucherId, plateNo } });
export const replaceFolder = (voucherId, plateNo, file) => {
const data = new FormData();
data.append('voucherId', voucherId);
data.append('plateNo', plateNo);
data.append('file', file);
return request({ url: `${baseUrl}/folder-replace`, method: 'post', data });
};
export const replaceFolderByObject = (voucherId, plateNo, fileInfo) =>
request({
url: `${baseUrl}/folder-replace-object`,
method: 'post',
params: { voucherId, plateNo, ...fileInfo },
timeout: 120000,
});
export const removeFolder = (voucherId, plateNo) =>
request({ url: `${baseUrl}/folder-remove`, method: 'post', params: { voucherId, plateNo } });
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
export const changeWaybillBatch = data =>
request({ url: `${baseUrl}/change-waybill-batch`, method: 'post', data });
export const createUploadDraft = data =>
request({ url: `${baseUrl}/upload-draft`, method: 'post', data });
export const completeUploadFile = data =>
@@ -17,6 +39,9 @@ export const getWaybillBatches = (current, size, params) =>
method: 'get',
params: { current, size, ...params },
});
export const auditPass = id => request({ url: `${baseUrl}/audit-pass`, method: 'post', params: { id } });
export const auditReject = (id, rejectReason) =>
request({ url: `${baseUrl}/audit-reject`, method: 'post', params: { id, rejectReason } });
const fileTaskBaseUrl = '/blade-file/fileTask';
export const queryFileTask = md5 =>
+47 -2
View File
@@ -7,6 +7,7 @@ const api = createCrudApi(baseUrl);
export const getList = api.getList;
export const getDetail = api.getDetail;
export const submit = api.submit;
export const saveDraft = data => request({ url: `${baseUrl}/save-draft`, method: 'post', data });
export const remove = api.remove;
export const copy = api.copy;
export const cancel = api.cancel;
@@ -19,6 +20,38 @@ export const changeRoute = data =>
method: 'post',
data,
});
export const maintainMileage = data =>
request({
url: `${baseUrl}/maintain-mileage`,
method: 'post',
data,
});
/** 运单打卡记录 + 司机上传凭证图 */
export const getPunchRecords = waybillId =>
request({
url: `${baseUrl}/punch-records`,
method: 'get',
params: { waybillId },
});
/** 运单车辆实时定位 */
export const locateVehicle = id =>
request({
url: `${baseUrl}/locate`,
method: 'post',
params: { id },
timeout: 60000,
});
/** 运单车辆历史轨迹 */
export const trackVehicle = (id, startDate, endDate) =>
request({
url: `${baseUrl}/track`,
method: 'post',
params: { id, startDate, endDate },
timeout: 60000,
});
export const roadLoading = ids =>
request({
url: `${baseUrl}/road-loading`,
@@ -29,9 +62,21 @@ export const roadLoading = ids =>
});
export const getImportBatches = params => request({ url: `${baseUrl}/import-batch/list`, method: 'get', params });
export const getImportBatchNextCode = () => request({ url: `${baseUrl}/import-batch/next-code`, method: 'get' });
export const getImportDetails = params => request({ url: `${baseUrl}/import-batch/details`, method: 'get', params });
export const removeImportBatches = ids => request({ url: `${baseUrl}/import-batch/remove`, method: 'post', params: { ids } });
export const getImportOptions = () => request({ url: `${baseUrl}/import-batch/options`, method: 'get' });
export const exportImportTemplate = () => request({ url: `${baseUrl}/import-batch/export-template`, method: 'get', responseType: 'blob' });
export const saveImportDraft = data => request({ url: `${baseUrl}/import-batch/draft`, method: 'post', data });
export const confirmImport = data => request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data });
export const saveImportDraft = data => request({ url: `${baseUrl}/import-batch/draft`, method: 'post', data, timeout: 120000 });
export const validateImport = data =>
request({ url: `${baseUrl}/import-batch/validate`, method: 'post', data, timeout: 120000 });
export const exportImportFailures = data =>
request({
url: `${baseUrl}/import-batch/export-failures`,
method: 'post',
data,
responseType: 'blob',
timeout: 120000,
});
export const confirmImport = data =>
request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data, timeout: 120000 });
+3 -2
View File
@@ -34,7 +34,8 @@ export const exportBlob = (url, params, options = {}) => {
});
};
export const importBlob = (url, file) => {
export const importBlob = (url, file, options = {}) => {
const { timeout = 60000 } = options;
const formData = new FormData();
formData.append('file', file);
return request({
@@ -42,6 +43,6 @@ export const importBlob = (url, file) => {
method: 'post',
data: formData,
responseType: 'blob',
timeout: 60000,
timeout,
});
};
+23
View File
@@ -0,0 +1,23 @@
import request from '@/axios';
export const getMkPublicDetail = (bizType, id) => {
return request({
url: `/blade-transport/mk-process/public/${bizType}/detail`,
method: 'get',
meta: {
isToken: false,
},
params: { id },
});
};
export const postMkPublicProcessMessage = (bizType, data) => {
return request({
url: `/blade-transport/mk-process/public/${bizType}/process-message`,
method: 'post',
meta: {
isToken: false,
},
data,
});
};
+6
View File
@@ -12,6 +12,12 @@ export const getAvailableOptions = (keyword, deptId, selectedId) =>
method: 'get',
params: { keyword, deptId, selectedId },
});
export const getAvailablePage = (current, size, keyword, deptId, selectedId) =>
request({
url: `${baseUrl}/available-page`,
method: 'get',
params: { current, size, keyword, deptId, selectedId },
});
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
+7 -3
View File
@@ -5,8 +5,12 @@ const baseUrl = '/blade-transport/invoice-application';
export const getList = (current, size, params) =>
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
export const getSettlementCandidates = keyword =>
request({ url: `${baseUrl}/settlement-candidates`, method: 'get', params: { keyword } });
export const getSettlementCandidates = (current, size, params) =>
request({
url: `${baseUrl}/settlement-candidates`,
method: 'get',
params: { current, size, ...params },
});
export const getSettlementDetails = settlementIds =>
request({ url: `${baseUrl}/settlement-details`, method: 'get', params: { settlementIds } });
export const getReceiverInformation = settlementIds =>
@@ -18,7 +22,7 @@ export const approve = data => request({ url: `${baseUrl}/approve`, method: 'pos
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
export const syncKingdee = id =>
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
export const invoiceTypeOptions = [
{ label: '电子专票', value: 'electronic_special' },
+18 -8
View File
@@ -5,8 +5,17 @@ const baseUrl = '/blade-transport/invoice-receipt';
export const getList = (current, size, params) =>
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
export const getInvoicePool = keyword =>
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params: { keyword } });
export const queryInvoicePool = params =>
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params });
export const downloadInvoicePoolAttachment = invoiceNo =>
// 金蝶票池发票文件下载(后端代理 A801-2 流式回源),耗时较普通接口长,放宽到 120s
request({
url: `${baseUrl}/invoice-pool/attachment/download`,
method: 'get',
params: { invoiceNo },
responseType: 'blob',
timeout: 120000,
});
export const getSettlementCandidates = (keyword, receiptId) =>
request({
url: `${baseUrl}/settlement-candidates`,
@@ -25,8 +34,8 @@ export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post'
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
export const syncKingdee = id =>
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
export const pushKingdee = ids =>
request({ url: `${baseUrl}/push-kingdee`, method: 'post', timeout: 30000, data: ids });
export const approvalStatusOptions = [
{ label: '草稿', value: 'draft' },
@@ -36,8 +45,9 @@ export const approvalStatusOptions = [
{ label: '已作废', value: 'voided' },
];
export const kingdeeStatusOptions = [
{ label: '未同步', value: 'unsynced' },
{ label: '已同步', value: 'synced' },
{ label: '同步失败', value: 'failed' },
export const kingdeePushStatusOptions = [
{ label: '未推送', value: 'unsynced' },
{ label: '已保存收票单', value: 'pushed' },
{ label: '已指定应付', value: 'assigned' },
{ label: '推送失败', value: 'failed' },
];
+18 -7
View File
@@ -4,24 +4,32 @@ const baseUrl = '/blade-transport/payment-application';
export const getList = (current, size, params) =>
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
export const getReferenceAmount = (paymentType, referenceId, excludeId) =>
request({
url: `${baseUrl}/reference-amount`,
method: 'get',
params: { paymentType, referenceId, excludeId: excludeId || undefined },
});
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
// 同步金蝶涉及多次金蝶远程接口调用(A103-2 预检 + A103-1/A103-3 推送),单笔可达 20s+,覆盖全局 10s 超时
export const syncKingdee = id =>
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
// 批量同步逐单单据级事务串行推送,耗时随单量线性增长
export const syncKingdeeBatch = ids =>
request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', timeout: 30000, data: ids });
// 回写任务按 paymentNo 分批远程查询 A103-2/A103-4,同样可能超过全局超时
export const syncKingdeeResult = () =>
request({ url: `${baseUrl}/sync-kingdee-result`, method: 'post', timeout: 30000 });
export const paymentTypeOptions = [
{ label: '项目预付', value: 'project_advance' },
{ label: '进度预付', value: 'progress_advance' },
{ label: '结算付款', value: 'settlement_payment' },
];
export const paymentMethodOptions = [
{ label: '银行转账', value: 'bank_transfer' },
{ label: '银行承兑汇票', value: 'bank_draft' },
{ label: '商业承兑汇票', value: 'commercial_draft' },
{ label: '尾款付款', value: 'settlement_payment' },
];
export const approvalStatusOptions = [
{ label: '草稿', value: 'draft' },
@@ -33,5 +41,8 @@ export const approvalStatusOptions = [
export const kingdeeStatusOptions = [
{ label: '未生成', value: 'unsynced' },
{ label: '已生成', value: 'synced' },
{ label: '付款中', value: 'paying' },
{ label: '已付款', value: 'paid' },
{ label: '已关闭', value: 'closed' },
{ label: '生成失败', value: 'failed' },
];
+10
View File
@@ -8,14 +8,24 @@ export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get'
export const updateAttachments = data =>
request({ url: `${baseUrl}/attachments`, method: 'post', data });
export const voidClaim = id => request({ url: `${baseUrl}/void`, method: 'post', params: { id } });
export const queryKingdeeResult = () =>
request({ url: `${baseUrl}/query-kingdee-result`, method: 'post' });
export const claimStatusOptions = [
{ label: '已认领', value: 'claimed' },
{ label: '已作废', value: 'voided' },
];
export const pushStatusOptions = [
{ label: '未推送', value: 'none' },
{ label: '已推送', value: 'pushed' },
{ label: '推送失败', value: 'failed' },
];
export const kingdeeBillStatusOptions = [
{ label: '未生成', value: 'none' },
{ label: '已推送', value: 'pushed' },
{ label: '审核通过', value: 'approved' },
{ label: '已收款', value: 'received' },
{ label: '处理失败', value: 'failed' },
];
+15
View File
@@ -13,9 +13,24 @@ export const getSettlementCandidates = (keyword, flowId) =>
});
export const claim = data => request({ url: `${baseUrl}/claim`, method: 'post', data });
export const sync = () => request({ url: `${baseUrl}/sync`, method: 'post' });
export const pushKingdee = flowId =>
request({ url: `${baseUrl}/push-kingdee`, method: 'post', params: { flowId } });
export const claimStatusOptions = [
{ label: '未认领', value: 'unclaimed' },
{ label: '部分认领', value: 'partial' },
{ label: '认领完成', value: 'claimed' },
];
export const pushStatusOptions = [
{ label: '未推送', value: 'none' },
{ label: '已推送', value: 'pushed' },
{ label: '推送失败', value: 'failed' },
];
export const kingdeeBillStatusOptions = [
{ label: '已推送', value: 'pushed' },
{ label: '审核通过', value: 'approved' },
{ label: '已收款', value: 'received' },
{ label: '处理失败', value: 'failed' },
];
+18 -2
View File
@@ -4,6 +4,8 @@ const baseUrl = '/blade-transport/formal-settlement';
export const getList = (current, size, params) =>
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
export const exportList = params =>
request({ url: `${baseUrl}/export`, method: 'get', params, responseType: 'blob' });
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
export const getCandidates = (current, size, params) =>
request({
@@ -13,7 +15,8 @@ export const getCandidates = (current, size, params) =>
});
export const getContractOptions = (keyword, projectId) =>
request({ url: `${baseUrl}/contract-options`, method: 'get', params: { keyword, projectId } });
export const getNextNo = () => request({ url: `${baseUrl}/next-no`, method: 'get' });
export const getNextNo = settlementType =>
request({ url: `${baseUrl}/next-no`, method: 'get', params: { settlementType } });
export const getFeeOptions = () => request({ url: `${baseUrl}/fee-options`, method: 'get' });
export const getCandidateDetails = (current, size, params) =>
request({
@@ -27,11 +30,24 @@ export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post'
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
// 同步金蝶涉及多次金蝶远程接口调用(A102-4 预检 + A102-2 保存),可达 20s+,覆盖全局 10s 超时
export const syncKingdee = id =>
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
export const getDetailFees = detailId =>
request({ url: `${baseUrl}/detail-fees`, method: 'get', params: { detailId } });
export const getReceiptClaims = formalSettlementId =>
request({
url: `${baseUrl}/receipt-claims`,
method: 'get',
params: { formalSettlementId },
});
export const adjustDetail = data =>
request({ url: `${baseUrl}/adjust-detail`, method: 'post', data });
export const applyPayment = data =>
request({ url: `${baseUrl}/apply-payment`, method: 'post', data });
export const applyPayments = data =>
request({ url: `${baseUrl}/apply-payments`, method: 'post', data });
export const claimInvoices = data =>
request({ url: `${baseUrl}/claim-invoices`, method: 'post', data });
export const queryInvoicePool = params =>
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params });
+12
View File
@@ -10,6 +10,18 @@ export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get'
export const getContractOptions = keyword =>
request({ url: `${baseUrl}/contract-options`, method: 'get', params: { keyword } });
export const getContractList = (current, size, params) =>
request({
url: '/blade-transport/contract-manage/list',
method: 'get',
params: {
current,
size,
approvalStatuses: 'approved,change_approved',
...params,
},
});
export const getFeeOptions = () => request({ url: `${baseUrl}/fee-options`, method: 'get' });
export const getCandidateDetails = (current, size, params) =>
@@ -8,6 +8,8 @@ export const getFormalSettlements = keyword =>
request({ url: `${baseUrl}/candidate-formal-settlements`, method: 'get', params: { keyword } });
export const getFormalDetails = formalSettlementId =>
request({ url: `${baseUrl}/formal-details`, method: 'get', params: { formalSettlementId } });
export const getFeeOptions = () =>
request({ url: `${baseUrl}/fee-options`, method: 'get' });
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
export const submit = id => request({ url: `${baseUrl}/submit`, method: 'post', data: { id } });
+10 -4
View File
@@ -4,6 +4,8 @@ const baseUrl = '/blade-transport/transport-reconciliation';
export const getList = (current, size, params) =>
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
export const exportList = params =>
request({ url: `${baseUrl}/export`, method: 'get', params, responseType: 'blob' });
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
export const getFormalOptions = (current, size, params) =>
request({
@@ -14,17 +16,21 @@ export const getFormalOptions = (current, size, params) =>
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
export const match = id => request({ url: `${baseUrl}/match`, method: 'post', params: { id } });
export const matchPreview = data =>
request({ url: `${baseUrl}/match-preview`, method: 'post', data });
export const manualMatch = data =>
request({ url: `${baseUrl}/manual-match`, method: 'post', data });
export const unmatch = internalId =>
request({ url: `${baseUrl}/unmatch`, method: 'post', params: { internalId } });
export const adjust = data => request({ url: `${baseUrl}/adjust`, method: 'post', data });
export const updateByMatch = id =>
request({ url: `${baseUrl}/update-by-match`, method: 'post', params: { id } });
export const updateByMatch = (id, data) =>
request({ url: `${baseUrl}/update-by-match`, method: 'post', params: { id }, data });
export const complete = id =>
request({ url: `${baseUrl}/complete`, method: 'post', params: { id } });
export const template = mode =>
request({ url: `${baseUrl}/template`, method: 'get', params: { mode }, responseType: 'blob' });
export const completeWithData = data =>
request({ url: `${baseUrl}/complete-with-data`, method: 'post', data });
export const template = (mode, params) =>
request({ url: `${baseUrl}/template`, method: 'get', params: { mode, ...params }, responseType: 'blob' });
const importFile = (url, id, file) => {
const data = new FormData();
+59
View File
@@ -0,0 +1,59 @@
import request from '@/axios';
/**
* 调用 MK processSubmit 提交审核流
* @param {Object} data
* @param {string} data.templateCode 模板编码
* @param {string} data.submitIdentity 提交人(手机号)
* @param {string} data.loginName 登录账号(手机号)
* @param {string} data.bizId 业务主键(后端生成/复用 formInstanceId)
* @param {string} [data.subject] 流程标题
* @param {string} [data.bizType] 业务类型
*/
export const processSubmit = data => {
return request({
url: '/blade-system/businessProcess/processSubmit',
method: 'post',
data,
});
};
/**
* 调用 MK processDelete 删除审核流(驳回后重新提交前使用)
* @param {Object} data
* @param {string} data.bizId 业务主键
*/
export const processDelete = data => {
return request({
url: '/blade-system/businessProcess/processDelete',
method: 'post',
data,
});
};
/**
* 查询 MK 流程实例是否仍存在(有数据则无需删除/重新提交)
* @param {Object} params
* @param {string} params.bizId 业务主键
*/
export const hasMkProcessInstance = params => {
return request({
url: '/blade-system/businessProcess/hasMkProcessInstance',
method: 'get',
params,
});
};
/**
* 获取 MK 审批页跳转链接(后端按 thirdParty.mk 配置拼接,含 SSO)
* @param {Object} params
* @param {string} [params.bizId] 业务 id(如客商 id)
* @param {string} [params.processInstanceId] 流程实例 id
*/
export const getMKApprovalUrl = params => {
return request({
url: '/blade-system/businessProcess/getMKApprovalUrl',
method: 'get',
params,
});
};
+49
View File
@@ -41,6 +41,48 @@ export const add = row => {
});
};
export const syncIamOrganizations = () => {
return request({
url: '/blade-system/dept/sync-iam-organizations',
method: 'post',
});
};
export const clearNonTopDept = signal => {
return request({
url: '/blade-system/dept/clear-non-top',
method: 'post',
timeout: 60000,
signal,
});
};
export const syncOaCompany = (current = 1, size = 20, signal) => {
return request({
url: '/blade-system/dept/sync-oa-company',
method: 'post',
params: {
current,
size,
},
timeout: 60000,
signal,
});
};
export const syncOaDepartment = (current = 1, size = 20, signal) => {
return request({
url: '/blade-system/dept/sync-oa-department',
method: 'post',
params: {
current,
size,
},
timeout: 60000,
signal,
});
};
export const update = row => {
return request({
url: '/blade-system/dept/submit',
@@ -78,3 +120,10 @@ export const getDeptLazyTree = parentId => {
},
});
};
export const getPlatformCompanySelect = () => {
return request({
url: '/blade-system/dept/platform-company-select',
method: 'get',
});
};
+7
View File
@@ -86,3 +86,10 @@ export const getDictionary = params => {
params,
});
};
export const getDictionaryAll = () => {
return request({
url: '/blade-system/dict-biz/select-all',
method: 'get',
});
};
+13
View File
@@ -31,6 +31,19 @@ export const add = row => {
});
};
export const syncIamAccounts = (current = 1, size = 50, signal) => {
return request({
url: '/blade-system/user/sync-iam-accounts',
method: 'post',
params: {
current,
size,
},
timeout: 60000,
signal,
});
};
export const update = row => {
return request({
url: '/blade-system/user/update',
@@ -0,0 +1,25 @@
import request from '@/axios';
const baseUrl = '/blade-transport/vehicle-dispatch';
export const getList = (current, size, params) =>
request({ url: `${baseUrl}/list`, method: 'get', params: { ...params, current, size } });
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
export const submit = row => request({ url: `${baseUrl}/submit`, method: 'post', data: row });
export const remove = ids => request({ url: `${baseUrl}/remove`, method: 'post', params: { ids } });
export const submitApproval = id =>
request({ url: `${baseUrl}/submit-approval`, method: 'post', params: { id } });
export const approve = id => request({ url: `${baseUrl}/approve`, method: 'post', params: { id } });
export const exportVehicleDispatch = params =>
request({
url: `${baseUrl}/export-vehicle-dispatch`,
method: 'get',
params,
responseType: 'blob',
});
@@ -47,3 +47,11 @@ export const update = row => {
data: row,
});
};
export const getExpiryStat = params => {
return request({
url: '/blade-transport/annual-inspection-record/expiry-stat',
method: 'get',
params,
});
};
+61
View File
@@ -22,6 +22,19 @@ export const getDetail = id => {
});
};
export const getPublicDetail = id => {
return request({
url: '/blade-transport/customer-archive/public/detail',
method: 'get',
meta: {
isToken: false,
},
params: {
id,
},
});
};
export const getChangeRecordList = (customerId, current, size) => {
return request({
url: '/blade-transport/customer-archive/change-record/list',
@@ -34,6 +47,32 @@ export const getChangeRecordList = (customerId, current, size) => {
});
};
export const getPublicChangeRecordList = (customerId, current, size) => {
return request({
url: '/blade-transport/customer-archive/public/change-record/list',
method: 'get',
meta: {
isToken: false,
},
params: {
customerId,
current,
size,
},
});
};
export const postPublicProcessMessage = data => {
return request({
url: '/blade-transport/customer-archive/public/process-message',
method: 'post',
meta: {
isToken: false,
},
data,
});
};
export const submit = row => {
return request({
url: '/blade-transport/customer-archive/submit',
@@ -113,6 +152,28 @@ export const getScoreTemplate = quantificationId => {
});
};
export const queryKingdeeCustomer = creditCode => {
return request({
url: '/blade-transport/bfi/customer/query-by-credit-code',
method: 'get',
timeout: 30000,
params: {
creditCode,
},
});
};
export const syncKingdee = id => {
return request({
url: '/blade-transport/customer-archive/sync-kingdee',
method: 'post',
timeout: 30000,
params: {
id,
},
});
};
export const recognizeBusinessLicenseOcr = imageUrl => {
return request({
url: '/blade-transport/baidu-ocr/recognize-url',
+63 -11
View File
@@ -70,11 +70,44 @@ const redirectToLogin = () => {
store.dispatch('FedLogOut').then(() => router.push({ path: '/login' }));
};
/**
* 统一弹出错误提示并返回带标记的 Error。
* `_notified` 标记用于让业务层(批量导入等)识别「拦截器已经提示过」,
* 避免同一条错误被弹两次。
*/
const notifiedError = message => {
ElMessage({
message: message,
type: 'error',
});
const error = new Error(message);
error._notified = true;
return error;
};
axios.defaults.timeout = 10000;
//返回其他状态码
axios.defaults.validateStatus = function (status) {
return status >= 200 && status <= 500; // 默认的
};
/**
* 判断响应是否为 JSON。
* 用于 blob 请求(导入/导出)的兜底:后端在业务校验失败时返回的是 JSON
* (Content-Type: application/json),而不是文件流,需要解析后才能取到 msg。
*/
const isJsonContentType = res => {
const headers = res.headers || {};
const contentType = String(headers['content-type'] || res.data?.type || '').toLowerCase();
return contentType.includes('application/json') || contentType.includes('text/json');
};
/**
* 判断响应体是否已是解析好的对象(无需再解析)。
* 注意 Blob 也是 object,必须排除,否则会把待解析的文件流误判为已完成。
*/
const isPlainObject = value =>
value !== null && typeof value === 'object' && !(value instanceof Blob);
//跨域请求,允许保存cookie
axios.defaults.withCredentials = true;
// NProgress Configuration
@@ -140,7 +173,7 @@ axios.interceptors.request.use(
//http response拦截
axios.interceptors.response.use(
res => {
async res => {
NProgress.done();
//获取配置信息
const config = res.config;
@@ -149,6 +182,22 @@ axios.interceptors.response.use(
if (cryptoData) {
res.data = JSON.parse(crypto.decryptAES(res.data, crypto.aesKey));
}
// blob 请求(文件下载、导入)的响应体是 Blob,直接读 res.data.code / res.data.msg
// 拿不到任何字段:状态码会退回 res.status、提示文案会落到「系统错误」兜底,
// 用户就会以为系统崩了——而后端其实返回了明确的业务提示(如「未读取到数据行…」)。
// 这里按 Content-Type 判断:JSON 才解析回对象;真·文件流(Excel 等)保持原样不动。
// 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。
if (isJsonContentType(res) && !isPlainObject(res.data)) {
try {
const text = res.data instanceof Blob ? await res.data.text() : res.data;
const parsed = typeof text === 'string' && text ? JSON.parse(text) : null;
if (parsed && typeof parsed === 'object') {
res.data = parsed;
}
} catch (ignored) {
// 声明是 JSON 却解析失败:保持原样,交给下游按文件处理
}
}
//获取状态信息
const status = res.data.error_code || res.data.code || res.status;
const statusWhiteList = website.statusWhiteList || [];
@@ -204,26 +253,29 @@ axios.interceptors.response.use(
// OAuth2 业务错误码提示
if (status > 2000 && !validatenull(res.data.error_description)) {
ElMessage({
message: message,
type: 'error',
});
return Promise.reject(new Error(message));
return Promise.reject(notifiedError(message));
}
// 其余非 200 响应统一提示
if (status !== 200) {
ElMessage({
message: message,
type: 'error',
});
return Promise.reject(new Error(message));
return Promise.reject(notifiedError(message));
}
return res;
},
error => {
NProgress.done();
// 网络级失败(HTTP 状态 >500、网关错误、超时、断网等 validateStatus 放行之外的响应)
// 原为静默 reject,接口报错页面无任何感知;统一补错误提示,取消请求与刷新令牌请求除外
if (!axios.isCancel(error) && !isRefreshTokenRequest(error?.config)) {
ElMessage({
message: error?.response?.data?.msg || '请求失败,请稍后重试',
type: 'error',
});
}
return Promise.reject(new Error(error));
// 保留原始 axios 错误对象:下游(如导入失败明细解析)需要 error.response
// 读取响应体。若包装成 new Error(error),response 会丢失、message 退化成
// "[object Object]",用户只能看到无意义的通用提示。
}
);
+143 -34
View File
@@ -1,11 +1,5 @@
<template>
<el-dialog
v-model="visible"
title="地图选择地址"
width="920px"
append-to-body
@opened="initMap"
>
<el-dialog v-model="visible" title="地图选择地址" width="920px" append-to-body @opened="initMap">
<div class="address-map-picker__toolbar">
<el-input
v-model="keyword"
@@ -15,7 +9,10 @@
/>
<el-button type="primary" :loading="searching" @click="searchKeyword">搜索</el-button>
</div>
<div ref="map" class="address-map-picker__map"></div>
<div class="address-map-picker__content">
<div ref="map" class="address-map-picker__map"></div>
<map-search-results :results="searchResults" @select="selectSearchResult" />
</div>
<div class="address-map-picker__info">{{ status }}</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
@@ -27,8 +24,10 @@
</template>
<script>
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
const AMAP_KEY = '13dcb15bc29bfe9c432d17ecd808b812';
const AMAP_SECURITY_CODE = 'f9d036ef29e376e15d309e0de5c0f7ab';
let amapLoader;
export default {
@@ -41,6 +40,14 @@ export default {
type: String,
default: '',
},
longitude: {
type: [String, Number],
default: '',
},
latitude: {
type: [String, Number],
default: '',
},
},
emits: ['update:modelValue', 'confirm'],
data() {
@@ -54,6 +61,8 @@ export default {
amap: null,
marker: null,
geocoder: null,
searchResults: [],
mapInitializing: null,
};
},
watch: {
@@ -63,8 +72,11 @@ export default {
this.visible = value;
if (value) {
this.keyword = this.address || '';
this.selected = {};
this.status = '可搜索地址或点击地图选点';
this.searchResults = [];
this.selected = this.buildInitialSelection();
this.status = this.selected.longitude
? this.selected.address || '已选点,可确认回填'
: '可搜索地址或点击地图选点';
}
},
},
@@ -78,6 +90,32 @@ export default {
}
},
methods: {
hasCoordinateValue(value) {
return value !== '' && value !== null && value !== undefined;
},
buildInitialSelection() {
// Number('') === 0,空字符串不能当有效坐标,否则会落到 [0,0](非洲附近)
if (!this.hasCoordinateValue(this.longitude) || !this.hasCoordinateValue(this.latitude)) {
return {};
}
const longitude = Number(this.longitude);
const latitude = Number(this.latitude);
if (
!Number.isFinite(longitude) ||
!Number.isFinite(latitude) ||
longitude < -180 ||
longitude > 180 ||
latitude < -90 ||
latitude > 90
) {
return {};
}
return {
longitude,
latitude,
address: this.address || '',
};
},
loadAmap() {
if (window.AMap && window.AMap.Map) {
return Promise.resolve();
@@ -96,25 +134,53 @@ export default {
return amapLoader;
},
initMap() {
this.loadAmap()
if (this.mapInitializing) return this.mapInitializing;
this.mapInitializing = this.loadAmap()
.then(() => {
this.$nextTick(() => {
if (!this.amap) {
this.amap = new window.AMap.Map(this.$refs.map, {
center: [116.40769, 39.89945],
zoom: 11,
});
this.amap.on('click', event => this.pickPoint(event.lnglat));
} else {
this.amap.resize();
this.clearMarker();
}
if (!window.AMap?.Map) {
throw new Error('高德地图组件未就绪');
}
return new Promise(resolve => {
this.$nextTick(() => {
if (!this.$refs.map) return resolve(null);
const initial = this.buildInitialSelection();
const hasPoint =
Number.isFinite(initial.longitude) && Number.isFinite(initial.latitude);
const center = hasPoint
? [initial.longitude, initial.latitude]
: [116.40769, 39.89945];
const zoom = hasPoint ? 14 : 11;
if (!this.amap) {
this.amap = new window.AMap.Map(this.$refs.map, {
center,
zoom,
});
this.amap.on('click', event => this.pickPoint(event.lnglat));
} else {
this.amap.resize();
this.clearMarker();
this.amap.setZoomAndCenter(zoom, center);
}
if (hasPoint) {
const point = new window.AMap.LngLat(initial.longitude, initial.latitude);
this.renderMarker(point);
this.selected = { ...initial };
this.status = initial.address || '已选点,可确认回填';
}
resolve(this.amap);
});
});
})
.catch(() => {
this.$message.error('高德地图组件加载失败,请稍后重试');
this.visible = false;
return null;
})
.finally(() => {
this.mapInitializing = null;
});
return this.mapInitializing;
},
ensureGeocoder() {
if (this.geocoder) return Promise.resolve(this.geocoder);
@@ -154,15 +220,22 @@ export default {
this.$message.warning('请输入地址关键词');
return;
}
if (this.searching) return;
this.searchResults = [];
this.searching = true;
this.loadAmap()
.then(() => this.runGeocode('location', keyword))
.then(result => {
const point = result.geocodes?.[0]?.location || result.location;
if (!point) {
this.initMap()
.then(map => (map ? searchAmapPlaceText(keyword) : null))
.then(pois => {
if (!pois) return;
this.searchResults = normalizeMapSearchResults(pois, keyword);
const first = this.searchResults[0];
if (!first?.location) {
throw new Error('未找到匹配地址');
}
this.pickPoint(point, keyword);
return this.$nextTick().then(() => {
this.amap?.resize();
this.pickPoint(first.location, first.address || first.name || keyword);
});
})
.catch(() => {
this.status = '未找到匹配地址';
@@ -172,16 +245,27 @@ export default {
this.searching = false;
});
},
selectSearchResult(item) {
if (item && item.location) {
this.pickPoint(item.location, item.address || item.name || '');
}
},
pickPoint(point, fallbackAddress = '') {
const longitude = typeof point.getLng === 'function' ? point.getLng() : point.lng;
const latitude = typeof point.getLat === 'function' ? point.getLat() : point.lat;
const longitude =
typeof point.getLng === 'function' ? point.getLng() : point.lng ?? point.longitude;
const latitude =
typeof point.getLat === 'function' ? point.getLat() : point.lat ?? point.latitude;
if (!Number.isFinite(Number(longitude)) || !Number.isFinite(Number(latitude))) {
this.$message.warning('选点坐标无效');
return;
}
const lnglat = new window.AMap.LngLat(Number(longitude), Number(latitude));
this.renderMarker(lnglat);
this.selected = { longitude: Number(longitude), latitude: Number(latitude), address: fallbackAddress };
this.selected = {
longitude: Number(longitude),
latitude: Number(latitude),
address: fallbackAddress,
};
this.status = '正在解析地址...';
this.resolving = true;
this.runGeocode('address', lnglat)
@@ -214,11 +298,19 @@ export default {
}
},
confirm() {
if (!this.selected.longitude || !this.selected.latitude) {
this.$message.warning('请先搜索或点击地图完成选点');
return;
}
if (!this.selected.address) {
this.$message.warning('请等待地址解析完成后再确认');
return;
}
this.$emit('confirm', this.selected.address);
this.$emit('confirm', {
address: this.selected.address,
longitude: this.selected.longitude,
latitude: this.selected.latitude,
});
this.visible = false;
},
},
@@ -239,10 +331,27 @@ export default {
&__map {
width: 100%;
min-width: 0;
height: 420px;
border: 1px solid #eff1f7;
}
&__content {
position: relative;
display: block;
min-width: 0;
overflow: visible;
.address-map-picker__map {
position: relative;
z-index: 1;
}
:deep(.map-search-results) {
z-index: 2000;
}
}
&__info {
margin-top: 10px;
color: #606266;
@@ -0,0 +1,108 @@
<template>
<el-dialog
:model-value="modelValue"
title="变更记录详情"
append-to-body
destroy-on-close
align-center
width="1100px"
class="change-record-detail-dialog"
@update:model-value="$emit('update:modelValue', $event)"
@open="resetPage"
>
<el-table
:data="pageRows"
border
max-height="60vh"
:show-overflow-tooltip="false"
>
<el-table-column prop="field" label="变更字段" min-width="180" />
<el-table-column
prop="before"
label="变更前"
min-width="360"
class-name="change-record-detail-value"
/>
<el-table-column
prop="after"
label="变更后"
min-width="500"
class-name="change-record-detail-value"
/>
</el-table>
<el-empty v-if="!total" description="暂无变更内容" :image-size="60" />
<div class="change-record-detail-dialog__pager">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
background
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
/>
</div>
<template #footer>
<el-button type="primary" @click="$emit('update:modelValue', false)">关闭</el-button>
</template>
</el-dialog>
</template>
<script>
export default {
name: 'ChangeRecordDetailDialog',
props: {
modelValue: {
type: Boolean,
default: false,
},
rows: {
type: Array,
default: () => [],
},
},
emits: ['update:modelValue'],
data() {
return {
currentPage: 1,
pageSize: 10,
};
},
computed: {
total() {
return this.rows.length;
},
pageRows() {
const start = (this.currentPage - 1) * this.pageSize;
return this.rows.slice(start, start + this.pageSize);
},
},
watch: {
rows() {
const maxPage = Math.max(1, Math.ceil(this.total / this.pageSize) || 1);
if (this.currentPage > maxPage) this.currentPage = 1;
},
},
methods: {
resetPage() {
this.currentPage = 1;
this.pageSize = 10;
},
},
};
</script>
<style lang="scss" scoped>
.change-record-detail-dialog__pager {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
:deep(.change-record-detail-value .cell) {
white-space: pre-wrap;
word-break: break-word;
overflow: visible;
text-overflow: clip;
line-height: 1.6;
}
</style>
+1 -1
View File
@@ -576,7 +576,7 @@ export default {
}
.mode-content :deep(.el-form-item) {
margin-bottom: 18px;
margin-bottom: 14px;
}
.mode-content :deep(.el-form-item:last-child) {
@@ -0,0 +1,434 @@
<template>
<el-dialog v-model="visible" :title="title" width="92%" append-to-body destroy-on-close @open="handleOpen">
<el-form :model="query" inline label-width="130px" @submit.prevent>
<el-form-item label="发票号码">
<el-input v-model="query.invoiceNo" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="开票日期">
<el-date-picker
v-model="query.invoiceDateRange"
type="daterange"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item label="开票单位">
<el-input v-model="query.salerName" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="受票单位">
<el-input v-model="query.buyerName" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="开票单位(销方)">
<el-select
v-model="query.salerTaxNo"
clearable
filterable
remote
reserve-keyword
:remote-method="keyword => searchCustomers(keyword, 'saler')"
:loading="customerLoading.saler"
placeholder="输入名称搜索或直接粘贴税号"
style="width: 220px"
@keyup.enter="handleQuery"
>
<el-option
v-for="item in customerOptions.saler"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="受票单位(购方)">
<el-select
v-model="query.buyerTaxNo"
clearable
filterable
remote
reserve-keyword
:remote-method="keyword => searchCustomers(keyword, 'buyer')"
:loading="customerLoading.buyer"
placeholder="输入名称搜索或直接粘贴税号"
style="width: 220px"
@keyup.enter="handleQuery"
>
<el-option
v-for="item in customerOptions.buyer"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="发票类型">
<el-select
v-model="query.invoiceType"
clearable
filterable
placeholder="不限"
style="width: 230px"
>
<el-option
v-for="option in invoiceTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
<el-form-item label="认领状态">
<el-select v-model="query.claimed" clearable placeholder="不限" style="width: 120px">
<el-option label="未认领" :value="false" />
<el-option label="已认领" :value="true" />
</el-select>
</el-form-item>
<el-form-item label="登记状态">
<el-select v-model="query.registered" clearable placeholder="不限" style="width: 120px">
<el-option label="未登记" :value="false" />
<el-option label="已登记" :value="true" />
</el-select>
</el-form-item>
<el-form-item>
<el-button :disabled="loading" @click="resetQuery">重置</el-button>
<el-button type="primary" :disabled="loading" @click="handleQuery">查询</el-button>
</el-form-item>
</el-form>
<el-table
ref="poolTable"
v-loading="loading"
:data="rows"
:row-key="rowKey"
border
height="520"
:highlight-current-row="!multiple"
@selection-change="selection => (selected = selection)"
@row-dblclick="handleRowDblclick"
>
<el-table-column v-if="multiple" type="selection" width="52" fixed="left" align="center" reserve-selection />
<el-table-column v-else width="52" align="center" fixed="left">
<template #default="{ row }">
<el-radio
:model-value="current && current.invoiceNo"
:value="row.invoiceNo"
@change="current = row"
>
<span></span>
</el-radio>
</template>
</el-table-column>
<el-table-column prop="invoiceNo" label="发票号码" min-width="180" />
<el-table-column prop="invoiceDate" label="开票日期" min-width="120" />
<el-table-column prop="invoiceType" label="发票类型" min-width="140" />
<el-table-column prop="issuerName" label="开票单位" min-width="180" show-overflow-tooltip />
<el-table-column prop="receiverName" label="受票单位" min-width="180" show-overflow-tooltip />
<el-table-column label="金额(含税)" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
</el-table-column>
<el-table-column label="税率" min-width="90" align="center">
<template #default="{ row }">{{ formatRate(row.taxRate) }}</template>
</el-table-column>
<el-table-column label="税额" min-width="120" align="right">
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
</el-table-column>
<el-table-column label="剩余可用金额" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.availableTaxAmount) }}</template>
</el-table-column>
<!--<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="210" show-overflow-tooltip />-->
<el-table-column label="认领状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.claimed" type="danger" size="small">已认领</el-tag>
<el-tag v-else type="info" size="small">未认领</el-tag>
</template>
</el-table-column>
<el-table-column label="登记状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.registered" type="warning" size="small">已登记</el-tag>
<el-tag v-else type="info" size="small">未登记</el-tag>
</template>
</el-table-column>
<el-table-column label="附件" width="120" fixed="right" align="center">
<template #default="{ row }">
<el-link
type="primary"
:underline="false"
:disabled="attachmentBusy === row.invoiceNo"
@click.stop="viewAttachment(row)"
>查看</el-link
>
<el-link
type="primary"
:underline="false"
:disabled="attachmentBusy === row.invoiceNo"
@click.stop="downloadAttachment(row)"
>下载</el-link
>
</template>
</el-table-column>
<el-table-column v-if="!multiple" label="操作" width="90" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click.stop="confirmRows([row])">选择</el-link>
</template>
</el-table-column>
</el-table>
<div class="kingdee-pool-dialog__pager">
<span class="kingdee-pool-dialog__pager-tip">按名称检索范围为已同步票,未找到请改用发票号码或税号精确查找</span>
<el-pagination
v-model:current-page="page.current"
v-model:page-size="page.size"
:total="page.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:disabled="loading"
@current-change="load"
@size-change="handleSizeChange"
/>
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleConfirm">{{ confirmText }}</el-button>
</template>
</el-dialog>
</template>
<script>
import request from '@/axios';
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
import {
downloadKingdeeInvoiceFile,
isKingdeePathAttachment,
viewKingdeeInvoiceFile,
} from '@/utils/kingdee-attachment';
export default {
name: 'KingdeeInvoicePoolDialog',
props: {
modelValue: { type: Boolean, required: true },
title: { type: String, default: '查询金蝶票据池' },
confirmText: { type: String, default: '确定' },
multiple: { type: Boolean, default: false },
initialInvoiceNo: { type: String, default: '' },
queryApi: { type: Function, required: true },
},
emits: ['update:modelValue', 'confirm'],
data() {
return {
loading: false,
// 与后端 BfiConstant.INVOICE_TYPE_NAMES 对齐:value 为发票类型编码(镜像按 invoice_type_code 精确匹配)
// 选票场景只开放数电票(普票/专票)
invoiceTypeOptions: [
{ value: '26', label: '数电发票(普通发票)' },
{ value: '27', label: '数电发票(增值税专用发票)' },
],
rows: [],
selected: [],
current: null,
page: { current: 1, size: 10, total: 0 },
// 正在查看/下载附件的发票号(行级 loading 守卫)
attachmentBusy: null,
// 销/购方客商下拉:远程按名称搜客商档案,选中带出税号查询;手动输入的裸税号直接作为值过滤
customerOptions: { saler: [], buyer: [] },
customerLoading: { saler: false, buyer: false },
query: this.emptyQuery(),
};
},
computed: {
visible: {
get() {
return this.modelValue;
},
set(value) {
this.$emit('update:modelValue', value);
},
},
},
methods: {
emptyQuery() {
return {
invoiceNo: '',
// 默认查近一个月,随请求传后端并在页面回显
invoiceDateRange: [this.$dayjs().subtract(1, 'month').format('YYYY-MM-DD'), this.$dayjs().format('YYYY-MM-DD')],
salerName: '',
buyerName: '',
salerTaxNo: '',
buyerTaxNo: '',
invoiceType: '',
claimed: null,
registered: null,
};
},
// 镜像行主键为 id,回源入库前的响应仍可能无 id,用发票号码兜底
rowKey(row) {
return row.id || row.invoiceNo;
},
// 票池行附件字段为复数 attachmentsJson,行内首项即发票文件({source,path,name})
rowAttachment(row = {}) {
const value = row.attachmentsJson;
if (!value) return null;
let parsed = value;
if (typeof value === 'string') {
try {
parsed = JSON.parse(value);
} catch {
return null;
}
}
return Array.isArray(parsed) ? parsed[0] || null : parsed || null;
},
attachmentName(row) {
const attachment = this.rowAttachment(row);
return attachment?.name || `${row.invoiceNo || '发票'}.pdf`;
},
async viewAttachment(row) {
const attachment = this.rowAttachment(row);
if (!isKingdeePathAttachment(attachment)) {
this.$message.warning('当前发票暂无可查看附件');
return;
}
this.attachmentBusy = row.invoiceNo;
try {
await viewKingdeeInvoiceFile(row.invoiceNo, this.attachmentName(row));
} finally {
this.attachmentBusy = null;
}
},
async downloadAttachment(row) {
const attachment = this.rowAttachment(row);
if (!isKingdeePathAttachment(attachment)) {
this.$message.warning('当前发票暂无可下载附件');
return;
}
this.attachmentBusy = row.invoiceNo;
try {
await downloadKingdeeInvoiceFile(row.invoiceNo, this.attachmentName(row));
} finally {
this.attachmentBusy = null;
}
},
handleOpen() {
this.query = this.emptyQuery();
this.query.invoiceNo = this.initialInvoiceNo || '';
this.current = null;
this.selected = [];
this.$refs.poolTable?.clearSelection();
this.page.current = 1;
this.load();
},
handleQuery() {
this.page.current = 1;
this.load();
},
handleSizeChange() {
this.page.current = 1;
this.load();
},
resetQuery() {
this.query = this.emptyQuery();
this.handleQuery();
},
// 客商名称远程搜索:含 18 位社会信用代码特征(数字+大写字母)时按裸税号直查;
// 否则搜客商档案,取客商发票信息(blade_customer_invoice_info)的抬头/税号作选项——票池税号是开票税号,非统一信用代码
async searchCustomers(keyword, side) {
const text = String(keyword || '').trim();
if (!text) return;
if (/^[0-9A-Z]{15,20}$/.test(text)) {
this.customerOptions[side] = [{ label: `${text}(手输税号)`, value: text }];
return;
}
this.customerLoading[side] = true;
try {
const data = this.unwrap(await getCustomerList(1, 20, { fullName: text }));
const customers = (data.records || []).filter(item => item.id);
if (!customers.length) {
this.customerOptions[side] = [];
return;
}
// 列表接口不带发票信息,逐户取 detail(detail 组装 invoices,每户一张发票信息)
const options = await Promise.all(
customers.map(async customer => {
const detail = this.unwrap(await request({ url: '/blade-transport/customer-archive/detail', method: 'get', params: { id: customer.id } }));
return (detail?.invoices || [])
.filter(invoice => invoice.taxNo)
.map(invoice => ({ label: `${invoice.invoiceTitle || customer.fullName}(${invoice.taxNo})`, value: invoice.taxNo }));
})
);
this.customerOptions[side] = options.flat().filter(
(option, index, list) => list.findIndex(item => item.value === option.value) === index
);
} finally {
this.customerLoading[side] = false;
}
},
unwrap(response) {
const body = response?.data || response || {};
return body?.data || body;
},
async load() {
this.loading = true;
try {
const range = this.query.invoiceDateRange || [];
const data = this.unwrap(
await this.queryApi({
current: this.page.current,
size: this.page.size,
invoiceNo: this.query.invoiceNo || undefined,
invoiceDateStart: range[0] || undefined,
invoiceDateEnd: range[1] || undefined,
salerName: this.query.salerName || undefined,
buyerName: this.query.buyerName || undefined,
salerTaxNo: this.query.salerTaxNo || undefined,
buyerTaxNo: this.query.buyerTaxNo || undefined,
invoiceType: this.query.invoiceType || undefined,
claimed: this.query.claimed ?? undefined,
registered: this.query.registered ?? undefined,
})
);
this.rows = data.records || [];
this.page.total = Number(data.total || 0);
} finally {
this.loading = false;
}
},
handleRowDblclick(row) {
if (!this.multiple) {
this.confirmRows([row]);
}
},
handleConfirm() {
const picked = this.multiple ? this.selected : this.current ? [this.current] : [];
this.confirmRows(picked);
},
confirmRows(rows) {
if (!rows.length) {
this.$message.warning(this.multiple ? '请至少选择一条发票' : '请选择一张发票');
return;
}
this.$emit('confirm', rows);
this.visible = false;
},
formatMoney(value) {
const number = Number(value);
return Number.isFinite(number) ? number.toFixed(2) : '-';
},
formatRate(value) {
const number = Number(value);
return Number.isFinite(number) ? `${number}%` : '-';
},
},
};
</script>
<style lang="scss" scoped>
.kingdee-pool-dialog__pager {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12px;
gap: 16px;
}
.kingdee-pool-dialog__pager-tip {
color: #909399;
font-size: 12px;
}
</style>
+224
View File
@@ -0,0 +1,224 @@
<template>
<div v-if="results.length" class="map-search-results">
<div class="map-search-results__list">
<div
v-for="(item, index) in pageResults"
:key="item.id || `${currentPage}-${index}`"
class="map-search-results__item"
:class="{ 'is-active': isActive(item, index) }"
@click="handleSelect(item, index)"
>
<img
v-if="item.photo || item.image"
class="map-search-results__thumb"
:src="item.photo || item.image"
alt=""
/>
<div class="map-search-results__body">
<div class="map-search-results__name">
{{ item.name || item.address || '未命名地址' }}
</div>
<div class="map-search-results__address">
地址:{{ item.address || item.name || '-' }}
</div>
</div>
</div>
</div>
<div v-if="totalPages > 1" class="map-search-results__pager">
<button
v-for="page in visiblePages"
:key="page"
type="button"
class="map-search-results__page"
:class="{ 'is-active': page === currentPage }"
@click="currentPage = page"
>
{{ page }}
</button>
<button
type="button"
class="map-search-results__page map-search-results__page--next"
:disabled="currentPage >= totalPages"
@click="goNext"
>
下一页
</button>
</div>
</div>
</template>
<script>
const PAGE_SIZE = 5;
export default {
props: {
results: {
type: Array,
default: () => [],
},
},
emits: ['select'],
data() {
return {
currentPage: 1,
activeKey: '',
};
},
computed: {
totalPages() {
return Math.max(1, Math.ceil((this.results || []).length / PAGE_SIZE));
},
pageResults() {
const start = (this.currentPage - 1) * PAGE_SIZE;
return (this.results || []).slice(start, start + PAGE_SIZE);
},
visiblePages() {
const maxButtons = 3;
let start = Math.max(1, this.currentPage - 1);
let end = Math.min(this.totalPages, start + maxButtons - 1);
start = Math.max(1, end - maxButtons + 1);
const pages = [];
for (let page = start; page <= end; page += 1) {
pages.push(page);
}
return pages;
},
},
watch: {
results() {
this.currentPage = 1;
this.activeKey = '';
},
},
methods: {
resultKey(item, index) {
return item?.id ?? `${this.currentPage}-${index}`;
},
isActive(item, index) {
return this.activeKey === this.resultKey(item, index);
},
handleSelect(item, index) {
this.activeKey = this.resultKey(item, index);
this.$emit('select', item);
},
goNext() {
if (this.currentPage < this.totalPages) {
this.currentPage += 1;
}
},
},
};
</script>
<style lang="scss" scoped>
.map-search-results {
position: absolute;
top: 0;
left: 0;
z-index: 2000;
display: flex;
flex-direction: column;
width: 360px;
max-height: 360px;
overflow: hidden;
background: #fff;
border: 1px solid #dcdfe6;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
pointer-events: auto;
&__list {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
}
&__item {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 10px 12px;
cursor: pointer;
border-bottom: 1px solid #ebeef5;
}
&__item:hover,
&__item.is-active {
background: #f5f5f5;
}
&__thumb {
flex: 0 0 48px;
width: 48px;
height: 48px;
object-fit: cover;
border: 1px solid #ebeef5;
background: #fafafa;
}
&__body {
flex: 1 1 auto;
min-width: 0;
}
&__name {
overflow: hidden;
color: #303133;
font-size: 14px;
font-weight: 600;
line-height: 1.4;
white-space: nowrap;
text-overflow: ellipsis;
}
&__address {
margin-top: 4px;
color: #909399;
font-size: 12px;
line-height: 1.5;
word-break: break-all;
}
&__pager {
display: flex;
flex: 0 0 auto;
flex-wrap: wrap;
gap: 6px;
align-items: center;
padding: 8px 10px;
border-top: 1px solid #ebeef5;
background: #fff;
}
&__page {
min-width: 28px;
height: 28px;
padding: 0 8px;
color: #606266;
font-size: 13px;
line-height: 26px;
text-align: center;
background: #fff;
border: 1px solid #dcdfe6;
cursor: pointer;
&:hover:not(:disabled) {
color: #409eff;
border-color: #409eff;
}
&.is-active {
color: #409eff;
border-color: #409eff;
}
&:disabled {
color: #c0c4cc;
cursor: not-allowed;
}
&--next {
min-width: 56px;
}
}
}
</style>
+81
View File
@@ -0,0 +1,81 @@
<template>
<div v-loading="loading" class="pdf-preview">
<el-empty v-if="error" description="PDF预览失败" :image-size="72" />
<vue-pdf-embed
v-else-if="source"
:key="source"
:source="source"
annotation-layer
text-layer
@loaded="handleLoaded"
@loading-failed="handleError"
@rendering-failed="handleError"
/>
</div>
</template>
<script>
import { defineAsyncComponent } from 'vue';
import 'vue-pdf-embed/dist/styles/annotationLayer.css';
import 'vue-pdf-embed/dist/styles/textLayer.css';
const VuePdfEmbed = defineAsyncComponent(() => import('vue-pdf-embed'));
export default {
name: 'PdfPreview',
components: { VuePdfEmbed },
props: {
source: {
type: [String, Object],
default: '',
},
},
emits: ['error', 'loaded'],
data() {
return {
loading: true,
error: false,
};
},
watch: {
source: {
handler() {
this.loading = true;
this.error = false;
},
immediate: true,
},
},
methods: {
handleLoaded(pdf) {
this.loading = false;
this.$emit('loaded', pdf);
},
handleError(error) {
this.loading = false;
this.error = true;
this.$emit('error', error);
},
},
};
</script>
<style lang="scss" scoped>
.pdf-preview {
width: 100%;
height: 72vh;
overflow: auto;
background-color: #f5f7fa;
:deep(.vue-pdf-embed) {
width: min(100%, 1200px);
margin: 0 auto;
}
:deep(.vue-pdf-embed__page) {
margin-bottom: 12px;
background-color: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
}
}
</style>
@@ -0,0 +1,464 @@
<template>
<div class="vehicle-attachment-table">
<div class="vehicle-attachment-table__toolbar">
<el-button type="primary" :disabled="!rows.length" @click="batchDownload">
批量下载
</el-button>
</div>
<el-table :data="rows" border @selection-change="selectedRows = $event">
<el-table-column v-if="!readonly" type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="文件名" min-width="240" show-overflow-tooltip>
<template #default="{ row }">
<span
class="vehicle-attachment-table__file-name"
role="button"
tabindex="0"
@click="previewAttachment(row)"
@keydown.enter="previewAttachment(row)"
@keydown.space.prevent="previewAttachment(row)"
>
{{ attachmentName(row) }}
</span>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220">
<template #default="{ row, $index }">
<span v-if="readonly">{{ row.description || '-' }}</span>
<el-input
v-else
:model-value="row.description"
maxlength="200"
placeholder="请输入"
@update:model-value="value => updateDescription($index, value)"
/>
</template>
</el-table-column>
<el-table-column label="文件大小" width="120" align="center">
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
</el-table-column>
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
<el-table-column prop="uploadTime" label="上传时间" width="180" align="center" sortable />
<el-table-column
v-if="!readonly || showViewActions"
label="操作"
:width="readonly ? 120 : 170"
align="center"
fixed="right"
>
<template #default="{ row, $index }">
<div class="vehicle-attachment-table__actions">
<el-link type="primary" :disabled="kingdeeBusy" @click="previewAttachment(row)"
>查看</el-link
>
<el-link type="primary" :disabled="kingdeeBusy" @click="downloadAttachment(row)"
>下载</el-link
>
<el-link v-if="!readonly" type="danger" @click="removeAttachment($index)">删除</el-link>
</div>
</template>
</el-table-column>
</el-table>
<div v-if="!readonly" class="vehicle-attachment-table__upload">
<vehicle-attachment-upload
:model-value="rows"
:file-types="attachmentFileTypes"
:max-size="maxSize"
:show-tip="true"
:tip="`支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过${maxSize}MB`"
:show-file-list="false"
button-text="上传附件"
@update:model-value="handleUploadChange"
/>
</div>
<el-dialog
v-model="documentPreviewVisible"
:title="previewFile.name || '附件预览'"
append-to-body
destroy-on-close
width="90%"
top="4vh"
class="vehicle-attachment-table__viewer-dialog"
@closed="handlePreviewClosed"
>
<pdf-preview
v-if="documentPreviewVisible && previewFile.url && previewFile.isPdf"
:source="previewFile.url"
@error="handlePreviewError"
/>
<open-file-viewer
v-else-if="documentPreviewVisible && previewFile.url"
:file="previewFile.url"
:file-name="previewFile.name"
:mime-type="previewFile.mimeType"
width="100%"
height="72vh"
fit="contain"
theme="auto"
locale="zh-CN"
:toolbar="viewerToolbar"
:plugins="viewerPlugins"
@unsupported="handlePreviewUnsupported"
@error="handlePreviewError"
/>
</el-dialog>
<el-image-viewer
v-if="imagePreviewVisible"
:url-list="imagePreviewUrls"
:initial-index="imagePreviewIndex"
@close="imagePreviewVisible = false"
/>
</div>
</template>
<script>
import { ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue';
import { fallbackPlugin, imagePlugin, officePlugin, textPlugin } from '@open-file-viewer/core';
import '@open-file-viewer/core/style.css';
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
import PdfPreview from '@/components/pdf-preview/main.vue';
import { downloadFileByUrl } from '@/utils/util';
import {
downloadKingdeeInvoiceFile,
isKingdeePathAttachment,
requestKingdeeFile,
} from '@/utils/kingdee-attachment';
const attachmentFileTypes = [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
];
const viewerPlugins = [
imagePlugin(),
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
textPlugin(),
fallbackPlugin(),
];
export default {
name: 'VehicleAttachmentTable',
components: {
ElImageViewer,
OpenFileViewer,
PdfPreview,
},
props: {
modelValue: {
type: [Array, String, Object],
default: () => [],
},
readonly: {
type: Boolean,
default: false,
},
maxSize: {
type: Number,
default: 500,
},
// 金蝶侧附件(piaozone path 型)代理下载所依赖的发票号;行内自带 invoiceNo 时优先用行内值
kingdeeInvoiceNo: {
type: String,
default: '',
},
// 查看态也展示「查看/下载」操作列(编辑态始终展示,含删除)
showViewActions: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue'],
data() {
return {
attachmentFileTypes,
selectedRows: [],
imagePreviewVisible: false,
imagePreviewUrls: [],
imagePreviewIndex: 0,
documentPreviewVisible: false,
previewFile: {},
// 金蝶侧附件代理请求进行中(行级按钮共享 loading 守卫,防重复触发)
kingdeeBusy: false,
// 金蝶附件预览用的 objectURL,弹窗关闭时回收
kingdeeObjectUrl: '',
viewerPlugins,
viewerToolbar: {
download: true,
fullscreen: true,
print: true,
rotate: true,
zoom: true,
},
};
},
computed: {
rows() {
if (Array.isArray(this.modelValue)) return this.modelValue;
if (this.modelValue && typeof this.modelValue === 'object') return [this.modelValue];
if (!this.modelValue) return [];
try {
const rows = JSON.parse(this.modelValue);
return Array.isArray(rows) ? rows : [];
} catch (error) {
return String(this.modelValue)
.split(',')
.map(item => item.trim())
.filter(Boolean)
.map(item => ({ name: item, url: item }));
}
},
},
methods: {
attachmentName(row = {}) {
return row.originalName || row.name || row.fileName || '附件';
},
attachmentUrl(row = {}) {
return row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || row.src || '';
},
kingdeeInvoiceNoOf(row = {}) {
return row.invoiceNo || this.kingdeeInvoiceNo || '';
},
isKingdeeRow(row = {}) {
return isKingdeePathAttachment(row) && Boolean(this.kingdeeInvoiceNoOf(row));
},
attachmentExtension(row = {}) {
const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0];
const index = source.lastIndexOf('.');
return index > -1 ? source.slice(index + 1).toLowerCase() : '';
},
isImage(row) {
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(this.attachmentExtension(row));
},
isPdf(row) {
const mimeType = String(row.mimeType || row.contentType || '').toLowerCase();
return mimeType.includes('application/pdf') || this.attachmentExtension(row) === 'pdf';
},
emitRows(rows) {
this.$emit('update:modelValue', rows);
},
handleUploadChange(rows) {
const uploadUserName = this.$store.getters.userInfo?.realName || '';
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.emitRows(
(rows || []).map(row => ({
...row,
description: row.description || '',
uploadUserName: row.uploadUserName || uploadUserName,
uploadTime: row.uploadTime || uploadTime,
}))
);
},
updateDescription(index, description) {
const rows = this.rows.map((row, rowIndex) =>
rowIndex === index ? { ...row, description } : row
);
this.emitRows(rows);
},
removeAttachment(index) {
const rows = [...this.rows];
rows.splice(index, 1);
this.emitRows(rows);
},
formatFileSize(size) {
if (!size) return '';
const value = Number(size);
if (!Number.isFinite(value)) return String(size);
if (value < 1024) return `${value}B`;
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)}KB`;
return `${(value / 1024 / 1024).toFixed(1)}MB`;
},
downloadAttachment(row) {
// 金蝶侧附件(piaozone path 型)经后端代理 A801-2 下载,OSS 直链维持前端直连下载
if (this.isKingdeeRow(row)) {
return this.downloadKingdeeAttachment(row);
}
const url = this.attachmentUrl(row);
if (!url) {
this.$message.warning('附件地址为空,无法下载');
return;
}
downloadFileByUrl(url, this.attachmentName(row));
},
async downloadKingdeeAttachment(row) {
if (this.kingdeeBusy) return;
this.kingdeeBusy = true;
try {
await downloadKingdeeInvoiceFile(this.kingdeeInvoiceNoOf(row), this.attachmentName(row));
} finally {
this.kingdeeBusy = false;
}
},
batchDownload() {
const rows = this.selectedRows.length ? this.selectedRows : this.rows;
const kingdeeRows = rows.filter(row => this.isKingdeeRow(row));
const urlRows = rows.filter(row => !this.isKingdeeRow(row) && this.attachmentUrl(row));
if (!kingdeeRows.length && !urlRows.length) {
this.$message.warning(
this.selectedRows.length ? '所选附件暂无可下载地址' : '暂无可下载附件'
);
return;
}
urlRows.forEach((row, index) => {
window.setTimeout(() => this.downloadAttachment(row), index * 500);
});
// 金蝶侧附件走代理接口,串行触发避免并发重复回源
this.downloadKingdeeRows(kingdeeRows);
},
async downloadKingdeeRows(rows) {
if (this.kingdeeBusy) return;
this.kingdeeBusy = true;
try {
for (const row of rows) {
await downloadKingdeeInvoiceFile(this.kingdeeInvoiceNoOf(row), this.attachmentName(row));
}
} finally {
this.kingdeeBusy = false;
}
},
previewAttachment(row) {
// 金蝶侧附件经 A801-2 代理取流后进弹窗预览,OSS 直链维持原逻辑
if (this.isKingdeeRow(row)) {
return this.previewKingdeeAttachment(row);
}
const url = this.attachmentUrl(row);
if (!url) {
this.$message.warning('附件地址为空,无法预览');
return;
}
if (this.isImage(row)) {
this.imagePreviewUrls = this.rows
.filter(item => this.isImage(item) && this.attachmentUrl(item))
.map(item => this.attachmentUrl(item));
this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0);
this.imagePreviewVisible = true;
return;
}
const isPdf = this.isPdf(row);
this.previewFile = {
name: this.attachmentName(row),
url,
mimeType: isPdf ? 'application/pdf' : row.mimeType || row.contentType || '',
isPdf,
};
this.documentPreviewVisible = true;
},
async previewKingdeeAttachment(row) {
// OFD 版式文件浏览器无法在线渲染,直接提示下载,避免拉流后预览失败
if (this.attachmentExtension(row) === 'ofd') {
this.$message.warning('OFD 版式文件不支持在线预览,请使用下载获取后查看');
return;
}
if (this.kingdeeBusy) return;
this.kingdeeBusy = true;
try {
const blob = await requestKingdeeFile(this.kingdeeInvoiceNoOf(row));
this.releaseKingdeeObjectUrl();
const isPdf = this.isPdf(row) || String(blob.type || '').includes('application/pdf');
this.kingdeeObjectUrl = URL.createObjectURL(blob);
this.previewFile = {
name: this.attachmentName(row),
url: this.kingdeeObjectUrl,
mimeType: isPdf ? 'application/pdf' : blob.type || '',
isPdf,
};
this.documentPreviewVisible = true;
} catch (error) {
if (!error?.silent) this.$message.error(error?.message || '附件预览失败');
} finally {
this.kingdeeBusy = false;
}
},
handlePreviewClosed() {
this.releaseKingdeeObjectUrl();
this.previewFile = {};
},
releaseKingdeeObjectUrl() {
if (this.kingdeeObjectUrl) {
URL.revokeObjectURL(this.kingdeeObjectUrl);
this.kingdeeObjectUrl = '';
}
},
handlePreviewUnsupported() {
this.$message.warning('当前文件暂不支持在线预览');
},
handlePreviewError() {
this.$message.error('附件预览失败');
},
},
};
</script>
<style lang="scss" scoped>
.vehicle-attachment-table {
width: 100%;
&__toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 16px;
}
&__file-name {
color: #409eff;
cursor: pointer;
}
&__file-name:hover,
&__file-name:focus-visible {
text-decoration: underline;
}
&__actions {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
:deep(.el-link) {
font-size: 13px;
}
}
&__upload {
display: flex;
justify-content: flex-start;
margin-top: 12px;
:deep(.vehicle-attachment-upload) {
width: auto;
}
}
:deep(.el-table th.el-table__cell) {
height: 42px;
padding: 8px 0;
background: #fafafa;
}
:deep(.el-table td.el-table__cell) {
height: 52px;
padding: 8px 0;
}
}
:global(.vehicle-attachment-table__viewer-dialog .el-dialog__body) {
padding: 12px 16px 16px;
}
</style>
@@ -75,8 +75,8 @@ import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
import { baseUrl } from '@/config/env';
import { getUploadHeaders } from '@/utils/upload';
const UNIFIED_ATTACHMENT_TIP =
'支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过500M';
const UNIFIED_ATTACHMENT_TIP_PREFIX =
'支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过';
const MIME_MAP = {
jpg: 'image/jpeg',
@@ -212,10 +212,17 @@ export default {
.filter(Boolean);
},
acceptText() {
return this.acceptedList.join(',');
return this.acceptedList
.map(item => {
const value = String(item).trim();
if (!value || value.includes('/') || value.startsWith('.')) return value;
return `.${value}`;
})
.filter(Boolean)
.join(',');
},
tipText() {
return this.tip || UNIFIED_ATTACHMENT_TIP;
return this.tip || `${UNIFIED_ATTACHMENT_TIP_PREFIX}${this.maxSize}M`;
},
uploadingCount() {
return this.fileList.filter(file => ['ready', 'uploading'].includes(file.status)).length;
@@ -304,12 +311,22 @@ export default {
});
},
handleSuccess(response, file, files) {
if (!response || response.success === false || (response.code && response.code !== 200)) {
const code = response?.code;
const codeInvalid = code !== undefined && code !== null && code !== '' && Number(code) !== 200;
if (!response || response.success === false || codeInvalid) {
this.$message.error((response && response.msg) || '上传失败');
return;
}
this.emitUploadFiles(this.multiple ? files : [file]);
this.$emit('success', this.normalizeUploadFile(file));
if (file && !file.response) {
file.response = response;
}
const normalized = this.normalizeUploadFile(file, response);
if (!normalized.url) {
this.$message.error('上传成功但未返回文件地址');
return;
}
this.$emit('success', normalized);
this.emitUploadFiles(this.multiple ? files : [file], normalized);
this.$message.success('上传成功');
},
handleChange(file, files) {
@@ -323,30 +340,89 @@ export default {
this.emitUploadFiles(files);
return true;
},
emitUploadFiles(files) {
const list = files
.filter(file => file.status === 'success' || this.getFileUrl(file))
.map(this.normalizeUploadFile)
emitUploadFiles(files, preferredFile) {
const list = (files || [])
.filter(file => {
if (file?.status === 'fail') return false;
return (
file?.status === 'success' ||
this.getFileUrl(file) ||
file?.response?.data ||
file?.response?.link ||
file?.response
);
})
.map(file => this.normalizeUploadFile(file))
.filter(item => item.url);
this.$emit('update:modelValue', list);
this.$emit('change', list);
if (preferredFile?.url) {
const exists = list.some(
item =>
(preferredFile.uid && item.uid && String(item.uid) === String(preferredFile.uid)) ||
item.url === preferredFile.url
);
if (!exists) list.push(preferredFile);
}
const current = this.parseValue(this.modelValue).map(item => ({
...item,
url: this.getFileUrl(item),
}));
const merged = [...current];
list.forEach(file => {
const index = merged.findIndex(
item =>
(file.uid && item.uid && String(item.uid) === String(file.uid)) ||
(file.url && this.getFileUrl(item) === file.url)
);
if (index >= 0) merged.splice(index, 1, { ...merged[index], ...file });
else merged.push(file);
});
const result = merged.filter(item => this.getFileUrl(item));
this.$emit('update:modelValue', result);
this.$emit('change', result);
},
normalizeUploadFile(file) {
const data = (file.response && file.response.data) || file.data || file;
const url = data.link || data.url || data.domain || file.url || '';
normalizeUploadFile(file, responseOverride) {
const response = responseOverride || file?.response;
let data = {};
if (response && typeof response === 'object') {
if (response.data && typeof response.data === 'object') {
data = response.data;
} else if (response.link || response.url || response.domain) {
data = response;
}
}
if (!data.link && !data.url && !data.domain) {
data = file?.data && typeof file.data === 'object' ? file.data : file || {};
}
const url =
data.link ||
data.url ||
data.domain ||
data.fileLink ||
data.fileUrl ||
file?.url ||
file?.link ||
'';
const originalName =
data.originalName || file.originalName || data.name || file.name || this.getFileName(url) || '附件';
data.originalName ||
file?.originalName ||
file?.name ||
data.name ||
this.getFileName(url) ||
'附件';
const extension = this.getExtension({ name: originalName, url });
return {
...data,
uid: file.uid || data.uid,
uid: file?.uid || data.uid,
originalName,
name: originalName,
url,
link: data.link || url,
size: data.size || data.attachSize || file.size || '',
size: data.size || data.attachSize || file?.size || '',
extension,
mimeType: data.mimeType || data.contentType || file.raw?.type || MIME_MAP[extension] || '',
mimeType: data.mimeType || data.contentType || file?.raw?.type || MIME_MAP[extension] || '',
};
},
handlePreview(file) {
+1 -1
View File
@@ -11,7 +11,7 @@ export default {
tenantMode: true, // 是否开启租户模式
tenantId: '000000', // 管理组租户编号
captchaMode: true, // 是否开启验证码模式
captchaType: 'behavior', // 验证码类型(image:首屏图形验证码 behavior:登录时弹出点选/滑块/旋转随机行为验证)
captchaType: 'image', // 验证码类型(image:首屏图形验证码 behavior:登录时弹出点选/滑块/旋转随机行为验证)
switchMode: false, // 是否开启登录切换角色部门
lockPage: '/lock', // 锁屏页面地址
tokenTime: 3000, // 定时刷新token间隔(单位:毫秒)
+1
View File
@@ -1,6 +1,7 @@
FROM nginx
VOLUME /tmp
ENV LANG en_US.UTF-8
ADD ./src/docker/nginx.conf /etc/nginx/conf.d/default.conf
ADD ./dist/ /usr/share/nginx/html/
EXPOSE 80
EXPOSE 443
+19
View File
@@ -0,0 +1,19 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
location / {
add_header Cache-Control "no-store, no-cache, must-revalidate";
try_files $uri $uri/ /index.html;
}
}
+4
View File
@@ -1,8 +1,12 @@
import store from './store';
import { isChunkLoadError, reloadForChunkError } from './utils/chunk-reload';
export default {
install: app => {
app.config.errorHandler = (err, vm, info) => {
if (isChunkLoadError(err) && reloadForChunkError()) {
return;
}
store.commit('ADD_LOGS', {
type: 'error',
message: err.message,
+5 -1
View File
@@ -1,4 +1,5 @@
import { createApp } from 'vue';
import { installChunkReload } from './utils/chunk-reload';
import website from './config/website';
import axios from './axios';
import router from './router/';
@@ -43,8 +44,10 @@ import tenantPackage from './views/system/tenantpackage.vue';
import tenantDatasource from './views/system/tenantdatasource.vue';
// 通用弹窗分组白卡(灰底弹窗内使用,标题在卡内)
import sectionCard from './components/section-card/main.vue';
import mapSearchResults from './components/map-search-results/main.vue';
window.$crudCommon = crudCommon;
installChunkReload();
debug();
window.axios = axios;
const app = createApp(App);
@@ -66,6 +69,7 @@ app.component('formSetting', formSetting);
app.component('tenantPackage', tenantPackage);
app.component('tenantDatasource', tenantDatasource);
app.component('sectionCard', sectionCard);
app.component('mapSearchResults', mapSearchResults);
app.config.globalProperties.$app = app;
app.config.globalProperties.$dayjs = dayjs;
app.config.globalProperties.website = website;
@@ -81,7 +85,7 @@ app.use(ElementPlus, {
});
app.use(Avue, {
axios,
calcHeight: 10,
calcHeight: -26,
crudOption: {
searchIcon: true,
searchIndex: 4,
+173
View File
@@ -0,0 +1,173 @@
import { getDeptTree } from '@/api/system/dept';
import { ElCascader } from 'element-plus';
import { h } from 'vue';
/**
* 所属组织 统一搜索 mixin
* ------------------------------------------------------------------
* 全站「所属组织」搜索栏统一为级联选择器(el-cascader,单选),风格与客商档案
* (vehicle/customer-archive.vue)保持一致。
*
* 接入步骤(在使用了 avue-crud 的页面中):
* 1. 混入本 mixin:mixins: [organizationSearch]
* 2. option 中「所属组织」列加上 searchslot: true(prop 为 organizationName 或 deptName 均可)
* 3. 在 created / 初始化时调用 this.loadOrganizationOptions()
*
* 说明:
* - 自动识别 organizationName / deptName 两种 prop,并挂载 column.renderSearch
* - 树节点标识统一用 id,级联值为「根 → 选中节点」的 id 路径数组
* - checkStrictly 打开,任意层级(含非末级)都可直接选中
* - 页面若已有 excludeExternalOrganization / normalizeOrgTree 等方法,
* 组件自身方法优先级更高,会覆盖本 mixin 的默认实现
* - 搜索值需在各自的 searchChange / normalizeSearch 中按后端要求转换为部门名称:
* 级联值是路径数组,统一用 findOrgById(路径)?.rawLabel 取末级名称
* - 表单区(新增/编辑)需要回显时,可用 findOrgPath(deptId) 把 id 转成路径,
* 参考 customer-archive 的 deptCascaderValue 计算属性
*/
const ORG_PROPS = ['organizationName', 'deptName'];
export default {
data() {
return {
// 树形组织数据,仅供搜索栏 el-cascader 使用;
// 与页面自身 organizationOptions(多为表单扁平列表)隔离,避免相互覆盖
organizationTreeOptions: [],
organizationTreeFlatOptions: [],
organizationLoading: false,
};
},
computed: {
// 所属组织级联配置:checkStrictly 允许选中任意层级,不必选到末级
organizationCascaderProps() {
return {
value: 'id',
label: 'label',
children: 'children',
emitPath: true,
checkStrictly: true,
expandTrigger: 'click',
};
},
},
methods: {
loadOrganizationOptions() {
if (this.organizationLoading) return;
this.organizationLoading = true;
const tenantId = this.userInfo?.tenantId;
getDeptTree(tenantId)
.then(res => {
const source = res.data?.data || res.data || [];
const tree = this.excludeExternalOrganization
? this.excludeExternalOrganization(source)
: source;
this.organizationTreeOptions = this.normalizeOrgTree(tree);
this.organizationTreeFlatOptions = this.flattenOrgTree(this.organizationTreeOptions);
const option = this.tableOption || this.option;
if (!option) return;
ORG_PROPS.forEach(prop => {
const column = this.findColumn?.(option.column, prop);
if (column) {
column.renderSearch = scope => this.renderOrganizationSearch(scope, prop);
}
});
})
.finally(() => {
this.organizationLoading = false;
});
},
// 默认排除「外部组织」节点;页面可覆盖
excludeExternalOrganization(tree = []) {
return (tree || []).reduce((result, item) => {
const name = item.title || item.deptName || item.name || item.label || '';
if (String(name).trim() === '外部组织') return result;
result.push({
...item,
children: this.excludeExternalOrganization(item.children || []),
});
return result;
}, []);
},
normalizeOrgTree(tree = []) {
return (tree || []).map(item => {
const label = item.title || item.deptName || item.name || item.label || '';
const children = this.normalizeOrgTree(item.children || []);
return {
...item,
id: String(item.id),
label,
rawLabel: label,
children: children.length ? children : undefined,
};
});
},
flattenOrgTree(tree = []) {
return (tree || []).flatMap(item => [
{ id: String(item.id), label: item.label, rawLabel: item.rawLabel },
...this.flattenOrgTree(item.children || []),
]);
},
// 兼容两种入参:级联路径数组(取末级)或单个部门 id
findOrgById(id) {
const value = Array.isArray(id) ? id.at(-1) : id;
return this.organizationTreeFlatOptions.find(item => String(item.id) === String(value));
},
// 由部门 id 反查「根 → 该节点」的 id 路径,供级联回显(找不到返回 null)
findOrgPath(id, tree = this.organizationTreeOptions, parents = []) {
const target = Array.isArray(id) ? id.at(-1) : id;
if (target === undefined || target === null || target === '') return null;
for (const node of tree || []) {
const path = [...parents, String(node.id)];
if (String(node.id) === String(target)) return path;
const matched = this.findOrgPath(target, node.children, path);
if (matched) return matched;
}
return null;
},
// 由部门 id 取组织名称(末级),供表单提交
findOrgName(id) {
return this.findOrgById(id)?.rawLabel || '';
},
// 组织名称 → 级联路径(表单回显用;部门树异步加载完成后会自动重算)
resolveOrgPath(name) {
const value = String(name ?? '').trim();
if (!value) return [];
const node = this.organizationTreeFlatOptions.find(item => item.rawLabel === value);
return node ? this.findOrgPath(node.id) || [] : [];
},
// 级联路径 → 组织名称(表单/搜索提交用,取末级节点名称)
resolveOrgName(path) {
const value = Array.isArray(path)
? path.filter(item => item !== undefined && item !== null && item !== '')
: [];
return value.length ? this.findOrgById(value)?.rawLabel || '' : '';
},
// 搜索提交前把级联路径数组就地转成组织名称(后端按名称过滤)
normalizeOrganizationSearch(params, prop = 'organizationName') {
if (Array.isArray(params?.[prop])) {
params[prop] = this.resolveOrgName(params[prop]);
}
return params;
},
renderOrganizationSearch(scope, prop = 'organizationName') {
return h(ElCascader, {
modelValue: Array.isArray(scope.row?.[prop]) ? scope.row[prop] : [],
'onUpdate:modelValue': value => {
// 级联单选:modelValue 必须是 id 路径数组,否则控件内部状态会错乱导致无法再次选中
if (scope.row) scope.row[prop] = Array.isArray(value) ? value : [];
},
options: this.organizationTreeOptions,
props: {
label: 'label',
value: 'id',
children: 'children',
checkStrictly: true,
expandTrigger: 'click',
},
filterable: true,
clearable: true,
style: 'width: 100%',
placeholder: '请选择 所属组织',
});
},
},
};
+3
View File
@@ -26,6 +26,8 @@ export const getCargoTypeOption = ctx => ({
height: 'auto',
calcHeight: 22,
dialogWidth: 760,
// 货物类型弹窗单独使用样式,避免全局 Avue footer 上移遮挡底部备注字段
dialogCustomClass: 'cargo-type-dialog',
labelPosition: 'right',
labelWidth: 'auto',
tip: false,
@@ -172,6 +174,7 @@ export const getCargoTypeOption = ctx => ({
label: '备注',
prop: 'remark',
type: 'textarea',
formslot: true,
minRows: 2,
span: 24,
minWidth: 180,
+7
View File
@@ -146,6 +146,13 @@ export const createOption = () => ({
showWordLimit: true,
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
},
{
label: '创建人',
prop: 'createUserName',
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '更新时间',
prop: 'updateTime',
+148
View File
@@ -0,0 +1,148 @@
const dimensionOptions = [
{ label: '重量', value: '重量' },
{ label: '体积', value: '体积' },
{ label: '数量', value: '数量' },
];
const statusOptions = [
{ label: '启用', value: 1 },
{ label: '停用', value: 2 },
];
export const createOption = () => ({
height: 'auto',
calcHeight: 32,
dialogWidth: 680,
labelPosition: 'right',
labelWidth: 'auto',
tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
saveBtnText: '提交',
updateBtnText: '提交',
searchShow: true,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 4,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
indexWidth: 70,
addBtn: false,
viewBtn: false,
editBtn: false,
delBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 240,
menuFixed: 'right',
column: [
{
label: '计量单位编码',
prop: 'unitCode',
minWidth: 150,
span: 24,
search: true,
searchOrder: 4,
searchSpan: 6,
maxlength: 50,
showWordLimit: true,
rules: [
{ required: true, message: '请输入计量单位编码', trigger: 'blur' },
{ max: 50, message: '计量单位编码不能超过50个字', trigger: 'blur' },
],
},
{
label: '计量单位',
prop: 'unitName',
minWidth: 150,
search: true,
searchOrder: 3,
searchSpan: 6,
maxlength: 50,
showWordLimit: true,
rules: [
{ required: true, message: '请输入计量单位', trigger: 'blur' },
{ max: 50, message: '计量单位不能超过50个字', trigger: 'blur' },
],
},
{
label: '计量维度',
prop: 'dimension',
type: 'select',
minWidth: 130,
search: true,
searchOrder: 2,
searchSpan: 6,
dicData: dimensionOptions,
rules: [{ required: true, message: '请选择计量维度', trigger: 'change' }],
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
minRows: 2,
span: 24,
minWidth: 200,
maxlength: 200,
showWordLimit: true,
overHidden: true,
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
},
{
label: '状态',
prop: 'status',
type: 'select',
search: true,
searchOrder: 1,
searchSpan: 6,
slot: true,
dataType: 'number',
dicData: statusOptions,
clearable: true,
value: 1,
addDisplay: false,
editDisplay: false,
minWidth: 100,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
viewDisplay: false,
display: false,
},
{
label: '更新人',
prop: 'updateUserName',
addDisplay: false,
editDisplay: false,
viewDisplay: false,
display: false,
},
{
label: '创建时间',
prop: 'createTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
sortable: true,
addDisplay: false,
editDisplay: false,
display: false,
},
{
label: '更新时间',
prop: 'updateTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
sortable: true,
addDisplay: false,
editDisplay: false,
display: false,
},
],
});
+144 -118
View File
@@ -2,7 +2,6 @@ import {
auditColumns,
createCrudOption,
packageOptions,
priceUnitOptions,
textRule,
} from './common';
@@ -25,10 +24,23 @@ export const config = {
enableAllDept: false,
};
// 单价统一按两位小数展示;格式非法时保留原值,交由表单校验提示。
export const formatCargoValue = value => {
if (value === undefined || value === null) return '';
const raw = String(value).trim();
if (!raw || raw === '-1') return '';
if (!/^\d+(?:\.\d+)?$/.test(raw)) return raw;
const number = Number(raw);
return Number.isFinite(number) ? number.toFixed(2) : raw;
};
export const option = createCrudOption([
// ---------- 列表字段(顺序即表格展示顺序) ----------
{
label: '货物名称',
prop: 'cargoName',
order: 50,
span: 12,
minWidth: 150,
placeholder: '请输入',
rules: textRule('货物名称', 100, true),
@@ -37,6 +49,8 @@ export const option = createCrudOption([
label: '货物编号',
prop: 'cargoCode',
formslot: true,
order: 49,
span: 12,
minWidth: 120,
maxlength: 20,
showWordLimit: true,
@@ -49,22 +63,18 @@ export const option = createCrudOption([
label: '一级货物类型',
prop: 'firstCargoTypeName',
formslot: true,
order: 60,
span: 12,
minWidth: 150,
editDisabled: true,
rules: [{ required: true, message: '请选择一级货物类型', trigger: 'change' }],
},
{
label: '一级货物类型编码',
prop: 'firstCargoTypeCode',
hide: true,
display: false,
addDisplay: false,
editDisabled: true,
},
{
label: '二级货物类型',
prop: 'secondCargoTypeName',
formslot: true,
order: 59,
span: 12,
minWidth: 150,
editDisabled: true,
rules: [{ required: true, message: '请选择二级货物类型', trigger: 'change' }],
@@ -75,9 +85,132 @@ export const option = createCrudOption([
display: false,
addDisplay: false,
editDisabled: true,
minWidth: 180,
minWidth: 160,
},
// 搜索字段与表格、表单字段独立,避免筛选配置影响数据展示和编辑布局。
{
label: '包装',
prop: 'packageType',
type: 'select',
dicData: packageOptions,
order: 39,
span: 12,
minWidth: 100,
},
{
label: '品牌',
prop: 'brand',
search: false,
order: 40,
span: 12,
minWidth: 120,
placeholder: '请输入',
rules: textRule('品牌', 50),
},
{
label: '规格',
prop: 'specification',
order: 29,
span: 12,
minWidth: 120,
placeholder: '请输入',
rules: textRule('规格', 50),
},
{
label: '型号',
prop: 'model',
order: 19,
span: 12,
minWidth: 120,
placeholder: '请输入',
rules: textRule('型号', 50, true),
},
{
label: '单价',
prop: 'cargoValue',
formslot: true,
order: 30,
formatter: row => formatCargoValue(row.cargoValue),
minWidth: 120,
rules: [
{
validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback();
return;
}
if (!/^\d+(\.\d{1,2})?$/.test(String(value))) {
callback(new Error('单价只能输入数字,最多保留2位小数'));
return;
}
callback();
},
trigger: 'blur',
},
],
},
{
label: '计价单位',
prop: 'priceUnit',
type: 'select',
dicData: [],
display: false,
minWidth: 120,
},
{
label: '尺寸',
prop: 'sizeText',
minWidth: 120,
addDisplay: false,
editDisplay: false,
rules: textRule('尺寸', 100),
},
{
label: '说明',
prop: 'descriptionOne',
order: 20,
span: 12,
minWidth: 160,
placeholder: '请输入',
rules: textRule('说明', 100),
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
order: 10,
minRows: 2,
span: 24,
maxlength: 200,
showWordLimit: true,
placeholder: '请输入',
rules: textRule('备注', 200),
},
{
label: '组织',
prop: 'deptName',
minWidth: 150,
addDisplay: false,
editDisplay: false,
},
...auditColumns
.filter(column => ['createUserName', 'createTime', 'updateTime'].includes(column.prop))
.sort(
(first, second) =>
['createUserName', 'createTime', 'updateTime'].indexOf(first.prop) -
['createUserName', 'createTime', 'updateTime'].indexOf(second.prop)
),
// ---------- 表单隐藏字段 ----------
{
label: '一级货物类型编码',
prop: 'firstCargoTypeCode',
hide: true,
display: false,
addDisplay: false,
editDisabled: true,
},
// ---------- 搜索字段(与表格、表单独立) ----------
{
label: '货物名称',
prop: 'searchCargoName',
@@ -169,113 +302,6 @@ export const option = createCrudOption([
editDisplay: false,
viewDisplay: false,
},
{
label: '包装',
prop: 'packageType',
type: 'select',
dicData: packageOptions,
minWidth: 100,
},
{
label: '品牌',
prop: 'brand',
search: false,
minWidth: 120,
placeholder: '请输入',
rules: textRule('品牌', 50),
},
{
label: '规格',
prop: 'specification',
minWidth: 120,
placeholder: '请输入',
rules: textRule('规格', 50),
},
{
label: '型号',
prop: 'model',
minWidth: 120,
placeholder: '请输入',
rules: textRule('型号', 50),
},
{
label: '单价',
prop: 'cargoValue',
formslot: true,
formatter: row => (String(row.cargoValue) === '-1' ? '' : row.cargoValue),
minWidth: 120,
rules: [
{
validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback();
return;
}
if (!/^\d+(\.\d{1,2})?$/.test(String(value))) {
callback(new Error('货值只能输入数字,最多保留2位小数'));
return;
}
callback();
},
trigger: 'blur',
},
],
},
{
label: '计价单位',
prop: 'priceUnit',
type: 'select',
dicData: priceUnitOptions,
display: false,
minWidth: 120,
},
{
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: '备注',
prop: 'remark',
type: 'textarea',
minRows: 2,
span: 24,
maxlength: 200,
showWordLimit: true,
placeholder: '请输入',
rules: textRule('备注', 200),
},
{
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)
),
]);
// 搜索栏标签统一加宽,给较长的货物类型字段预留足够展示空间。
+2 -3
View File
@@ -281,14 +281,13 @@ export const option = {
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)
['createUserName', 'updateUserName', 'createTime', 'updateTime'].indexOf(first.prop) -
['createUserName', 'updateUserName', 'createTime', 'updateTime'].indexOf(second.prop)
),
]),
dialogCustomClass: 'avue-dialog--common-route',
+38 -2
View File
@@ -41,7 +41,7 @@ export const planStatusOptions = [
export const waybillStatusOptions = [
{ label: '草稿', value: 'draft' },
{ label: '待执行', value: 'pending' },
{ label: '进行中', value: 'processing' },
{ label: '进行中', value: 'running' },
{ label: '已完成', value: 'completed' },
{ label: '已取消', value: 'cancelled' },
];
@@ -49,7 +49,6 @@ export const waybillStatusOptions = [
export const dataSourceOptions = [
{ label: '手工创建', value: '手工创建' },
{ label: '批量导入', value: '批量导入' },
{ label: '模板生成', value: '模板生成' },
{ label: '计划调度', value: '计划调度' },
];
@@ -98,6 +97,11 @@ export const businessTypeOptions = [
{ label: '普通业务', value: '普通业务' },
];
export const businessModeOptions = [
{ label: '国内运输', value: '国内运输' },
{ label: '跨境运输', value: '跨境运输' },
];
export const settlementModeOptions = [
{ label: '先票后款', value: '先票后款' },
{ label: '先款后票', value: '先款后票' },
@@ -126,12 +130,18 @@ export const contractApprovalStatusOptions = [
{ label: '草稿', value: 'draft' },
{ label: '审批中', value: 'reviewing' },
{ label: '已驳回', value: 'rejected' },
{ label: '已撤回', value: 'withdrawn' },
{ label: '审批通过', value: 'approved' },
{ label: '变更审批中', value: 'change_reviewing' },
{ label: '变更驳回', value: 'change_rejected' },
{ label: '变更审批通过', value: 'change_approved' },
];
export const contractArchiveStatusOptions = [
{ label: '未归档', value: '未归档' },
{ label: '已归档', value: '已归档' },
];
export const nodeOptions = [
{ label: '接单', value: '接单' },
{ label: '到场', value: '到场' },
@@ -170,6 +180,32 @@ export const nonNegativeRule = label => ({
trigger: 'blur',
});
// 日期必须晚于当前日期(当天不可选):选择后立即校验,trigger 固定为 change
// 取值支持 YYYY-MM-DD / YYYY-MM-DD HH:mm:ss / 时间戳
export const futureDateRule = label => ({
validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback();
return;
}
const raw = String(value).trim().replace(/-/g, '/');
const selected = new Date(raw);
if (Number.isNaN(selected.getTime())) {
callback();
return;
}
const today = new Date();
today.setHours(0, 0, 0, 0);
selected.setHours(0, 0, 0, 0);
if (selected.getTime() <= today.getTime()) {
callback(new Error(`${label}必须晚于当前日期`));
return;
}
callback();
},
trigger: ['change', 'blur'],
});
export const phoneRule = label => ({
pattern: /^1[3-9]\d{9}$/,
message: `${label}格式不正确`,
+96 -19
View File
@@ -1,6 +1,7 @@
import {
auditColumns,
contractApprovalStatusOptions,
contractArchiveStatusOptions,
contractCategoryOptions,
contractStageOptions,
createCrudOption,
@@ -27,6 +28,12 @@ const listDicFormatter = res => {
return [];
};
const projectNameDicFormatter = res => {
const list = listDicFormatter(res);
// 过滤掉 projectName 为空的记录
return list.filter(item => item && item.projectName && String(item.projectName).trim());
};
export const config = {
title: '合同管理',
permission: 'contract_manage',
@@ -42,7 +49,7 @@ export const config = {
batchDelete: false,
enableProjectSelect: true,
projectQueryParams: {
approvalStatuses: 'approved,change_approved',
contractSelectable: true,
},
actions: ['copy'],
statusProp: 'approvalStatus',
@@ -65,6 +72,10 @@ export const config = {
['legalSealFlag', '是否需要加盖法人章'],
['copyCount', '一式(份)'],
['paymentDays', '回款账期(天)'],
['contractAmount', '合同金额'],
['templateFlag', '是否范本'],
['originalContractNo', '原件合同编号'],
['electronicSealFlag', '是否电子章'],
['remark', '备注', 2],
],
},
@@ -78,6 +89,7 @@ export const config = {
['effectiveType', '生效类型'],
['contractStage', '合同阶段'],
['approvalStatus', '审核状态'],
['archiveStatus', '归档状态'],
['currentNode', '当前节点'],
['currentProcessor', '当前处理人'],
],
@@ -98,11 +110,11 @@ export const config = {
},
deleteStatus: ['draft'],
deleteStage: ['draft'],
editStatus: ['draft', 'rejected', 'change_rejected'],
editStatus: ['draft', 'withdrawn', 'rejected', 'change_rejected'],
operations: [
{
action: 'flow',
label: '查看流程',
label: '流程',
statusProp: 'approvalStatus',
status: ['reviewing', 'change_reviewing'],
stage: ['temporary', 'formal'],
@@ -148,6 +160,16 @@ export const config = {
stage: ['formal'],
permission: 'contract_manage_reject',
},
{
action: 'submitFormal',
label: '转正式合同',
statusProp: 'approvalStatus',
status: ['approved'],
stage: ['temporary'],
permission: 'contract_manage_formal',
confirmMessage: '确定将该临时合同提交正式合同审批吗?',
successMessage: '已提交正式合同审批',
},
{
action: 'submitFormal',
label: '重新提交转正',
@@ -184,7 +206,7 @@ export const config = {
},
{
action: 'flow',
label: '查看流程',
label: '流程',
statusProp: 'approvalStatus',
status: ['approved', 'change_approved'],
stage: ['formal', 'terminated'],
@@ -241,6 +263,7 @@ export const option = createCrudOption([
searchPlaceholder: '请输入',
order: 180,
slot: true,
overHidden: true,
minWidth: 240,
rules: textRule('合同名称', 100, true),
},
@@ -254,8 +277,8 @@ export const option = createCrudOption([
formslot: true,
order: 165,
dicUrl:
'/blade-transport/project-apply/list?current=1&size=9999&approvalStatuses=approved,change_approved',
dicFormatter: listDicFormatter,
'/blade-transport/project-apply/list?current=1&size=9999&contractSelectable=true',
dicFormatter: projectNameDicFormatter,
props: {
label: 'projectName',
value: 'projectName',
@@ -275,20 +298,11 @@ export const option = createCrudOption([
prop: 'organizationName',
search: true,
searchOrder: 3,
searchType: 'cascader',
searchPlaceholder: '全部',
searchslot: true,
searchPlaceholder: '请选择 所属组织',
formslot: true,
order: 120,
dicData: [],
props: {
label: 'label',
value: 'id',
children: 'children',
},
checkStrictly: true,
emitPath: false,
showAllLevels: false,
filterable: true,
minWidth: 150,
rules: selectRule('所属组织'),
},
@@ -308,7 +322,9 @@ export const option = createCrudOption([
label: '签约类型',
prop: 'signType',
type: 'select',
search: false,
search: true,
searchOrder: 10,
searchPlaceholder: '请选择',
span: 8,
order: 190,
dicData: signTypeOptions,
@@ -475,6 +491,54 @@ export const option = createCrudOption([
order: 50,
minWidth: 130,
},
{
label: '合同金额',
prop: 'contractAmount',
type: 'number',
min: 0,
precision: 2,
hide: true,
order: 45,
minWidth: 130,
},
{
label: '是否范本',
prop: 'templateFlag',
type: 'select',
dicData: [
{ label: '否', value: 0 },
{ label: '是', value: 1 },
],
hide: true,
order: 40,
minWidth: 100,
},
{
label: '原件合同编号',
prop: 'originalContractNo',
hide: true,
order: 35,
minWidth: 160,
},
{
label: '是否电子章',
prop: 'electronicSealFlag',
type: 'select',
dicData: [
{ label: '否', value: 0 },
{ label: '是', value: 1 },
],
hide: true,
order: 30,
minWidth: 110,
},
{
label: '创建人',
prop: 'createUserName',
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '创建时间',
prop: 'createTime',
@@ -512,6 +576,19 @@ export const option = createCrudOption([
addDisplay: false,
editDisplay: false,
},
{
label: '归档状态',
prop: 'archiveStatus',
type: 'select',
search: true,
searchOrder: 5,
searchPlaceholder: '请选择',
slot: true,
dicData: contractArchiveStatusOptions,
minWidth: 110,
addDisplay: false,
editDisplay: false,
},
{
label: '当前节点',
prop: 'currentNode',
@@ -660,7 +737,7 @@ export const option = createCrudOption([
labelWidth: 0,
},
...auditColumns
.filter(column => column.prop !== 'createTime')
.filter(column => !['createTime', 'createUserName'].includes(column.prop))
.map(column => ({ ...column, hide: true })),
]);
+3 -2
View File
@@ -8,7 +8,7 @@ export const transportTypeDict = {
},
};
export const carrierTypeOptions = ['承运商', '自运', '网货平台'];
export const carrierTypeOptions = ['承运商', '自运'];
export const loadingStatusOptions = [
{ label: '草稿', value: 'draft' },
@@ -31,7 +31,7 @@ export const loadingManageConfig = {
exportColumns: [
{ prop: 'loadingNo', label: '配载单号' },
{ prop: 'loadingSubNos', label: '配载子单号' },
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' },
{ prop: 'vehicleNo', label: '车牌号/船班号/船次号/班次号' },
{ prop: 'driverName', label: '司机' },
{ prop: 'driverPhone', label: '联系电话' },
{ prop: 'carrierType', label: '承运类型' },
@@ -41,6 +41,7 @@ export const loadingManageConfig = {
{ prop: 'carrierName', label: '承运商' },
{ prop: 'transportType', label: '运输方式' },
{ prop: 'dataSource', label: '数据来源' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
{ prop: 'updateTime', label: '更新时间' },
{ prop: 'currentProcessNode', label: '过程节点' },
+4 -4
View File
@@ -85,6 +85,7 @@ export const option = {
type: 'number',
min: 0,
max: 999,
formatter: row => (Number(row.defaultFinishDays) === -1 ? '' : row.defaultFinishDays),
span: 12,
minWidth: 170,
addDisplay: false,
@@ -111,12 +112,12 @@ export const option = {
editDisplay: false,
},
...auditColumns
.filter(column => ['updateTime', 'createTime'].includes(column.prop))
.filter(column => ['createUserName', 'createTime', 'updateTime'].includes(column.prop))
.map(column => ({ ...column, minWidth: 190 }))
.sort(
(first, second) =>
['updateTime', 'createTime'].indexOf(first.prop) -
['updateTime', 'createTime'].indexOf(second.prop)
['createUserName', 'createTime', 'updateTime'].indexOf(first.prop) -
['createUserName', 'createTime', 'updateTime'].indexOf(second.prop)
),
{
label: '项目ID集合',
@@ -137,7 +138,6 @@ export const option = {
searchPlaceholder: '请选择',
dicData: processStatusOptions,
slot: true,
hide: true,
fixed: 'right',
minWidth: 100,
addDisplay: false,
+84 -47
View File
@@ -1,8 +1,8 @@
import {
auditColumns,
businessModeOptions,
businessTypeOptions,
createCrudOption,
effectiveTypeOptions,
nonNegativeRule,
projectApprovalStatusOptions,
projectSourceOptions,
@@ -12,23 +12,28 @@ import {
textRule,
} from './common';
const projectEffectiveTypeOptions = [
{ label: '临时', value: 'temporary' },
{ label: '正式', value: 'formal' },
];
export const fundUseRiskOptions = [
{ label: '高风险', value: 'high' },
{ label: '中风险', value: 'medium' },
{ label: '正常', value: 'none' },
];
// 项目资金额度使用率阈值:中风险 ≥ 80%,高风险 ≥ 90%
export const fundUseRiskThreshold = {
high: 90,
medium: 80,
};
const amountRules = label => [
{ required: true, message: `请输入${label}`, trigger: 'blur' },
nonNegativeRule(label),
];
const userDicFormatter = res => {
const list = Array.isArray(res)
? res
: Array.isArray(res.data)
? res.data
: res.data?.records || [];
return list.map(user => ({
...user,
realName: user.realName || user.name || user.account,
}));
};
const formatUserRealName = (row, role) =>
row[`${role}RealName`] ||
row[`${role}UserRealName`] ||
@@ -52,16 +57,24 @@ export const config = {
action: 'submitApproval',
label: '提交',
statusProp: 'approvalStatus',
status: ['draft'],
status: ['draft', 'rejected', 'withdrawn'],
permission: 'project_apply_submit',
},
{
action: 'withdraw',
label: '撤回',
statusProp: 'approvalStatus',
status: ['reviewing'],
status: ['draft', 'reviewing'],
permission: 'project_apply_withdraw',
},
{
action: 'mkAudit',
label: '流程',
statusProp: 'approvalStatus',
status: ['reviewing', 'change_reviewing'],
permission: 'project_apply_approve',
mkOnly: true,
},
{
action: 'approve',
label: '通过',
@@ -69,6 +82,7 @@ export const config = {
statusProp: 'approvalStatus',
status: ['reviewing', 'change_reviewing'],
permission: 'project_apply_approve',
localOnly: true,
},
{
action: 'reject',
@@ -77,6 +91,7 @@ export const config = {
statusProp: 'approvalStatus',
status: ['reviewing', 'change_reviewing'],
permission: 'project_apply_reject',
localOnly: true,
},
{
action: 'startChange',
@@ -85,25 +100,16 @@ export const config = {
status: ['approved'],
permission: 'project_apply_change',
},
{
action: 'voidProject',
label: '作废',
type: 'danger',
statusProp: 'approvalStatus',
status: ['approved', 'change_approved'],
permission: 'project_apply_void',
prompt: '请输入作废原因',
},
],
};
export const option = createCrudOption([
{
label: '立项申请单号',
label: '申请单号',
prop: 'applyNo',
search: true,
searchPlaceholder: '请输入',
searchOrder: 7,
searchOrder: 10,
minWidth: 220,
addDisplay: false,
editDisabled: true,
@@ -111,6 +117,9 @@ export const option = createCrudOption([
{
label: '项目编号',
prop: 'projectCode',
search: true,
searchPlaceholder: '请输入',
searchOrder: 5,
minWidth: 150,
addDisplay: false,
editDisabled: true,
@@ -120,7 +129,7 @@ export const option = createCrudOption([
prop: 'projectName',
search: true,
searchPlaceholder: '请输入',
searchOrder: 6,
searchOrder: 9,
minWidth: 180,
maxlength: 50,
rules: textRule('项目名称', 50, true),
@@ -131,7 +140,7 @@ export const option = createCrudOption([
hide: true,
minWidth: 140,
maxlength: 20,
rules: textRule('项目简称', 20, true),
rules: textRule('项目简称', 20),
},
{
label: '项目类型',
@@ -147,12 +156,18 @@ export const option = createCrudOption([
{
label: '客户名称',
prop: 'customerNames',
search: true,
searchPlaceholder: '请输入',
searchOrder: 6,
minWidth: 180,
rules: textRule('客户名称', 255, true),
},
{
label: '下游承运商',
prop: 'carrierNames',
search: true,
searchPlaceholder: '请输入',
searchOrder: 7,
minWidth: 180,
rules: textRule('下游承运商', 255, true),
},
@@ -164,42 +179,42 @@ export const option = createCrudOption([
rules: textRule('业务部门', 50, true),
},
{
label: '承办部门',
label: '平台公司',
prop: 'undertakeDeptName',
search: true,
searchPlaceholder: '请输入',
searchOrder: 5,
searchOrder: 8,
minWidth: 150,
rules: textRule('承办部门', 50, true),
rules: textRule('平台公司', 50, true),
},
{
label: '承办部门ID',
label: '平台公司ID',
prop: 'undertakeDeptId',
hide: true,
},
{
label: '项目负责人',
prop: 'principalUserName',
search: true,
searchPlaceholder: '请输入',
searchOrder: 4,
minWidth: 130,
formatter: row => formatUserRealName(row, 'principal'),
rules: textRule('项目负责人', 50, true),
},
{
label: '项目负责人',
label: '资金使用风险',
prop: 'fundUseRisk',
slot: true,
minWidth: 150,
addDisplay: false,
editDisplay: false,
hide: false,
},
{
label: '项目负责人ID',
prop: 'principalUserId',
type: 'select',
search: true,
searchPlaceholder: '请选择',
searchOrder: 4,
filterable: true,
dicUrl: '/blade-system/user/user-list',
dicFormatter: userDicFormatter,
props: {
label: 'realName',
value: 'id',
},
hide: true,
display: false,
},
{
label: '项目经办人',
@@ -310,6 +325,15 @@ export const option = createCrudOption([
hide: true,
minWidth: 130,
},
{
label: '业务模式',
prop: 'businessMode',
type: 'select',
dicData: businessModeOptions,
hide: true,
minWidth: 120,
rules: [selectRule('业务模式')],
},
{
label: '项目规模(万元)',
prop: 'projectScale',
@@ -328,6 +352,15 @@ export const option = createCrudOption([
minWidth: 150,
rules: [nonNegativeRule('预计利润')],
},
{
label: '利润率(%)',
prop: 'profitRate',
type: 'number',
precision: 2,
hide: true,
minWidth: 120,
rules: [nonNegativeRule('利润率')],
},
{
label: '资金需求(万元)',
prop: 'fundDemand',
@@ -379,7 +412,7 @@ export const option = createCrudOption([
search: true,
searchPlaceholder: '请选择',
searchOrder: 1,
dicData: effectiveTypeOptions,
dicData: projectEffectiveTypeOptions,
value: 'temporary',
minWidth: 110,
},
@@ -419,5 +452,9 @@ export const option = createCrudOption([
span: 24,
hide: true,
},
...auditColumns.map(column => ({ ...column, hide: true })),
...auditColumns.map(column =>
['createUserName', 'createTime'].includes(column.prop)
? column
: { ...column, hide: true }
),
]);
+28 -19
View File
@@ -55,8 +55,8 @@ export const config = {
'templateType',
'transportType',
'createUserName',
'remark',
'updateTime',
'remark',
'createTime',
],
exportColumns: [
@@ -65,8 +65,8 @@ export const config = {
{ prop: 'templateType', label: '模板类型' },
{ prop: 'transportType', label: '运输方式' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'remark', label: '备注' },
{ prop: 'updateTime', label: '更新时间' },
{ prop: 'remark', label: '备注' },
{ prop: 'createTime', label: '创建时间' },
],
enableAllDept: false,
@@ -82,12 +82,14 @@ export const config = {
detailAttachmentDescriptionPlain: true,
enableTransportPlanForm: true,
enableShippingTemplateFreight: true,
editableRoadAddress: true,
editableRoadAddress: false,
fixedTransportAddressType: true,
enableTemplateCodePreview: true,
attachmentTitle: '附件',
defaultForm: {
templateType: '运输计划',
transportType: '公路运输',
transportTypeName: '公路运输',
},
transportFormRequiredFields: [
['projectName', '项目'],
@@ -119,15 +121,16 @@ export const option = {
labelWidth: 0,
},
{
label: '模板编号',
prop: 'templateCode',
label: '模板类型',
prop: 'templateType',
type: 'select',
search: true,
searchOrder: 4,
searchOrder: 2,
span: 8,
order: 390,
minWidth: 170,
disabled: true,
placeholder: '系统自动生成',
dicData: templateTypeOptions,
minWidth: 120,
rules: selectRule('模板类型'),
display: true,
},
{
@@ -142,16 +145,15 @@ export const option = {
display: true,
},
{
label: '模板类型',
prop: 'templateType',
type: 'select',
label: '模板编号',
prop: 'templateCode',
search: true,
searchOrder: 2,
searchOrder: 4,
span: 8,
order: 370,
dicData: templateTypeOptions,
minWidth: 120,
rules: selectRule('模板类型'),
minWidth: 170,
disabled: true,
placeholder: '系统自动生成',
display: true,
},
{
@@ -269,7 +271,7 @@ export const option = {
order: 270,
hide: true,
labelWidth: '0px',
className: 'business-crud-page__full-form-item',
className: 'shipping-template-page__full-form-item',
},
{ label: '运费信息', prop: 'freightJson', hide: true, display: false },
{
@@ -290,7 +292,7 @@ export const option = {
order: 250,
hide: true,
labelWidth: '0px',
className: 'business-crud-page__full-form-item',
className: 'shipping-template-page__full-form-item',
},
{ label: '项目ID', prop: 'projectId', hide: true, display: false },
{ label: '客户合同ID', prop: 'contractId', hide: true, display: false },
@@ -300,7 +302,14 @@ export const option = {
{ label: '收货地', prop: 'arrivalName', hide: true, display: false },
{ label: '收货联系人', prop: 'arrivalContact', hide: true, display: false },
{ label: '收货联系方式', prop: 'arrivalPhone', hide: true, display: false },
{ label: '所属组织', prop: 'deptName', minWidth: 150, addDisplay: false, editDisplay: false, display: false },
{
label: '所属组织',
prop: 'deptName',
minWidth: 150,
addDisplay: false,
editDisplay: false,
display: false,
},
...auditColumns.map(column => ({
...column,
display: false,
+74 -40
View File
@@ -4,6 +4,7 @@ import {
createCrudOption,
nonNegativeRule,
textRule,
withSearchPlaceholders,
} from './common';
const listDicFormatter = res => {
@@ -15,6 +16,12 @@ const listDicFormatter = res => {
return [];
};
const projectNameDicFormatter = res => {
const list = listDicFormatter(res);
// 过滤掉 projectName 为空的记录
return list.filter(item => item && item.projectName && String(item.projectName).trim());
};
const userDicFormatter = res =>
listDicFormatter(res).map(user => ({
...user,
@@ -50,30 +57,25 @@ export const config = {
detailAlignCenter: true,
detailSections: [
{
title: '申请信息',
title: '项目额度信息',
fields: [
['applicationNo', '申请单号', 1],
['projectName', '项目', 1],
['projectName', '项目名称', 1],
['projectCode', '项目编号', 1],
['undertakeDeptName', '承办部门', 1],
['applyDeptName', '申请部门', 1],
['applicantName', '申请人', 1],
['approvalStatus', '审批状态', 1],
['currentNode', '当前节点', 1],
['currentProcessor', '当前处理人', 1],
['validUntil', '申请有效期至', 1],
['remark', '备注', 1],
],
},
{
title: '额度信息',
fields: [
['projectFundLimit', '项目资金使用额度(万元)', 1],
['usedFundLimit', '已使用项目资金使用额度(万元)', 1],
['remainingFundLimit', '剩余项目资金使用额度(万元)', 1],
['applyLimit', '申请临时额度(万元)', 1],
],
},
{
title: '临时额度信息',
fields: [
['applyLimit', '申请临时额度(万元)', 1],
['validUntil', '申请有效期至', 1],
['remark', '备注', 4],
],
showAttachment: true,
},
],
operations: [
{
@@ -118,7 +120,10 @@ export const config = {
export const option = {
dialogCustomClass: 'temporary-credit-limit-dialog',
...createCrudOption([
addTitle: '新增临时额度申请',
editTitle: '编辑临时额度申请',
viewTitle: '查看临时额度申请',
...createCrudOption(withSearchPlaceholders([
{
label: '申请单号',
prop: 'applicationNo',
@@ -129,6 +134,14 @@ export const option = {
editDisplay: false,
editDisabled: true,
},
{
label: '',
prop: 'projectQuotaInfoTitle',
formslot: true,
span: 24,
hide: true,
labelWidth: 0,
},
{
label: '项目名称',
prop: 'projectName',
@@ -137,7 +150,7 @@ export const option = {
searchType: 'select',
formslot: true,
dicUrl: '/blade-transport/project-apply/list?current=1&size=9999',
dicFormatter: listDicFormatter,
dicFormatter: projectNameDicFormatter,
props: {
label: 'projectName',
value: 'projectName',
@@ -158,6 +171,7 @@ export const option = {
prop: 'projectCode',
hide: true,
disabled: true,
placeholder: '请输入',
span: 6,
minWidth: 150,
rules: textRule('项目编号', 50),
@@ -167,6 +181,7 @@ export const option = {
prop: 'undertakeDeptName',
hide: true,
disabled: true,
placeholder: '请输入',
span: 6,
minWidth: 150,
rules: textRule('承办部门', 50, true),
@@ -176,6 +191,7 @@ export const option = {
prop: 'projectFundLimit',
hide: true,
disabled: true,
placeholder: '请输入',
span: 6,
minWidth: 200,
rules: [nonNegativeRule('项目资金使用额度')],
@@ -185,6 +201,7 @@ export const option = {
prop: 'usedFundLimit',
hide: true,
disabled: true,
placeholder: '请输入',
span: 6,
minWidth: 220,
value: 0,
@@ -195,11 +212,20 @@ export const option = {
prop: 'remainingFundLimit',
hide: true,
disabled: true,
placeholder: '请输入',
span: 6,
minWidth: 230,
value: 0,
rules: [nonNegativeRule('剩余项目资金使用额度')],
},
{
label: '',
prop: 'temporaryCreditInfoTitle',
formslot: true,
span: 24,
hide: true,
labelWidth: 0,
},
{
label: '申请临时额度(万元)',
prop: 'applyLimit',
@@ -217,9 +243,31 @@ export const option = {
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
placeholder: '请选择',
span: 6,
minWidth: 140,
rules: [{ required: true, message: '请选择申请有效期至', trigger: 'change' }],
rules: [
{ required: true, message: '请选择申请有效期至', trigger: 'change' },
],
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
span: 24,
minRows: 2,
hide: true,
placeholder: '请输入',
maxlength: 200,
showWordLimit: true,
rules: textRule('备注', 200),
},
{
label: '附件',
prop: 'attachmentsJson',
formslot: true,
span: 24,
hide: true,
},
{
label: '申请部门',
@@ -284,27 +332,13 @@ export const option = {
addDisplay: false,
editDisplay: false,
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
span: 24,
minRows: 2,
hide: true,
maxlength: 200,
showWordLimit: true,
rules: textRule('备注', 200),
},
{
label: '附件',
prop: 'attachmentsJson',
formslot: true,
span: 24,
hide: true,
},
...auditColumns.map(column => ({ ...column, hide: true })),
]),
dialogWidth: '96%',
...auditColumns.map(column =>
['createUserName', 'createTime'].includes(column.prop)
? column
: { ...column, hide: true }
),
])),
dialogWidth: 1100,
menuFixed: 'right',
menuWidth: 320,
index: false,
+53 -19
View File
@@ -1,7 +1,6 @@
import {
auditColumns,
createCrudOption,
dataSourceOptions,
phoneRule,
planStatusOptions,
selectRule,
@@ -9,6 +8,12 @@ import {
withSearchPlaceholders,
} from './common';
const transportPlanDataSourceOptions = [
{ label: '批量导入', value: '批量导入' },
{ label: '手工创建', value: '手工创建' },
{ label: '外部系统', value: '外部系统' },
];
const listDicFormatter = res => {
const data = res?.data || res;
if (Array.isArray(data)) return data;
@@ -18,6 +23,12 @@ const listDicFormatter = res => {
return [];
};
const projectNameDicFormatter = res => {
const list = listDicFormatter(res);
// 过滤掉 projectName 为空的记录
return list.filter(item => item && item.projectName && String(item.projectName).trim());
};
const parseGoodsRows = value => {
if (!value) return [];
if (Array.isArray(value)) return value;
@@ -61,7 +72,11 @@ const getSecondCargoTypeName = item =>
'货物';
const formatGoodsInfo = row => {
const rows = parseGoodsRows(row.goodsJson);
const rows =
[row.goodsJson, row.goodsList, row.goodsRows, row.cargoList, row.cargoRows, row.goods]
.map(source => parseGoodsRows(source))
.filter(source => source.length)
.sort((left, right) => right.length - left.length)[0] || [];
if (!rows.length) return row.goodsInfo || '';
const groups = rows.reduce((result, item = {}) => {
@@ -78,11 +93,22 @@ const formatGoodsInfo = row => {
item.goodsQuantity,
item.quantityUnit,
item.goodsQuantityUnit,
item.cargoUnit,
item.unit,
].some(Boolean);
if (!hasGoodsInfo) return result;
const unit = item.quantityUnit || item.goodsQuantityUnit || item.unit || '';
const unit = String(
item.quantityUnit ||
item.goodsQuantityUnit ||
item.cargoUnit ||
item.unit ||
row.quantityUnit ||
row.goodsQuantityUnit ||
row.cargoUnit ||
row.unit ||
''
).trim();
if (!result[unit]) {
result[unit] = {
typeName: getSecondCargoTypeName(item),
@@ -150,9 +176,12 @@ const formatDispatchProgress = row => {
}
}
const lines = Array.from(summary.entries()).map(
([unit, item]) => `${formatDispatchQuantity(item.assigned)}/${formatDispatchQuantity(item.total)}${unit}`
([unit, item]) =>
`${formatDispatchQuantity(item.assigned)}/${formatDispatchQuantity(item.total)}${unit}`
);
return lines.length ? lines.join('\n') : row.dispatchProgressName || row.dispatchProgress || row.progress || '';
return lines.length
? lines.join('\n')
: row.dispatchProgressName || row.dispatchProgress || row.progress || '';
};
const auditColumn = prop => ({ ...auditColumns.find(item => item.prop === prop) });
@@ -162,7 +191,6 @@ export const config = {
permission: 'transport_plan',
exportUrl: '/blade-transport/transport-plan/export-transport-plan',
exportName: '运输计划',
enableAllDept: false,
enableProjectSelect: true,
projectQueryParams: {
approvalStatuses: 'approved,change_approved',
@@ -176,18 +204,18 @@ export const config = {
enableTransportPlanCreateActions: true,
enableTransportPlanImport: true,
fixedTransportAddressType: true,
transportPlanImportTitle: '导入发货计划',
transportPlanImportTitle: '导入运输计划',
transportPlanTemplateUrl: '/blade-transport/transport-plan/export-template',
attachmentTitle: '附件',
searchRangeMap: {
planStartDateRange: ['planStartDateStart', 'planStartDateEnd'],
planEndDateRange: ['planEndDateStart', 'planEndDateEnd'],
createTimeRange: ['createTimeStart', 'createTimeEnd', '00:00:00', '23:59:59'],
},
actions: ['copy', 'complete'],
statusProp: 'businessStatus',
statusTextProp: 'businessStatusName',
deleteStatus: ['draft', 'waiting_dispatch'],
editStatus: ['draft', 'waiting_dispatch'],
deleteStatus: ['draft'],
editStatus: ['draft', 'waiting_dispatch', 'dispatching'],
detailButton: true,
detailSections: [
{
@@ -221,9 +249,10 @@ export const config = {
title: '其它信息',
fields: [
['remark', '备注'],
['createUserName', '创建人'],
['createTime', '创建时间'],
['updateUserName', '更新人'],
['updateTime', '更新时间'],
['createTime', '创建时间'],
],
},
],
@@ -293,7 +322,6 @@ export const option = {
prop: 'goodsInfo',
formatter: row => formatGoodsInfo(row),
minWidth: 320,
className: 'business-crud-page__goods-info-cell',
overHidden: false,
showOverflowTooltip: false,
addDisplay: false,
@@ -305,7 +333,6 @@ export const option = {
prop: 'dispatchProgress',
formatter: row => formatDispatchProgress(row),
minWidth: 120,
className: 'business-crud-page__dispatch-progress-cell',
overHidden: false,
showOverflowTooltip: false,
addDisplay: false,
@@ -315,6 +342,7 @@ export const option = {
{
label: '发货地址',
prop: 'departureAddress',
slot: true,
formslot: true,
search: true,
searchOrder: 7,
@@ -326,6 +354,7 @@ export const option = {
{
label: '到货地址',
prop: 'arrivalAddress',
slot: true,
formslot: true,
search: true,
searchOrder: 6,
@@ -344,7 +373,7 @@ export const option = {
span: 6,
order: 400,
dicUrl: '/blade-transport/project-apply/list?current=1&size=9999',
dicFormatter: listDicFormatter,
dicFormatter: projectNameDicFormatter,
props: {
label: 'projectName',
value: 'projectName',
@@ -371,6 +400,7 @@ export const option = {
span: 6,
order: 350,
minWidth: 150,
rules: [{ required: true, message: '请选择计划开始日期', trigger: 'change' }],
},
{
label: '计划结束日期',
@@ -381,6 +411,7 @@ export const option = {
span: 6,
order: 340,
minWidth: 150,
rules: [{ required: true, message: '请选择计划结束日期', trigger: 'change' }],
},
{
label: '数据来源',
@@ -388,7 +419,7 @@ export const option = {
type: 'select',
search: true,
searchOrder: 1,
dicData: dataSourceOptions,
dicData: transportPlanDataSourceOptions,
minWidth: 120,
addDisplay: false,
editDisplay: false,
@@ -427,6 +458,9 @@ export const option = {
showWordLimit: true,
rules: textRule('备注', 200),
},
{
...auditColumn('createUserName'),
},
{
...auditColumn('updateUserName'),
},
@@ -549,8 +583,8 @@ export const option = {
viewDisplay: false,
},
{
label: '计划结束日期',
prop: 'planEndDateRange',
label: '创建时间',
prop: 'createTimeRange',
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
@@ -580,7 +614,7 @@ export const option = {
order: 270,
hide: true,
labelWidth: '0px',
className: 'business-crud-page__full-form-item',
className: 'transport-plan-page__full-form-item',
},
{
label: '',
@@ -601,7 +635,7 @@ export const option = {
order: 260,
hide: true,
labelWidth: '0px',
className: 'business-crud-page__full-form-item',
className: 'transport-plan-page__full-form-item',
},
])
),
+163 -39
View File
@@ -19,6 +19,12 @@ const transportTypeDict = {
const isEmpty = value => value === undefined || value === null || value === '';
const normalizeNumericDisplayValue = value => (Number(value) === -1 ? '' : value);
const formatMoneyDisplay = value => {
if (isEmpty(value) || Number(value) === -1) return '';
const number = Number(value);
if (!Number.isFinite(number)) return '';
return number.toFixed(2);
};
const parseJsonArray = value => {
if (Array.isArray(value)) return value;
@@ -92,14 +98,32 @@ const getProcessConfigNodes = row => {
const isAcceptProcessNode = node => node.key === 'accept' || node.name === '接单';
const isTruthyFlag = value => {
if (value === true || value === 1) return true;
if (value === false || value === 0 || value === null || value === undefined || value === '') {
return false;
}
return ['true', '1', 'yes'].includes(String(value).trim().toLowerCase());
};
const requiresDriverAcceptConfirmation = row =>
getProcessConfigNodes(row).some(
node =>
isAcceptProcessNode(node) &&
node.enabled !== false &&
node.confirmMode === 'yes' &&
node.confirmDriver === true
);
getProcessConfigNodes(row).some(node => {
if (!isAcceptProcessNode(node) || node.enabled === false) return false;
if (node.confirmMode === 'no_confirm_accept') return false;
// 与后端一致:是否确认=是 即需接单(不强制依赖 confirmDriver)
return node.confirmMode === 'yes' || !node.confirmMode;
});
const getDriverRejectReason = row => {
if (!row) return '';
const reason =
row.driverRejectReason ||
row.driverRefuseReason ||
row.acceptRejectReason ||
row.rejectReason ||
'';
return String(reason || '').trim();
};
const isDriverRejectedStatus = value => {
if (value === null || value === undefined || typeof value === 'boolean') return false;
@@ -122,6 +146,7 @@ const isDriverRejectedStatus = value => {
};
const hasDriverRejectRecord = row => {
if (String(row.driverAcceptStatus || '').toLowerCase() === 'rejected') return true;
const rejectRecordValues = [
row.driverRejectTime,
row.driverRejectedTime,
@@ -161,9 +186,16 @@ const hasDriverRejectRecord = row => {
};
const isDriverRejectedWaybill = row =>
requiresDriverAcceptConfirmation(row) && hasDriverRejectRecord(row);
(row.requireAccept === true ||
row.requireAccept === 1 ||
requiresDriverAcceptConfirmation(row)) &&
hasDriverRejectRecord(row);
const hasDriverAcceptRecord = row => {
if (String(row.driverAcceptStatus || '').toLowerCase() === 'accepted') return true;
if (['rejected', 'pending'].includes(String(row.driverAcceptStatus || '').toLowerCase())) {
return false;
}
const recordValues = [
row.driverAcceptTime,
row.driverAcceptedTime,
@@ -172,6 +204,7 @@ const hasDriverAcceptRecord = row => {
row.driverAcceptedBy,
row.acceptUserId,
row.acceptUserName,
row.driverAcceptDriverId,
];
if (recordValues.some(value => value !== null && value !== undefined && value !== '')) {
return true;
@@ -207,6 +240,26 @@ const getFirstValue = (row, props) => {
const getGoodsRows = row => parseJsonArray(row.goodsList || row.goodsRows || row.goodsJson);
const getDetailedGoodsRows = row => {
const goodsRows = [
row.goodsJson,
row.goodsList,
row.goodsRows,
row.cargoList,
row.cargoRows,
row.goods,
]
.map(source => {
if (source === undefined || source === null || source === '') return [];
if (Array.isArray(source)) return source;
if (typeof source === 'object') return [source];
return parseJsonArray(source);
})
.filter(source => source.length)
.sort((left, right) => right.length - left.length);
return goodsRows[0] || [];
};
const getBillingRows = row => {
const freightRows = parseJsonArray(row.freightList || row.freightRows || row.freightJson);
if (freightRows.length) return freightRows;
@@ -218,9 +271,25 @@ const getBillingRows = row => {
const joinText = list => list.filter(item => !isEmpty(item)).join('/');
const formatGoodsInfo = row => {
const goodsRows = [
row.goodsJson,
row.goodsList,
row.goodsRows,
row.cargoList,
row.cargoRows,
row.goods,
]
.map(source => {
if (source === undefined || source === null || source === '') return [];
if (Array.isArray(source)) return source;
if (typeof source === 'object') return [source];
return parseJsonArray(source);
})
.filter(source => source.length)
.sort((left, right) => right.length - left.length)[0] || [];
const text = getFirstValue(row, ['goodsInfo', 'cargoInfo']);
if (text) return text;
return getGoodsRows(row)
if (!goodsRows.length) return text || '';
return goodsRows
.map(item => {
const name = getFirstValue(item, ['cargoName', 'goodsName', 'name']);
const type = getFirstValue(item, ['cargoType', 'goodsType', 'typeName']);
@@ -228,12 +297,13 @@ const formatGoodsInfo = row => {
normalizeNumericDisplayValue(
getFirstValue(item, ['quantity', 'cargoQuantity', 'goodsQuantity'])
),
getFirstValue(item, ['quantityUnit', 'cargoUnit', 'unit']),
getFirstValue(item, ['quantityUnit', 'goodsQuantityUnit', 'cargoUnit', 'unit']) ||
getFirstValue(row, ['quantityUnit', 'goodsQuantityUnit', 'cargoUnit', 'unit']),
]);
return joinText([name, type, quantity]);
})
.filter(Boolean)
.join('; ');
.join('; ') || text || '';
};
const formatGoodsField = (row, props) => {
@@ -246,6 +316,28 @@ const formatGoodsField = (row, props) => {
const formatUnitPrice = row => {
const value = normalizeNumericDisplayValue(getFirstValue(row, ['unitPrice', 'price']));
const unit = getFirstValue(row, ['priceUnit', 'billingUnit', 'unit']);
const goodsRows = getDetailedGoodsRows(row);
if (goodsRows.length > 1) {
const billingRows = getBillingRows(row);
const goodsPrices = goodsRows
.map((item, index) => {
const itemPrice = normalizeNumericDisplayValue(
getFirstValue(item, ['unitPrice', 'price'])
);
if (!isEmpty(itemPrice)) return itemPrice;
return normalizeNumericDisplayValue(
getFirstValue(billingRows[index] || {}, ['unitPrice', 'price'])
);
})
.filter(itemPrice => !isEmpty(itemPrice));
const priceKeys = goodsPrices.map(itemPrice => {
const numericPrice = Number(itemPrice);
return Number.isFinite(numericPrice) ? String(numericPrice) : String(itemPrice).trim();
});
if (new Set(priceKeys).size > 1) return '-';
}
if (!isEmpty(value)) return unit ? `${value}(${unit})` : value;
const billing = getBillingRows(row).find(
item => !isEmpty(normalizeNumericDisplayValue(item.unitPrice))
@@ -256,10 +348,10 @@ const formatUnitPrice = row => {
return billingUnit ? `${billingPrice}(${billingUnit})` : billingPrice;
};
const formatAmount = (row, props) => getFirstValue(row, props);
const formatAmount = (row, props) => formatMoneyDisplay(getFirstValue(row, props));
const sumAmount = list =>
list.reduce((sum, item) => {
const sumAmount = (rows = []) =>
rows.reduce((sum, item) => {
const value = getFirstValue(item, ['freightAmount', 'amount', 'totalAmount']);
return isEmpty(value) ? sum : sum + Number(value || 0);
}, 0);
@@ -278,7 +370,7 @@ const calculateFreightTotal = rows => {
return sum + Number(item.unitPrice || 0) * Number(item.quantity || 0);
}, 0);
if (!hasAmountField) return '';
return Number.isFinite(total) ? Number(total.toFixed(2)).toString() : '';
return Number.isFinite(total) ? total.toFixed(2) : '';
};
const formatFreight = row => {
@@ -286,9 +378,9 @@ const formatFreight = row => {
if (!isEmpty(value)) return value;
const freight = parseJsonObject(row.freightJson);
const freightValue = getFirstValue(freight, ['freightAmount', 'transportFee']);
if (!isEmpty(freightValue)) return freightValue;
if (!isEmpty(freightValue)) return formatMoneyDisplay(freightValue);
const total = sumAmount(Array.isArray(freight.freightItems) ? freight.freightItems : []);
return total ? total : '';
return total ? formatMoneyDisplay(total) : '';
};
const formatOtherFeeTotal = row => {
@@ -305,10 +397,10 @@ const formatFreightTotal = row => {
const otherFeeTotal = !isEmpty(otherFee) ? otherFee : formatOtherFeeTotal(row);
if (!isEmpty(calculated)) {
const total = Number(calculated || 0) + Number(otherFeeTotal || 0);
return Number.isFinite(total) ? Number(total.toFixed(2)).toString() : calculated;
return Number.isFinite(total) ? total.toFixed(2) : calculated;
}
const value = getFirstValue(freight, ['totalFreightAmount', 'totalFreight', 'totalAmount']);
if (!isEmpty(value)) return value;
if (!isEmpty(value)) return formatMoneyDisplay(value);
return formatAmount(row, ['freightTotal', 'totalFreight', 'totalAmount']);
};
@@ -318,6 +410,8 @@ export const config = {
title: '运单管理',
permission: 'waybill_manage',
importUrl: '/blade-transport/waybill-manage/import-waybill-manage',
exportUrl: '/blade-transport/waybill-manage/export-waybill-manage',
exportName: '运单管理',
defaultForm: {
carrierType: '承运商',
transportType: 'road',
@@ -364,29 +458,37 @@ export const config = {
statusProp: 'businessStatus',
statusTextProp: 'businessStatusName',
formatStatus(row, prop, defaultText) {
if (
prop === 'businessStatus' &&
requiresDriverAcceptConfirmation(row) &&
isInProgressStatus(row, defaultText) &&
!hasDriverAcceptRecord(row)
) {
if (prop !== 'businessStatus') return defaultText;
const status = String(row.businessStatus || '');
const terminalStatuses = ['draft', 'completed', 'cancelled', 'waiting_dispatch', 'dispatching'];
const needAccept =
row.requireAccept === true ||
row.requireAccept === 1 ||
requiresDriverAcceptConfirmation(row);
// 需司机接单且尚未接单 → 待执行(含库中仍为 running 的历史数据展示校正)
if (needAccept && !hasDriverAcceptRecord(row) && !terminalStatuses.includes(status)) {
return '待执行';
}
if (
prop === 'businessStatus' &&
row.businessStatus === 'pending' &&
!requiresDriverAcceptConfirmation(row)
) {
return '进行中';
}
if (status === 'pending') return '待执行';
if (status === 'running' || status === 'processing') return '进行中';
return defaultText;
},
canReassign(row) {
return isDriverRejectedWaybill(row);
},
getDriverRejectReason,
isDriverRejectedWaybill,
canEdit(row) {
// 进行中的运单不允许编辑
if (isInProgressStatus(row, row.businessStatusName)) {
return false;
}
return !isDriverRejectedWaybill(row);
},
canCancel(row) {
// 进行中的运单不允许取消
return !isInProgressStatus(row, row.businessStatusName);
},
deleteStatus: ['draft'],
editStatus: ['draft', 'pending'],
};
@@ -441,7 +543,8 @@ export const option = {
prop: 'projectName',
formslot: true,
search: true,
searchLabel: '项目',
searchLabel: '项目名称',
searchPlaceholder: '请选择或输入',
searchOrder: 22,
span: 6,
order: 890,
@@ -453,6 +556,7 @@ export const option = {
prop: 'customerName',
search: true,
searchLabel: '客户名称',
searchPlaceholder: '请选择或输入',
searchOrder: 21,
minWidth: 150,
addDisplay: false,
@@ -461,10 +565,10 @@ export const option = {
display: false,
},
{
label: '车牌号/航班号/船号/班列号',
label: '车牌号/船班号/船次号/班次号',
prop: 'vehicleNo',
search: true,
searchLabel: '车/船/航班/班列',
searchLabel: '车牌号/船班号/船次号/班次号',
searchOrder: 13,
minWidth: 210,
display: false,
@@ -474,6 +578,7 @@ export const option = {
prop: 'driverName',
search: true,
searchLabel: '司机名称',
searchPlaceholder: '请选择或输入',
searchOrder: 14,
minWidth: 120,
display: false,
@@ -524,6 +629,7 @@ export const option = {
prop: 'carrierName',
search: true,
searchLabel: '承运商名称',
searchPlaceholder: '请选择或输入',
searchOrder: 15,
minWidth: 150,
display: false,
@@ -578,7 +684,7 @@ export const option = {
viewDisplay: false,
},
{
label: '单价(计价单位)',
label: '单价',
prop: 'unitPrice',
formatter: row => formatUnitPrice(row),
minWidth: 140,
@@ -627,15 +733,30 @@ export const option = {
prop: 'planName',
formslot: true,
search: true,
searchPlaceholder: '请选择或输入',
searchOrder: 7,
span: 6,
order: 870,
minWidth: 150,
formatter: row => {
const value = row?.planName;
if (
value === undefined ||
value === null ||
value === '' ||
String(value) === '-1' ||
Number(value) === -1
) {
return '';
}
return value;
},
},
{
label: '货物类型',
prop: 'cargoType',
search: true,
searchPlaceholder: '请选择或输入',
searchOrder: 18,
formatter: row => formatGoodsField(row, ['cargoType', 'goodsType', 'typeName']),
minWidth: 130,
@@ -762,6 +883,9 @@ export const option = {
viewDisplay: false,
display: false,
},
{
...auditColumn('createUserName'),
},
{
...auditColumn('createTime'),
},
@@ -808,7 +932,7 @@ export const option = {
order: 760,
hide: true,
labelWidth: '0px',
className: 'business-crud-page__full-form-item',
className: 'waybill-manage-page__full-form-item',
},
{
label: '实际发货时间',
@@ -920,7 +1044,7 @@ export const option = {
order: 740,
hide: true,
labelWidth: '0px',
className: 'business-crud-page__full-form-item',
className: 'waybill-manage-page__full-form-item',
},
{
label: '所属组织',
+1 -1
View File
@@ -1,5 +1,5 @@
export const billLedgerTableColumns = [
{ prop: 'billNo', label: '票据编号', minWidth: 170 },
{ prop: 'billNo', label: '票据编号', minWidth: 170, link: true },
{ prop: 'receiverName', label: '收票单位', minWidth: 170 },
{ prop: 'issuerName', label: '出票单位', minWidth: 170 },
{ prop: 'billTypeName', label: '汇票类型', minWidth: 110 },
+4 -4
View File
@@ -1,6 +1,6 @@
export const invoiceApplicationTableColumns = [
{ prop: 'applicationNo', label: '单据号', minWidth: 170, link: true },
{ prop: 'settlementNos', label: '结算单号', minWidth: 190, link: true },
{ prop: 'settlementNos', label: '结算单号', minWidth: 190 },
{ prop: 'projectName', label: '所属项目', minWidth: 140 },
{ prop: 'deptName', label: '所属组织', minWidth: 140 },
{ prop: 'issuerName', label: '开票方', minWidth: 150 },
@@ -13,6 +13,7 @@ export const invoiceApplicationTableColumns = [
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
];
export const invoiceApplicationDetailColumns = [
@@ -21,8 +22,8 @@ export const invoiceApplicationDetailColumns = [
{ prop: 'vehicleNo', label: '车号', minWidth: 120 },
{ prop: 'departureAddress', label: '发货地址', minWidth: 180 },
{ prop: 'arrivalAddress', label: '到货地址', minWidth: 180 },
{ prop: 'actualDepartureTime', label: '实际发货时间', minWidth: 170 },
{ prop: 'actualCompletionTime', label: '实际完成时间', minWidth: 170 },
{ prop: 'actualDepartureTime', label: '实际发货时间', minWidth: 170, dateTime: true },
{ prop: 'actualCompletionTime', label: '实际完成时间', minWidth: 170, dateTime: true },
{ prop: 'transportType', label: '运输类型', minWidth: 120 },
{ prop: 'cargoName', label: '货物名称', minWidth: 140 },
{ prop: 'cargoType', label: '货物类型', minWidth: 130 },
@@ -30,5 +31,4 @@ export const invoiceApplicationDetailColumns = [
{ prop: 'mileage', label: '里程(KM)', minWidth: 120 },
{ prop: 'batchNo', label: '批次号', minWidth: 120 },
{ prop: 'freightAmount', label: '运费', minWidth: 120, money: true },
{ prop: 'feeItemsJson', label: '费用项目', minWidth: 180 },
];
+4 -2
View File
@@ -11,6 +11,8 @@ export const invoiceReceiptTableColumns = [
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
{ prop: 'createUserName', label: '创建人', minWidth: 110 },
{ prop: 'createTime', label: '创建时间', minWidth: 170 },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
{ prop: 'kingdeeReceivedInvoiceNo', label: '金蝶单据号', minWidth: 150 },
{ prop: 'kingdeePushStatusName', label: '金蝶状态', minWidth: 130 },
{ prop: 'kingdeePushMsg', label: '失败原因', minWidth: 160 },
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
];
+1
View File
@@ -17,4 +17,5 @@ export const paymentApplicationTableColumns = [
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 120 },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 130 },
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 130 },
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
];
+7 -1
View File
@@ -1,5 +1,5 @@
export const receiptFlowTableColumns = [
{ prop: 'receiptNoticeNo', label: '认领通知单', minWidth: 150 },
{ prop: 'receiptNoticeNo', label: '认领通知单', minWidth: 150, link: true },
{ prop: 'payerName', label: '付款人', minWidth: 130 },
{ prop: 'receiptAmount', label: '付款金额', minWidth: 130, money: true },
{ prop: 'counterpartyName', label: '对方户名', minWidth: 150 },
@@ -8,5 +8,11 @@ export const receiptFlowTableColumns = [
{ prop: 'summary', label: '摘要', minWidth: 160 },
{ prop: 'transactionTime', label: '交易时间', minWidth: 170 },
{ prop: 'claimStatusName', label: '认领状态', minWidth: 110, status: true },
{ prop: 'kingdeeClaimStatusName', label: '公告状态', minWidth: 110 },
{ prop: 'claimedAmount', label: '已认领金额', minWidth: 130, money: true },
{ prop: 'unclaimedAmount', label: '金蝶未认领金额', minWidth: 140, money: true },
{ prop: 'pushStatusName', label: '推送状态', minWidth: 110, status: true },
{ prop: 'pushFailReason', label: '推送失败原因', minWidth: 160 },
{ prop: 'kingdeeBillNo', label: '金蝶收款单号', minWidth: 150 },
{ prop: 'kingdeeBillStatusName', label: '金蝶状态', minWidth: 110 },
];
@@ -11,8 +11,8 @@ export const formalSettlementFormFields = [
{ label: '汇率日期', prop: 'exchangeRateDate', type: 'date', required: true },
{ label: '结算汇率', prop: 'exchangeRate', type: 'number', required: true },
{ label: '本位币合计', prop: 'localSettlementAmount', readonly: true, money: true },
{ label: '申请付款金额(含预付)', prop: 'appliedPaymentAmount', readonly: true, money: true },
{ label: '已收/已付合计', prop: 'paidAmount', readonly: true, money: true },
{ label: '创建人', prop: 'createUserName', readonly: true },
{ label: '创建时间', prop: 'createTime', readonly: true },
];
export const createFormalSettlementForm = () => ({
@@ -27,6 +27,8 @@ export const createFormalSettlementForm = () => ({
sourceDetailIds: [],
exchangeRateDate: '',
exchangeRate: 1,
createUserName: '',
createTime: '',
attachmentsJson: '[]',
remark: '',
});
@@ -7,11 +7,21 @@ export const approvalStatusOptions = [
];
export const invoiceStatusOptions = [
{ label: '未收/开票', value: 'unreceived' },
{ label: '部分收/开票', value: 'partial' },
{ label: '已收/开票', value: 'completed' },
{ label: '未开/收票', value: 'unreceived' },
{ label: '部分开/收票', value: 'partial' },
{ label: '已开/收票', value: 'completed' },
];
export const invoiceStatusName = (value, settlementType) => {
const payable = settlementType === 'payable';
const labels = {
unreceived: payable ? '未收票' : '未开票',
partial: payable ? '部分收票' : '部分开票',
completed: payable ? '已收票' : '已开票',
};
return labels[value] || value || '-';
};
export const paymentStatusOptions = [
{ label: '未收/付款', value: 'unpaid' },
{ label: '部分收/付款', value: 'partial' },
@@ -28,7 +38,7 @@ export const formalSettlementSearchFields = [
{ label: '结算单号', prop: 'formalSettlementNo', type: 'input' },
{ label: '预结算单号', prop: 'preSettlementNo', type: 'input' },
{ label: '项目', prop: 'projectName', type: 'input' },
{ label: '所属组织', prop: 'deptName', type: 'input' },
{ label: '所属组织', prop: 'deptName', type: 'cascader' },
{ label: '合同编号', prop: 'contractNo', type: 'input' },
{ label: '付款方', prop: 'payerName', type: 'input' },
{ label: '收款方', prop: 'payeeName', type: 'input' },
@@ -1,6 +1,5 @@
export const formalSettlementTableColumns = [
{ label: '结算单号', prop: 'formalSettlementNo', minWidth: 160, link: true, fixed: 'left' },
{ label: '预结算单号', prop: 'preSettlementNos', minWidth: 190 },
{ label: '来源', prop: 'sourceType', minWidth: 110 },
{ label: '付款方', prop: 'payerName', minWidth: 150 },
{ label: '收款方', prop: 'payeeName', minWidth: 150 },
@@ -12,13 +11,17 @@ export const formalSettlementTableColumns = [
{ label: '本位币结算金额', prop: 'localSettlementAmount', minWidth: 155, money: true },
{ label: '结算汇率', prop: 'exchangeRate', minWidth: 110 },
{ label: '申请付款金额(含预付)', prop: 'appliedPaymentAmount', minWidth: 185, money: true },
{ label: '已收/已付合计', prop: 'paidAmount', minWidth: 145, money: true },
{ label: '已收/已付合计(含预付)', prop: 'paidAmount', minWidth: 200, money: true },
{ label: '剩余可付金额', prop: 'remainingPayableAmount', minWidth: 145, money: true },
{ label: '发票金额', prop: 'invoiceAmount', minWidth: 130, money: true },
{ label: '发票状态', prop: 'invoiceStatusName', minWidth: 120 },
{ label: '收付款状态', prop: 'paymentStatusName', minWidth: 120 },
{ label: '审核状态', prop: 'approvalStatusName', minWidth: 110, status: true },
{ label: '当前节点', prop: 'currentNode', minWidth: 130 },
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 },
{ label: '金蝶单据状态', prop: 'kingdeeSyncStatus', minWidth: 135 },
{ label: '附件同步', prop: 'attachmentSyncStatusName', minWidth: 100 },
{ label: '创建人', prop: 'createUserName', minWidth: 110 },
{ label: '创建时间', prop: 'createTime', minWidth: 170 },
];
+1 -1
View File
@@ -10,7 +10,7 @@ export const preSettlementSearchFields = [
{ label: '预结算单号', prop: 'preSettlementNo', type: 'input' },
{ label: '预付单号', prop: 'advanceNo', type: 'input' },
{ label: '项目名称', prop: 'projectName', type: 'input' },
{ label: '所属组织', prop: 'deptName', type: 'input' },
{ label: '所属组织', prop: 'deptName', type: 'cascader' },
{ label: '合同名称', prop: 'contractName', type: 'input' },
{ label: '合同编号', prop: 'contractNo', type: 'input' },
{ label: '收款方', prop: 'payeeName', type: 'input' },
+12 -2
View File
@@ -10,8 +10,18 @@ export const preSettlementTableColumns = [
{ label: '原币结算金额', prop: 'settlementAmount', minWidth: 140, money: true, precision: 2 },
{ label: '本位币结算金额', prop: 'localSettlementAmount', minWidth: 150, money: true, precision: 2 },
{ label: '结算汇率', prop: 'exchangeRate', minWidth: 110, precision: 2 },
{ label: '申请预付金额', prop: 'advanceAppliedAmount', minWidth: 140, money: true },
{ label: '已付款金额', prop: 'advancePaidAmount', minWidth: 130, money: true },
{
label: '申请预付金额',
prop: 'advanceAppliedAmount',
minWidth: 190,
money: true,
},
{
label: '已付款金额',
prop: 'advancePaidAmount',
minWidth: 200,
money: true,
},
{ label: '审核状态', prop: 'approvalStatusName', minWidth: 110, status: true },
{ label: '当前节点', prop: 'currentNode', minWidth: 130 },
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
@@ -14,7 +14,7 @@ export const searchFields = [
{ label: '单据号', prop: 'documentNo', type: 'input' },
{ label: '生成日期', prop: 'generateDateRange', type: 'daterange' },
{ label: '客户名称', prop: 'customerName', type: 'input' },
{ label: '所属组织', prop: 'deptName', type: 'input' },
{ label: '所属组织', prop: 'deptName', type: 'cascader' },
{ label: '项目名称', prop: 'projectName', type: 'input' },
{ label: '货物类型', prop: 'cargoType', type: 'input' },
{ label: '货物名称', prop: 'cargoName', type: 'input' },
@@ -57,20 +57,17 @@ export const feeDetailBaseColumns = [
{ label: '货物类型', prop: 'cargoType', minWidth: 130 },
{ label: '规格', prop: 'specification', minWidth: 120 },
{ label: '型号', prop: 'model', minWidth: 120 },
{ label: '运费计费要素', prop: 'billingFactor', minWidth: 130 },
{ label: '运费计费类型', prop: 'billingType', minWidth: 130 },
{ label: '计费要素', prop: 'billingFactor', minWidth: 130 },
{ label: '计费类型', prop: 'billingType', minWidth: 130 },
{ label: '运输量', prop: 'transportQuantityText', minWidth: 120 },
{ label: '运费计算单位', prop: 'priceUnit', minWidth: 130 },
{ label: '运输单价', prop: 'unitPrice', minWidth: 120 },
{ label: '里程(KM)', prop: 'mileage', minWidth: 120 },
{ label: '运输费', prop: 'freightAmount', minWidth: 180 }
{ label: '运输费', prop: 'freightAmount', minWidth: 180 },
];
export const feeDetailTailColumns = [
{ label: '原总金额', prop: 'originalAmountText', minWidth: 120 },
{ label: '调整金额', prop: 'adjustAmountText', minWidth: 120 },
{ label: '调整后总金额', prop: 'afterAmountText', minWidth: 140 },
{ label: '结算金额', prop: 'originalAmountText', minWidth: 120 },
{ label: '备注', prop: 'remark', minWidth: 160 },
{ label: '最后录入人', prop: 'updateUserName', minWidth: 120 },
{ label: '最后录入时间', prop: 'updateTime', minWidth: 170 },
@@ -91,6 +88,8 @@ export const updateFeeFormFields = [
];
export const transferSearchFields = [
{ label: '单据号', prop: 'documentNo', type: 'input' },
{ label: '合同编码', prop: 'contractNo', type: 'input' },
{ label: '合同名称', prop: 'contractName', type: 'input' },
{ label: '批次号', prop: 'batchNo', type: 'input' },
{ label: '生成日期', prop: 'generateDateRange', type: 'daterange' },
@@ -107,7 +106,7 @@ export const generateWaybillColumns = [
{ label: '运单号', prop: 'waybillNo', minWidth: 160, link: true },
{ label: '项目名称', prop: 'projectName', minWidth: 160 },
{ label: '客户/委托方', prop: 'customerName', minWidth: 160 },
{ label: '车牌号/航班号/船号/班列号', prop: 'vehicleNo', minWidth: 190 },
{ label: '车牌号/船班号/船次号/班次号', prop: 'vehicleNo', minWidth: 190 },
{ label: '司机', prop: 'driverName', minWidth: 120 },
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
{ label: '运输类型', prop: 'transportType', minWidth: 130 },
@@ -139,4 +138,11 @@ export const generatePreviewColumns = [
{ label: '货物名称', prop: 'cargoName', minWidth: 140 },
{ label: '货物类型', prop: 'cargoType', minWidth: 140 },
{ label: '规格', prop: 'specification', minWidth: 140 },
{ label: '型号', prop: 'model', minWidth: 120 },
{ label: '计费要素', prop: 'billingFactor', minWidth: 130 },
{ label: '计费类型', prop: 'billingType', minWidth: 130 },
{ label: '运输量', prop: 'transportQuantityText', minWidth: 120 },
{ label: '运费计算单位', prop: 'priceUnit', minWidth: 130 },
{ label: '运输单价', prop: 'unitPrice', minWidth: 120 },
{ label: '里程(KM)', prop: 'mileage', minWidth: 120 },
];

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