Compare commits

..

48 Commits

Author SHA1 Message Date
b2894lxlx 18aeb08c90 🐛 修复变更记录按提交批次汇总,临时额度-1显示为-
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-14 10:58:42 +08:00
b2894lxlx cdd30b8211 ✨ 机构同步编码校验对齐GWWL三位数字规则
组织编码校验改为上级直拼三位数字;全量同步清除提示同步保留外部组织与集团锚点。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-14 10:45:01 +08:00
gxwebsoft 6762cf7b83 Merge remote-tracking branch 'origin/master' 2026-10-14 10:02:17 +08:00
gxwebsoft a87b5e4b95 fix(styles): 修正车辆调度弹窗表单标签与控件宽度不一致问题
- 修改选择器为后代选择器以适配 avue 表单项的嵌套结构
- 统一弹窗内表单标签宽度,避免 labelWidth: 'auto' 导致控件宽度不一
- 新增全局样式规则,固定标签宽度为 calc(4em + 24px)
- 删除用于调试控件宽度的临时文件 cascader-width-check.html
- 记录并说明 avue 弹窗宽度调整相关坑点和解决方案
2026-10-14 10:02:12 +08:00
b2894lxlx 1181aea3b9 🐛 凭证分片遇 HTTP 预签名时回退服务端代传
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:44:17 +08:00
gxwebsoft 68f4b2c35e style(transportCapacity): 调整车辆调度弹窗部门级联选择器宽度
- 注释掉多个页面及组件中el-form-item的margin-bottom样式
- 增加车辆调度弹窗配置dialogCustomClass: 'vehicle-dispatch-dialog'
- 在全局样式element-ui.scss中为.vehicle-dispatch-dialog内el-cascader设置宽度100%
- 解决avue-crud弹窗append-to-body导致页面容器scoped样式无法命中问题
- 更新vite配置,将开发服务器端口修改为2889,代理目标指向远程地址
- 补充工程文档说明和样式调整原因分析
2026-10-13 20:21:17 +08:00
b2894lxlx 6296c8f6f4 ⚡ 配载货物信息分列数量单位,并修复车辆尺寸 -1 展示
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 12:55:15 +08:00
b2894lxlx 6955784e93 ⚡ 优化表格工具栏、附件文件名展示与合同金额两位小数
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 12:44:40 +08:00
b2894lxlx bc7755a160 ✨ 小程序用户支持重置密码,列表隐藏 openid
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 14:47:28 +08:00
gxwebsoft 18a02a1969 Merge remote-tracking branch 'origin/master' 2026-10-12 14:20:26 +08:00
b2894lxlx dcaa3127a0 ✨ 新增小程序用户管理页,并加宽用户账号名展示
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 13:49:10 +08:00
b2894lxlx 959c1821c1 🐛 司机/车辆档案日期兼容 OCR 宽松格式
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 13:02:09 +08:00
gxwebsoft a4c2987c5f style(table): 全站文件名列添加省略号与溢出隐藏
- 统一给文件名相关的 el-table-column 添加 class-name="is-ellipsis-link"
- 解决了 el-link 为 flex 项导致列宽撑破的问题
- 在样式中新增 is-ellipsis-link 规则,实现文本溢出省略号效果
- 保证超长文件名不会破坏表格布局且能完整展示链接提示
- 适用于业务多个组件中涉及附件、文件名等长文本列的处理
2026-10-12 12:12:24 +08:00
b2894lxlx 2377a2e8d4 Merge remote-tracking branch 'websoft/dev' 2026-10-12 10:38:46 +08:00
b2894lxlx f455003014 ✨ 优化主数据地图回填与表单校验间距,并修复运单导入筛选与组织树定位
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-10 16:20:26 +08:00
b2894lxlx ea797356ce ✨ 完善组织人员增量同步与用户管理交互
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-10 13:56:29 +08:00
b2894lxlx eaece0209d ✨ 合同结算币种联动计费与运单计价单位,并完善合同与派车交互
结算币种驱动计费单价单位及运单/计划/总单计价单位;同步合同编号预览、部门前缀配置、派车备注与查看弹窗等交互优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-10 00:53:17 +08:00
weicw-dshsa cd92c36dab ✨ 认证审核详情弹窗增加审核人与审核时间 2026-10-09 20:47:35 +08:00
b2894lxlx a1da644c49 ✨ 优化港口地图回填、合同展示与运单导入空值
地图选点回填国家省市区;合同选择显示编号/名称;常用数据组织筛选修正;运单详情将-1费用字段显示为空。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-09 17:07:56 +08:00
weicw-dshsa 2500540933 🐛 从业资格证字段改名后审核页兼容旧键,补全封面/内容页与有效期起止
从业资格证信息组原先只列证号、照片、有效期止三项,且照片用的是字段改名前的旧键
qualificationUrl。改名之前提交的记录里仍存旧键,只读新键会得到空值,审核页把已填的
封面页显示成「—」,审核员据此以为对方没传材料。现按 VehicleCertSubmitRequest 的
字段名补全该组,并新增 readFieldValue 统一兼容旧键,新旧键并存时以新键为准。

旧键已并入所属字段展示,knownKeys 同步登记,避免同一张封面页出现两行。
2026-10-09 10:01:19 +08:00
b2894lxlx 570fbeb537 🐛 运单轨迹与实时定位WGS-84转GCJ-02后再绘制高德地图
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-08 19:37:27 +08:00
b2894lxlx d1f804056e ✨ 审核通过后保留流程按钮并跳转MK页面
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-08 18:57:49 +08:00
weicw-dshsa 3320155416 ✨ 打卡照片按凭证类型分组,编辑弹窗去掉打卡状态开关
- 打卡记录照片按类型分组展示与上传,每个类型各自最多 9 张,组内第一张标「主图」
- 打卡照片是每个打卡节点必有的类型,不由过程配置决定,与各凭证类型并列且互不替代
- 附件区抽出 punch-record-photos 组件,运单详情与配载单详情共用;编辑态沿用 96px 固定方图
- 弹窗上传体积上限由 5MB 提到 20MB,与小程序一致
- 弹窗不再提供「打卡状态」开关:未打卡的记录点「补卡」进来,保存即补出一条已打卡流水
2026-10-07 16:57:46 +08:00
weicw-dshsa 8ad53894b9 ✨ 运单详情编辑打卡记录支持地图选点定打卡地址
打卡地址改为只读输入框,点击弹出地图选址(复用 address-map-picker,
与空港、港口、维修记录等处同一套),确定后回填地址与经纬度。
地址与经纬度成套更新——只回填地址会让坐标停留在司机打卡时的旧点上。
经纬度输入框保留,仍可手工微调。
2026-10-06 13:55:57 +08:00
weicw-dshsa 360147e46a 🐛 修复编辑打卡记录无附件时附件上传入口消失
打卡照片原先按过程配置的「上传凭证」取舍:配置没勾该开关、记录里又没有照片的
节点,整块照片区连带上传入口都不渲染,管理端再也补不了附件。
过程配置的「上传凭证」是司机端的要求,后端保存打卡记录时不校验它,
管理端补录的角色改为始终展示照片列表与上传入口。
2026-10-06 13:45:04 +08:00
weicw-dshsa b00e12c9bb ✨ 异常上报弹窗附件支持在线预览 2026-10-06 13:04:53 +08:00
weicw-dshsa 7543a8c06b ✨ 打卡时间轴不再为接单节点提供编辑入口
接单节点的完成由运单的接单事实决定,不落打卡流水:补录或取消打卡都改不动它,
放出去只会造出一条与运单事实打架的流水(时间轴显示已打卡、运单却还没人接单)。
后端已拒绝该节点的补录与取消,前端同步隐藏「编辑」链接。

判据与后端一致(key=accept / name=接单),优先取过程配置里的节点、回退记录上的节点标识。
2026-10-06 12:36:37 +08:00
weicw-dshsa 1699ae873f ✨ 运单详情打卡详情支持编辑打卡记录
打卡时间轴每条记录后面加「编辑」文本按钮,弹窗回显当前填写的信息,可改可存;
打卡状态也在可编辑之列。

- 字段按过程配置取舍:打卡定位决定地址/经纬度,上传货量类型决定重量/体积/数量,
  上传凭证决定照片;记录里已有值的项即使配置后来改小也保留,否则历史数据再也改不掉
- 接单/回单是确认节点,过程配置里没有打卡定位开关,不采信其模板默认的 location
- 未打卡保存即清除该节点打卡流水,弹窗有对应提示;在途打卡表只有单张照片字段,
  上传时只留一张
- 入口与运单编辑同权限(waybill_manage_edit),管理员天然可见
2026-10-06 04:41:22 +08:00
weicw-dshsa 462f9bbea2 🐛 修复打卡时间轴异常标签随记录重排错位
时间轴 key 与异常索引共用 `${nodeCode}-${index}`,带上了序号。时间轴按打卡时间
排序,序号随顺序变化,异常标签就会挂到别的节点上或整个消失——编辑打卡时间或把
未打卡改成已打卡都会触发,实测改完「卸货」后它的「已处理异常/详情」直接不见了。

key 改为只认节点标识(nodeCode → id → 兜底序号),异常索引与 v-for 同源,
顺序怎么变都对得上。顺带修好原本就错位的「到场」「装货」:它们的异常标签此前
一直没显示出来。
2026-10-06 04:41:13 +08:00
weicw-dshsa d62d3da60e 🐛 配载管理打卡详情删除在途节点的「在途」标签
该标签与运单管理打卡详情是同一段代码的副本,此前只改了运单管理侧,配电单侧
仍旧渲染,节点名已表明是在途。
2026-10-05 15:25:53 +08:00
weicw-dshsa 50b8abf85c 🐛 修复 pnpm-workspace 中 allowBuilds 的占位值
三个包的 allowBuilds 值是 pnpm 生成的占位文本「set this to true or false」,
不是布尔值,全新 clone 后安装依赖会失败;按实际需要改为 true。
2026-10-05 15:13:39 +08:00
weicw-dshsa 509f5e163c ✨ 打卡详情展示异常状态并新增异常节点展示
运单详情「打卡详情」时间轴按节点关联异常处置:存在未了结异常时显示「异常」标签
与「跟进」链接,全部处理完成时显示「已处理异常」与「详情」链接。异常跟节点走,
节点未打卡也可能有异常。

异常来源改用异常处置表而非打卡行的 exception_flag —— 后者由打卡接口写入且未随
异常上报维护,实测有异常的节点该值仍为 0,原「异常」标签从未显示过。

「已处理」的口径与后端打卡门禁一致(已完成、已撤销都放行),故「处理中」仍算
未处理。

异常节点字段补入各处异常信息:运单详情「异常上报」表格、编辑弹窗、跟进/详情
弹窗,统一放在异常类型之前;表格内空节点显示「-」而不是留白(存量异常无节点)。

过程节点列显示配置中的中文名而非原始 key(库里混存 key 与中文名两种格式)。

删除打卡详情中在途节点的「在途」标签 —— 节点名已表明是在途。
2026-10-05 15:13:34 +08:00
weicw-dshsa 0e853ea751 🐛 关闭内层弹窗时一并复位异常处置弹窗
closeInnerDialogs 逐个复位了页面内其余弹窗,异常处置弹窗当时未列入。
2026-10-05 08:46:25 +08:00
weicw-dshsa d67240bc3b ✨ 运单详情页新增异常上报卡片
在运费信息之后展示该运单关联的异常处置记录,表格沿用异常处置页的操作与权限,
列只保留上报人、异常类型、上报说明、承运商、最新跟进说明、状态。
跟进/编辑/详情弹窗在本页内完成,操作后局部刷新该卡片,不跳转列表页。
2026-10-05 08:43:01 +08:00
weicw-dshsa d0770c8e83 🎨 详情弹窗按运单/异常拆成两块并统一成描述列表
原「异常信息」一块混了运单字段与上报字段,且用的是 dl/dt/dd 自定义栅格。
现拆成「运单信息」与「异常信息」两个 section-card,都用 el-descriptions,
与编辑弹窗的分区口径和视觉一致。

拆出的两个字段配置与编辑弹窗同源同序,只读展示;原 detailFields 的十个字段
一个不少。同时删掉因此不再使用的 __detail-grid 样式与它的响应式断点。

原先这些字段带 link: true,渲染成 el-link 但没有任何点击行为,
看着可点却没反应,一并去掉。
2026-10-05 08:18:02 +08:00
weicw-dshsa cc0b889fef ✨ 异常处置补上司机上报时间
详情/跟进弹窗与编辑弹窗的「异常信息」里都加上上报时间,列表也加一列。
列表原本只有上报时间的筛选条件、没有这一列,筛的是看不到的值。

编辑弹窗里上报人与上报时间同属上报行为,一并放进「异常信息」,
从「运单信息」移出,避免同一个事实显示两遍。
2026-10-05 08:09:20 +08:00
weicw-dshsa 3d27e981e1 🎨 异常跟进修排版并支持上传附件
跟进弹窗按编辑弹窗的排法改造:section-card 分组 + el-row/el-col 栅格,
依次为异常信息、现场照片、跟进、历史跟进。

新增跟进附件上传(v-model 走 vehicle-attachment-upload),
历史跟进表补附件列。跟进说明与附件至少填其一,二者都为空时前端拦下,
与后端校验一致。
2026-10-05 01:09:39 +08:00
weicw-dshsa 2994539de4 ✨ 异常处置新增编辑功能
列表操作列新增「编辑」,弹窗内只呈现运单信息与可编辑项,
按项目既有习惯用 section-card 分组、el-descriptions 展示只读信息、
el-row/el-col 排可编辑字段。

异常类型与异常原因的下拉值改为中文原文:库里 exception_type 存大类中文名、
exception_reason 存子类中文名(小程序上报即如此)。原先一级 value 是英文码
(traffic),而查询按该值做精确匹配,导致按异常类型筛选一行都查不到。

现场照片支持增删,上传走与我方 OSS 端点一致的 put-file 接口。
2026-10-05 00:54:22 +08:00
weicw-dshsa 785ad08b7e 📝 车辆审核字段注释去掉事故经过描述
保留「键名对不上不报错」这一约束,去掉对既往现象的铺陈。
2026-10-04 23:55:28 +08:00
weicw-dshsa 8de51e568d 🔀 合并认证审核页面分支到 master
新增认证审核-司机/车辆页(按字段级 diff 对比待审值与生效值)、
审核列表待审核/已通过/已驳回页签、司机审核补手机号、
车辆表单补所有人与人车辆识别代号,并移除旧认证审核入口。
2026-10-04 23:38:45 +08:00
b2894lxlx d22fe4777d 🐛 过程配置与常用线路组织筛选只传单个deptId
多组织账号默认搜索不再拼接逗号分隔 ID,避免列表接口 Long 转换失败。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-04 20:48:03 +08:00
b2894lxlx 7751a735c5 ✨ 业务数量单位改走计量单位,并放开运输计划日期必填
发货模板、运输计划、运单与总单调度从计量单位取 unitName;修复调度提交顶层单位为空,并取消计划开始/结束日期必填。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-04 20:26:35 +08:00
weicw-dshsa 069d0e92ff 🐛 修车辆审核详情字段元数据的旧键名,止期之外不再被长期有效遮蔽
车辆字段整组停留在改造前的旧键名(licenseNo / licenseValidStart / licenseValidEnd /
licenseLongTerm / roadTransportNo / roadTransportValid* / engineNo),而快照早已改名成
drivingLicense* / roadTransportCert* / registration*。键名对不上不报错——每个字段都算「没有值」,
于是车辆审核详情里行驶证与道路运输证两组全显示「—」,真值全落到未翻译的「其他字段」组里
露着 camelCase 键名,审核员既看不到内容、也 diff 不出变更。

- 按 VehicleCertSubmitRequest 重写车辆四组字段(基本信息 / 行驶证 / 道路运输证 / 机动车登记本),
  补上此前漏登记的所有人、外廓尺寸、核定载质量、准牵引总质量、能源类型、业务关系、机动车登记编号与六张图;
- longTermKey 只挂在被长期有效作废掉的那个字段(有效期止、强制报废日期)上:
  起期是独立日期,挂上去会把真实值显示成「长期有效」,还让审核员看不到起期的改动。
2026-10-03 17:57:01 +08:00
weicw-dshsa ce54d21344 ✨ 新增/编辑车辆表单补「所有人」与「车辆识别代号」
车辆表新增这两列(所有人:行驶证登记的单位名称,与「所属组织」是两个字段;
车辆识别代号:司机端车辆认证一直在收这个值,此前车辆档案里没有对应列,
只存在于认证快照中)。管理端表单同步补上,否则两个入口建出来的车字段不一致。

所有人为必填(行驶证上的登记主体),车辆识别代号选填、填了校验 17 位。
2026-10-03 16:34:59 +08:00
weicw-dshsa 715c0dc00c ✨ 司机审核与车辆审核新增司机手机号,统一按当前姓名取数
两个审核页的表格与搜索栏都加上「司机手机号」,姓名列改绑司机档案的当前值
(原绑提交时的历史姓名)。同时去掉「提交人」列:真实路径下提交人就是司机本人,
两列必然同值,而存量记录本就没有提交人,留着一列空值加同值没有意义。

手机号不脱敏:审核员靠它联系司机核实材料,与司机管理页一致。
2026-10-03 14:36:08 +08:00
weicw-dshsa b8d74de4a4 ✨ 认证审核列表新增待审核/已通过/已驳回页签
审核对象是「一条认证记录」,每次提交产生一条,原列表只能看审核中的,
已通过与已驳回的记录无从查看。

三个页签默认停在待审核;切换页签回到第一页;只有审核中的记录显示「审核」,
终态记录显示「查看」(详情弹窗本就对终态禁用通过/驳回)。
2026-10-02 19:58:17 +08:00
weicw-dshsa 71a6f7c4f8 ♻️ 车辆管理移除旧认证审核入口,认证状态改用统一状态词表
认证审核职责已迁到「认证审核-车辆」页:审核对象是一条认证记录,生效值与
待审值分离后,原来「看车辆行当前值直接通过/驳回」的单条模型语义不再成立。

- vehicle.vue:移除操作列的「审核」入口与认证审核弹窗及其方法、样式,
  同时删除 api/transportCapacity/transport-vehicle.js 的 auditCertification 封装
  (后端旧接口仅保留兼容,前端不再调用)
- 认证状态列与筛选项统一为 审核中 / 已通过 / 已驳回 / 已作废(新增 已作废,
  原先的「认证中/已认证」不再使用),缓存为空时显示「未认证」
- 去掉按证件到期日期推断「已认证 / 已过期」的兜底:是否已认证由生效快照指针
  决定(曾审核通过即永久保持),与证件日期无关;到期信息仍由列表顶部到期标签
  与日期列标色展示
- driver.vue 选择车辆弹窗的车辆认证状态文案一并统一
2026-10-02 17:15:09 +08:00
weicw-dshsa 7c6ac2e5db ✨ 新增认证审核-司机/车辆页,审核详情按字段级 diff 对比待审值与生效值
审核对象由「司机行 / 车辆行」改为「一条认证记录」,故新增两个待审列表页
(挂在运力管理下,path /transportCapacity/cert-audit-driver|vehicle,与页面
文件名一一对应)与审核详情弹窗:

- api/transportCapacity/certification-audit.js:pending-page / detail / audit 三个接口
- utils/cert-audit.js:快照字段元数据(中文名、控件类型、分组)与两侧快照的
  字段级 diff 计算。两侧字段名一致(同一套快照结构),故逐键比较;日期归一化后
  判等、长期有效联动展示、未登记字段兜底进「其他字段」
- components/cert-audit-diff:当前生效值与待审提交值并排展示,差异行高亮并标注
  新增 / 已修改 / 已清空,支持切换只看变更项;从未认证通过(生效快照为空)时
  显示「无对比基准」,不报错
- components/cert-audit-detail:审核详情弹窗(提交信息 + diff + 审核意见),
  通过 / 驳回动作。驳回原因前端先拦一道(必填且不超过 200 字),通过时不提交原因;
  已终态记录不可再审
- components/cert-audit-list + 两个菜单页:分页与按展示键(司机姓名 / 车牌号)筛选,
  审核链接按 cert_certification_audit_{driver,vehicle}_audit 权限点控制

检索过的现有实现:driver.vue / vehicle.vue 的列表与弹窗写法(Avue option + search、
权限点 hasPermission 写法)、image-upload-field、section-card、utils/table-menu.js。
未找到可复用的快照或 diff 实现,故新增 utils/cert-audit.js 纯函数承担比对;
两个列表页除 bizType 与权限点外完全相同,故抽出通用列表组件承接分页/搜索,
避免两页逻辑各自演化。
2026-10-02 17:15:03 +08:00
128 changed files with 9508 additions and 3628 deletions
-1
View File
@@ -1,6 +1,5 @@
.DS_Store
node_modules
pnpm-lock.yaml
/dist
/tests/e2e/videos/
+18
View File
@@ -0,0 +1,18 @@
# 2026-10-13
## 车辆调度弹窗:使用部门级联选择器宽度对齐
- 现象:`/transportCapacity/vehicle-dispatch` 新增/编辑弹窗中,使用部门 `el-cascader` 明显窄于同行车牌号 `el-input`。
- 根因:`el-cascader` 是 `display: inline-block` 宽度按内容收缩(不像 `el-autocomplete` / `el-input` 有 `--el-input-width: 100%` 兜底);而页面里写的 `.vehicle-dispatch-page :deep(.vehicle-dispatch-form-control){width:100%}` 命中不到 —— avue-crud 弹窗默认 `append-to-body: true`(`append-to-body": validData(crud.tableOption.dialogAppendToBody, true)`),DOM Teleport 到 body,已不在 `.vehicle-dispatch-page` 之下。
- 修法(走项目既有约定):
1. `src/option/transportCapacity/vehicle-dispatch.js` 增加 `dialogCustomClass: 'vehicle-dispatch-dialog'`;
2. `src/styles/element-ui.scss` 增加全局规则 `.vehicle-dispatch-dialog .el-form-item__content > .el-cascader { width:100%; max-width:100% }`。
- 自检:dev(2889) `curl /src/styles/element-ui.scss` 与 `/src/option/transportCapacity/vehicle-dispatch.js` 均 200,编译产物含新规则。
- 遗留观察:同文件里 `.vehicle-dispatch-page :deep(.avue--detail ...)` 那一批查看态禁用样式,同样因 Teleport 属死规则(未改,待确认是否需要迁到 `.vehicle-dispatch-dialog` 作用域)。
## 追加修正(同日 20:xx):第一次改完用户反馈「还是没对齐」
- 原因一:选择器写成了 `.el-form-item__content > .el-cascader`(直接子级)。**avue 表单项在 `el-form-item__content` 与控件之间还有两层包裹 div**(`packages/element-plus/form/index3.js`:content > div > [div|elTooltip] > 控件),必须用后代选择器。已改成 `.vehicle-dispatch-dialog .el-form-item__content .el-cascader`。
- 原因二(更隐蔽):option 用 `labelWidth: 'auto'`,**EP 的 auto 是逐项计算**(实测:3 字 54/64px、2 字 40px、4 字无星号 68px、4 字带星号 78px),label 越宽控件可用宽度越小 → 车牌号 input 486px、使用部门 cascader 472px,仍差 14px。已加 `.vehicle-dispatch-dialog .el-form-item__label { flex: 0 0 calc(4em + 24px) !important; width: calc(4em + 24px) !important }`(本弹窗左右两列最长 label 均为 4 汉字)。
- 验证方式(可复用):临时在项目根建 `cascader-width-check.html`,Vite 直接服务 → `import './src/option/...'` 用真实 option + `import './src/styles/element-ui.scss'`,挂 avue-crud 并 `mounted(){ this.$refs.crud.rowAdd() }` 弹出真实弹窗;用 agent-browser(`/Users/gxwebsoft/.workbuddy/binaries/node/workspace/node_modules/.bin/agent-browser`)`open` + `eval` 读 DOM 链路与 `getBoundingClientRect`。修完已删除该临时文件。
- 结果:全部 label 80px,车牌号 input 与使用部门 cascader 均为 left=732 / width=470 / right=1202,完全一致。
+8
View File
@@ -12,6 +12,14 @@
**坑二:宿主 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。
**坑四:avue-crud 弹窗 `append-to-body` 默认 true(`dialog-form3.js`:`validData(tableOption.dialogAppendToBody, true)`),DOM Teleport 到 body。** 于是页面里以页面容器为祖先的 scoped 选择器(`.xxx-page :deep(.yyy)`)**命中不到弹窗内容**,尤其 `el-cascader`(inline-block、宽度按内容收缩,不像 `el-input`/`el-autocomplete` 有 `--el-input-width:100%` 兜底)会明显比同行 input 窄。修法:option 里配 `dialogCustomClass: 'xxx-dialog'`,规则写到 `src/styles/element-ui.scss` 的 `.xxx-dialog ...`(项目既有约定,10+ 处同款);或把基准类放在 **Teleport 内部元素**上(如 ship.vue 的 `.ship-form` 挂在 el-form 上,自身带 scope 属性,照样生效)。
**坑五:avue 表单项控件不是 `.el-form-item__content` 的直接子级。** `form/index3.js` 渲染为 `content > div > [div|elTooltip] > 控件`,所以 `.el-form-item__content > .el-cascader` 这类**直接子级选择器对 avue 表单全部失效**(手写 `<el-form-item>` 才生效),一律用后代选择器。
**坑六:`labelWidth: 'auto'` 在 Element Plus 里逐项计算,不是全表单共享。** 实测同一弹窗:3 字 54/64px、2 字 40px、4 字 68px、4 字带必填星号 78px;label 越宽 → 控件可用宽度越小,同级控件宽度就不一致(车牌号 input 486 vs 使用部门 cascader 472)。要让同一行控件等宽必须统一 label 宽度:`.xxx-dialog .el-form-item__label { flex: 0 0 calc(Nem + 24px) !important; width: calc(Nem + 24px) !important }`(N=本列最长汉字数,与 ship.vue、`.dialog-form--carded` 同款)。
**验证 avue 弹窗 DOM/宽度的可复用办法:** 项目根建临时 html(Vite 直接服务)→ 引真实 option 与 `src/styles/element-ui.scss`,挂 avue-crud 并 `mounted(){ this.$refs.crud.rowAdd() }` 弹出真弹窗;用 `agent-browser`(`/Users/gxwebsoft/.workbuddy/binaries/node/workspace/node_modules/.bin/agent-browser`)`open` + `eval` 读 DOM 链路与 `getBoundingClientRect`,用完删文件。
**坑三:路由表重复定义 → 内容区整页空白(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」重复项。
## 样式硬规则
+2 -5
View File
@@ -6,8 +6,7 @@
"prod": "vite --mode production",
"build": "vite build",
"build:prod": "vite build --mode production",
"serve": "vite preview --host",
"typecheck": "vue-tsc --noEmit"
"serve": "vite preview --host"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
@@ -48,11 +47,9 @@
"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",
"vue-tsc": "^3.3.11"
"vite-plugin-vue-setup-extend": "^0.4.0"
}
}
+41 -72
View File
@@ -4,9 +4,6 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
patchedDependencies:
'@smallwei/avue': 5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db
importers:
.:
@@ -31,16 +28,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))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)
version: 3.9.2(@element-plus/icons-vue@2.3.2(vue@3.5.34))(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(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34)
version: 1.0.4(axios@1.16.0)(vue@3.5.34)
axios:
specifier: ^1.13.5
version: 1.16.0(debug@4.4.3(supports-color@7.2.0))
version: 1.16.0
codemirror:
specifier: ^5.65.21
version: 5.65.21
@@ -128,7 +125,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(supports-color@7.2.0)(vite@5.4.21(sass@1.99.0)(terser@5.46.2))
version: 0.5.1(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))
@@ -407,35 +404,30 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-arm64-musl@1.0.2':
resolution: {integrity: sha512-qwROoDIC9upfvDoRLuPn2aNg9CGW1x0Ygr4k2Or+8paA9d0qBLwk87U+g8KQpoOviKoPoiwl97kvBYuYD7qZoA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-linux-riscv64-gnu@1.0.2':
resolution: {integrity: sha512-fXRjnPihdnbO6qy1QQOgxAonb68A0TCEG7rj1x7v7rxNElsE8EVIKIEUTvyDtU+sthYSbX+8e7g3oZiLGnOmxw==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-gnu@1.0.2':
resolution: {integrity: sha512-nPR97DXhbWIAy7yazF3jc06kEPMqYMLmPzFOVNlwKPfIoSChnI+x7dc0hTLaihz3jxrjL6j4BbA7earxfx4X3g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-musl@1.0.2':
resolution: {integrity: sha512-l7zZY5+jL5qnBZtDz7CoBtY6p7EkHu422g/0zWwrOrzIwWyWxZFRfZZORY1UG7YApymPLx+UbOkN206xXn/c1Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-win32-arm64-msvc@1.0.2':
resolution: {integrity: sha512-yE0koHCFF4PIbMc2o2SEALhnipz7WBISh5glLvQiomtIoCcW0np3H4Lw93ceJAfJttTTeIIWFbwH84F7EVzjMQ==}
@@ -511,42 +503,36 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.6':
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.6':
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.6':
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.6':
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.6':
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.6':
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
@@ -613,79 +599,66 @@ packages:
resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.60.3':
resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.60.3':
resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.60.3':
resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.60.3':
resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.60.3':
resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.60.3':
resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.60.3':
resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.60.3':
resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.60.3':
resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.60.3':
resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.60.3':
resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.60.3':
resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.60.3':
resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==}
@@ -2654,10 +2627,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))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)':
'@smallwei/avue@3.9.2(@element-plus/icons-vue@2.3.2(vue@3.5.34))(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(debug@4.4.3(supports-color@7.2.0))
axios: 0.21.4
countup.js: 1.9.3
dayjs: 1.11.20
element-plus: 2.14.2(vue@3.5.34)
@@ -2793,25 +2766,25 @@ 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@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/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@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@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(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@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/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
is-url: 1.2.4
lodash.throttle: 4.1.1
nanoid: 5.1.11
slate: 0.123.0
nanoid: 3.3.12
slate: 0.72.8
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/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@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)(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@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/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
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@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/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)':
dependencies:
'@types/event-emitter': 0.3.5
'@uppy/core': 2.3.4
@@ -2827,9 +2800,9 @@ snapshots:
lodash.foreach: 4.5.0
lodash.throttle: 4.1.1
lodash.toarray: 4.4.0
nanoid: 5.1.11
nanoid: 3.3.12
scroll-into-view-if-needed: 3.1.0
slate: 0.123.0
slate: 0.72.8
slate-history: 0.115.0(slate@0.123.0)
snabbdom: 3.6.3
@@ -2837,13 +2810,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@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)
'@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@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@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(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@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)(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@3.3.12)(slate@0.72.8)(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@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)(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@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)(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@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)(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))(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@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)(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
@@ -2856,16 +2829,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@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/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@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)(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@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/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@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/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@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)(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@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/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
@@ -2873,22 +2846,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@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/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@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)(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))(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@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)
'@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@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@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@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)':
'@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@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)(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@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/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
@@ -3065,22 +3038,22 @@ snapshots:
asynckit@0.4.0: {}
avue-plugin-ueditor@1.0.4(axios@1.16.0(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34):
avue-plugin-ueditor@1.0.4(axios@1.16.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(debug@4.4.3(supports-color@7.2.0))
axios: 1.16.0
vue: 3.5.34
axios@0.21.4(debug@4.4.3(supports-color@7.2.0)):
axios@0.21.4:
dependencies:
follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0))
follow-redirects: 1.16.0
transitivePeerDependencies:
- debug
axios@1.16.0(debug@4.4.3(supports-color@7.2.0)):
axios@1.16.0:
dependencies:
follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0))
follow-redirects: 1.16.0
form-data: 4.0.5
proxy-from-env: 2.1.0
transitivePeerDependencies:
@@ -3248,11 +3221,9 @@ snapshots:
dayjs@1.11.20: {}
debug@4.4.3(supports-color@7.2.0):
debug@4.4.3:
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 7.2.0
delayed-stream@1.0.0: {}
@@ -3460,9 +3431,7 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)):
optionalDependencies:
debug: 4.4.3(supports-color@7.2.0)
follow-redirects@1.16.0: {}
form-data@4.0.5:
dependencies:
@@ -4157,10 +4126,10 @@ snapshots:
uuid@8.3.2: {}
vite-plugin-compression@0.5.1(supports-color@7.2.0)(vite@5.4.21(sass@1.99.0)(terser@5.46.2)):
vite-plugin-compression@0.5.1(vite@5.4.21(sass@1.99.0)(terser@5.46.2)):
dependencies:
chalk: 4.1.2
debug: 4.4.3(supports-color@7.2.0)
debug: 4.4.3
fs-extra: 10.1.0
vite: 5.4.21(sass@1.99.0)(terser@5.46.2)
transitivePeerDependencies:
+3 -3
View File
@@ -2,9 +2,9 @@ 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
'@parcel/watcher': true
es5-ext: true
esbuild: true
patchedDependencies:
'@smallwei/avue': patches/@smallwei__avue.patch
-11
View File
@@ -58,14 +58,3 @@ export const changeStatus = (id, status) => {
},
});
};
export const getBfiExchangeRate = (currencyCode, effectdate) => {
return request({
url: '/blade-transport/bfi/exchange-rate',
method: 'get',
params: {
currencyCode,
effectdate,
},
});
};
+8
View File
@@ -90,3 +90,11 @@ export const expireStats = params =>
method: 'get',
params,
});
/** 按合同负责部门预览下一合同编号 */
export const nextContractNo = organizationId =>
request({
url: `${baseUrl}/next-contract-no`,
method: 'get',
params: { organizationId },
});
+11
View File
@@ -52,6 +52,17 @@ export const createFileTask = data =>
request({ url: `${fileTaskBaseUrl}/createFileTask`, method: 'post', data });
export const getPartUploadUrl = (id, partNumber) =>
request({ url: `${fileTaskBaseUrl}/getUploadUrl`, method: 'get', params: { id, partNumber } });
export const uploadPartByProxy = (id, partNumber, partBlob) => {
const formData = new FormData();
formData.append('file', partBlob, `part-${partNumber}.bin`);
return request({
url: `${fileTaskBaseUrl}/uploadPart`,
method: 'post',
params: { id, partNumber },
data: formData,
timeout: 600000,
});
};
export const updateFileTask = data =>
request({ url: `${fileTaskBaseUrl}/updateFileTask`, method: 'post', data });
export const pauseFileTask = ids =>
+8
View File
@@ -34,6 +34,14 @@ export const getPunchRecords = waybillId =>
params: { waybillId },
});
/** 编辑打卡记录(含打卡状态;punched=false 表示取消打卡) */
export const savePunchRecord = data =>
request({
url: `${baseUrl}/punch-record/save`,
method: 'post',
data,
});
/** 运单车辆实时定位 */
export const locateVehicle = id =>
request({
+1 -1
View File
@@ -22,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', timeout: 30000, params: { id } });
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
export const invoiceTypeOptions = [
{ label: '电子专票', value: 'electronic_special' },
+8 -18
View File
@@ -5,17 +5,8 @@ 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 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 getInvoicePool = keyword =>
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params: { keyword } });
export const getSettlementCandidates = (keyword, receiptId) =>
request({
url: `${baseUrl}/settlement-candidates`,
@@ -34,8 +25,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 pushKingdee = ids =>
request({ url: `${baseUrl}/push-kingdee`, method: 'post', timeout: 30000, data: ids });
export const syncKingdee = id =>
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
export const approvalStatusOptions = [
{ label: '草稿', value: 'draft' },
@@ -45,9 +36,8 @@ export const approvalStatusOptions = [
{ label: '已作废', value: 'voided' },
];
export const kingdeePushStatusOptions = [
{ label: '未推送', value: 'unsynced' },
{ label: '已保存收票单', value: 'pushed' },
{ label: '已指定应付', value: 'assigned' },
{ label: '推送失败', value: 'failed' },
export const kingdeeStatusOptions = [
{ label: '未同步', value: 'unsynced' },
{ label: '已同步', value: 'synced' },
{ label: '同步失败', value: 'failed' },
];
+2 -10
View File
@@ -16,15 +16,10 @@ 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 });
// 同步金蝶涉及多次金蝶远程接口调用(A103-2 预检 + A103-1/A103-3 推送),单笔可达 20s+,覆盖全局 10s 超时
export const syncKingdee = id =>
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
// 批量同步逐单单据级事务串行推送,耗时随单量线性增长
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', 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 });
request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', data: ids });
export const paymentTypeOptions = [
{ label: '项目预付', value: 'project_advance' },
@@ -41,8 +36,5 @@ 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,24 +8,14 @@ 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' },
];
+1 -16
View File
@@ -12,25 +12,10 @@ export const getSettlementCandidates = (keyword, flowId) =>
params: { 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 sync = data => request({ url: `${baseUrl}/sync`, method: 'post', data });
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' },
];
+1 -4
View File
@@ -30,9 +30,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 });
// 同步金蝶涉及多次金蝶远程接口调用(A102-4 预检 + A102-2 保存),可达 20s+,覆盖全局 10s 超时
export const syncKingdee = id =>
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
export const getDetailFees = detailId =>
request({ url: `${baseUrl}/detail-fees`, method: 'get', params: { detailId } });
export const getReceiptClaims = formalSettlementId =>
@@ -49,5 +48,3 @@ 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 -2
View File
@@ -57,26 +57,36 @@ export const clearNonTopDept = signal => {
});
};
export const syncOaCompany = (current = 1, size = 20, signal) => {
export const syncOaCompany = (current = 1, size = 20, signal, incremental = false, modifiedSince) => {
return request({
url: '/blade-system/dept/sync-oa-company',
method: 'post',
params: {
current,
size,
incremental: !!incremental,
modifiedSince: modifiedSince || undefined,
},
timeout: 60000,
signal,
});
};
export const syncOaDepartment = (current = 1, size = 20, signal) => {
export const syncOaDepartment = (
current = 1,
size = 20,
signal,
incremental = false,
modifiedSince
) => {
return request({
url: '/blade-system/dept/sync-oa-department',
method: 'post',
params: {
current,
size,
incremental: !!incremental,
modifiedSince: modifiedSince || undefined,
},
timeout: 60000,
signal,
+62
View File
@@ -0,0 +1,62 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/mp-user/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-system/mp-user/detail',
method: 'get',
params: {
id,
},
});
};
export const submit = row => {
return request({
url: '/blade-system/mp-user/submit',
method: 'post',
data: row,
});
};
export const remove = ids => {
return request({
url: '/blade-system/mp-user/remove',
method: 'post',
params: {
ids,
},
});
};
export const changeStatus = (id, status) => {
return request({
url: '/blade-system/mp-user/change-status',
method: 'post',
params: {
id,
status,
},
});
};
export const resetPassword = userIds => {
return request({
url: '/blade-system/mp-user/reset-password',
method: 'post',
params: {
userIds,
},
});
};
+14 -1
View File
@@ -31,13 +31,15 @@ export const add = row => {
});
};
export const syncIamAccounts = (current = 1, size = 50, signal) => {
export const syncIamAccounts = (current = 1, size = 50, signal, incremental = false, modifiedSince) => {
return request({
url: '/blade-system/user/sync-iam-accounts',
method: 'post',
params: {
current,
size,
incremental: !!incremental,
modifiedSince: modifiedSince || undefined,
},
timeout: 60000,
signal,
@@ -52,6 +54,17 @@ export const update = row => {
});
};
export const changeStatus = (id, status) => {
return request({
url: '/blade-system/user/change-status',
method: 'post',
params: {
id,
status,
},
});
};
export const updatePlatform = (userId, userType, userExt) => {
return request({
url: '/blade-system/user/update-platform',
+2
View File
@@ -9,6 +9,8 @@ export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get'
export const follow = data => request({ url: `${baseUrl}/follow`, method: 'post', data });
export const update = data => request({ url: `${baseUrl}/update`, method: 'post', data });
export const complete = data => request({ url: `${baseUrl}/complete`, method: 'post', data });
export const batchComplete = ids =>
@@ -0,0 +1,49 @@
import request from '@/axios';
/**
* 认证记录列表:按审核状态分页,按提交时间倒序
* 审核对象是**一条认证记录**,不是一辆车或一名司机,故必须带 bizType 区分
* status 为审核状态(0审核中 1已通过 2已驳回 3已作废);不传时后端按 0 处理
*/
export const getPendingPage = (bizType, current, size, params) => {
return request({
url: '/blade-transport/certification-audit/pending-page',
method: 'get',
params: {
...params,
bizType,
current,
size,
},
});
};
/**
* 审核详情:同时返回本次待审快照与该对象当前生效快照,供前端做字段级对比
* 从未认证通过时 effectiveSnapshotJson 为空串
*/
export const getAuditDetail = id => {
return request({
url: '/blade-transport/certification-audit/detail',
method: 'get',
params: {
id,
},
});
};
/**
* 审核一条认证记录
* 通过时不传驳回原因;驳回时必须传,且原因不超过 200 字
*/
export const auditCertification = (id, pass, rejectReason) => {
return request({
url: '/blade-transport/certification-audit/audit',
method: 'post',
params: {
id,
pass,
...(rejectReason ? { rejectReason } : {}),
},
});
};
@@ -51,14 +51,6 @@ export const changeStatus = (id, status) => {
});
};
export const auditCertification = (id, certificationStatus, rejectReason) => {
return request({
url: '/blade-transport/transport-vehicle/audit-certification',
method: 'post',
params: { id, certificationStatus, rejectReason },
});
};
export const getExpiryStat = params => {
return request({
url: '/blade-transport/transport-vehicle/expiry-stat',
-22
View File
@@ -152,28 +152,6 @@ 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',
+9 -10
View File
@@ -189,7 +189,14 @@ axios.interceptors.response.use(
// 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。
if (isJsonContentType(res) && !isPlainObject(res.data)) {
try {
const text = res.data instanceof Blob ? await res.data.text() : res.data;
let text = '';
if (typeof res.data?.text === 'function') {
text = await res.data.text();
} else if (typeof res.data === 'string') {
text = res.data;
} else if (typeof res.data?.arrayBuffer === 'function') {
text = new TextDecoder().decode(await res.data.arrayBuffer());
}
const parsed = typeof text === 'string' && text ? JSON.parse(text) : null;
if (parsed && typeof parsed === 'object') {
res.data = parsed;
@@ -264,18 +271,10 @@ axios.interceptors.response.use(
},
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]",用户只能看到无意义的通用提示。
return Promise.reject(error);
}
);
+67 -5
View File
@@ -16,7 +16,11 @@
<div class="address-map-picker__info">{{ status }}</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :disabled="!selected.longitude || resolving" @click="confirm">
<el-button
type="primary"
:disabled="!hasCoordinateValue(selected.longitude) || !hasCoordinateValue(selected.latitude) || resolving"
@click="confirm"
>
确定
</el-button>
</template>
@@ -271,7 +275,12 @@ export default {
this.runGeocode('address', lnglat)
.then(result => {
const detailAddress = result.regeocode?.formattedAddress || fallbackAddress;
this.selected = { ...this.selected, address: detailAddress };
const region = this.resolveRegionFromGeocode(result);
this.selected = {
...this.selected,
address: detailAddress,
...region,
};
this.keyword = detailAddress || this.keyword;
this.status = detailAddress || '已选点,可确认回填';
})
@@ -285,6 +294,26 @@ export default {
this.resolving = false;
});
},
resolveRegionFromGeocode(result = {}) {
const component = result.regeocode?.addressComponent || {};
const city = Array.isArray(component.city) ? '' : String(component.city || '').trim();
const province = String(component.province || '').trim();
const district = String(component.district || '').trim();
const country = String(component.country || '').trim();
const adcode = String(component.adcode || '').trim();
return {
country,
province,
city: city || province,
district,
adcode,
regionCode: adcode,
provinceName: province,
cityName: city || province,
districtName: district,
countryName: country,
};
},
renderMarker(point) {
this.clearMarker();
this.marker = new window.AMap.Marker({ position: point });
@@ -297,8 +326,31 @@ export default {
this.marker = null;
}
},
syncSelectedFromMarker() {
// 确认时以当前标记点坐标为准,避免换地址后仍回填上一次经纬度
if (!this.marker || typeof this.marker.getPosition !== 'function') {
return;
}
const point = this.marker.getPosition();
if (!point) {
return;
}
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))) {
return;
}
this.selected = {
...this.selected,
longitude: Number(longitude),
latitude: Number(latitude),
};
},
confirm() {
if (!this.selected.longitude || !this.selected.latitude) {
this.syncSelectedFromMarker();
if (!this.hasCoordinateValue(this.selected.longitude) || !this.hasCoordinateValue(this.selected.latitude)) {
this.$message.warning('请先搜索或点击地图完成选点');
return;
}
@@ -308,8 +360,18 @@ export default {
}
this.$emit('confirm', {
address: this.selected.address,
longitude: this.selected.longitude,
latitude: this.selected.latitude,
longitude: Number(this.selected.longitude),
latitude: Number(this.selected.latitude),
country: this.selected.country || this.selected.countryName || '',
province: this.selected.province || this.selected.provinceName || '',
city: this.selected.city || this.selected.cityName || '',
district: this.selected.district || this.selected.districtName || '',
adcode: this.selected.adcode || this.selected.regionCode || '',
regionCode: this.selected.regionCode || this.selected.adcode || '',
countryName: this.selected.countryName || this.selected.country || '',
provinceName: this.selected.provinceName || this.selected.province || '',
cityName: this.selected.cityName || this.selected.city || '',
districtName: this.selected.districtName || this.selected.district || '',
});
this.visible = false;
},
+1 -1
View File
@@ -576,7 +576,7 @@ export default {
}
.mode-content :deep(.el-form-item) {
margin-bottom: 14px;
margin-bottom: 24px;
}
.mode-content :deep(.el-form-item:last-child) {
@@ -1,434 +0,0 @@
<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>
+113
View File
@@ -0,0 +1,113 @@
<template>
<div class="avue-crud__right table-header-tools">
<el-button v-if="showRefresh" icon="el-icon-refresh" circle @click="$emit('refresh')" />
<el-button
v-if="showColumn"
icon="el-icon-operation"
circle
@click="columnSettingVisible = true"
/>
<el-button
v-if="showSearch"
icon="el-icon-search"
circle
@click="$emit('update:searchVisible', !searchVisible)"
/>
<el-button v-if="showGrid" icon="el-icon-grid" circle @click="toggleTableSize" />
<el-dialog
v-if="showColumn"
v-model="columnSettingVisible"
title="列设置"
width="360px"
append-to-body
destroy-on-close
>
<el-checkbox-group
class="table-header-tools__column-setting"
:model-value="visibleColumnKeys"
@update:model-value="$emit('update:visibleColumnKeys', $event)"
>
<el-checkbox v-for="item in columns" :key="item.prop" :label="item.prop">
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
<template #footer>
<el-button @click="columnSettingVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
const TABLE_SIZE_OPTIONS = ['default', 'large', 'small'];
export default {
name: 'TableHeaderTools',
props: {
columns: {
type: Array,
default: () => [],
},
visibleColumnKeys: {
type: Array,
default: () => [],
},
tableSize: {
type: String,
default: 'default',
},
searchVisible: {
type: Boolean,
default: true,
},
showRefresh: {
type: Boolean,
default: true,
},
showColumn: {
type: Boolean,
default: true,
},
showSearch: {
type: Boolean,
default: true,
},
showGrid: {
type: Boolean,
default: true,
},
},
emits: ['refresh', 'update:visibleColumnKeys', 'update:tableSize', 'update:searchVisible'],
data() {
return {
columnSettingVisible: false,
};
},
methods: {
toggleTableSize() {
const index = TABLE_SIZE_OPTIONS.indexOf(this.tableSize);
const next = TABLE_SIZE_OPTIONS[(index + 1) % TABLE_SIZE_OPTIONS.length];
this.$emit('update:tableSize', next);
},
},
};
</script>
<style scoped lang="scss">
.table-header-tools.avue-crud__right {
width: auto;
margin: 0 0 0 auto;
flex: none;
white-space: nowrap;
text-align: right;
}
.table-header-tools__column-setting {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 360px;
overflow: auto;
}
</style>
+14 -139
View File
@@ -9,18 +9,21 @@
<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>
<el-table-column
label="文件名"
min-width="240"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<span
class="vehicle-attachment-table__file-name"
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(row)"
@keydown.enter="previewAttachment(row)"
@keydown.space.prevent="previewAttachment(row)"
>
{{ attachmentName(row) }}
</span>
>{{ attachmentName(row) }}</span>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220">
@@ -40,23 +43,9 @@
</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>
<el-table-column v-if="!readonly" label="操作" width="100" align="center" fixed="right">
<template #default="{ $index }">
<el-link type="danger" @click="removeAttachment($index)">删除</el-link>
</template>
</el-table-column>
</el-table>
@@ -82,7 +71,6 @@
width="90%"
top="4vh"
class="vehicle-attachment-table__viewer-dialog"
@closed="handlePreviewClosed"
>
<pdf-preview
v-if="documentPreviewVisible && previewFile.url && previewFile.isPdf"
@@ -123,11 +111,6 @@ 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',
@@ -173,16 +156,6 @@ export default {
type: Number,
default: 500,
},
// 金蝶侧附件(piaozone path 型)代理下载所依赖的发票号;行内自带 invoiceNo 时优先用行内值
kingdeeInvoiceNo: {
type: String,
default: '',
},
// 查看态也展示「查看/下载」操作列(编辑态始终展示,含删除)
showViewActions: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue'],
data() {
@@ -194,10 +167,6 @@ export default {
imagePreviewIndex: 0,
documentPreviewVisible: false,
previewFile: {},
// 金蝶侧附件代理请求进行中(行级按钮共享 loading 守卫,防重复触发)
kingdeeBusy: false,
// 金蝶附件预览用的 objectURL,弹窗关闭时回收
kingdeeObjectUrl: '',
viewerPlugins,
viewerToolbar: {
download: true,
@@ -232,12 +201,6 @@ export default {
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('.');
@@ -285,10 +248,6 @@ export default {
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('附件地址为空,无法下载');
@@ -296,47 +255,20 @@ export default {
}
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) {
const downloadableRows = rows.filter(row => this.attachmentUrl(row));
if (!downloadableRows.length) {
this.$message.warning(
this.selectedRows.length ? '所选附件暂无可下载地址' : '暂无可下载附件'
);
return;
}
urlRows.forEach((row, index) => {
downloadableRows.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('附件地址为空,无法预览');
@@ -359,42 +291,6 @@ export default {
};
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('当前文件暂不支持在线预览');
},
@@ -415,27 +311,6 @@ export default {
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;
+4
View File
@@ -305,6 +305,10 @@ export const createOption = () => ({
searchOrder: 3,
searchPlaceholder: '请选择',
dicData: [],
props: {
label: 'label',
value: 'value',
},
filterable: true,
hide: true,
display: false,
+3
View File
@@ -2,6 +2,9 @@ const dimensionOptions = [
{ label: '重量', value: '重量' },
{ label: '体积', value: '体积' },
{ label: '数量', value: '数量' },
{ label: '整车', value: '整车' },
{ label: '里程', value: '里程' },
{ label: '吨·公里', value: '吨·公里' },
];
const statusOptions = [
+1
View File
@@ -117,6 +117,7 @@ export const contractCategoryOptions = [
export const signTypeOptions = [
{ label: '新签', value: '新签' },
{ label: '续签', value: '续签' },
{ label: '补充协议', value: '补充协议' },
];
export const contractStageOptions = [
+39 -10
View File
@@ -6,6 +6,7 @@ import {
contractStageOptions,
createCrudOption,
effectiveTypeOptions,
nonNegativeRule,
selectRule,
signTypeOptions,
textRule,
@@ -61,13 +62,14 @@ export const config = {
['contractNo', '合同编号'],
['contractName', '合同名称'],
['projectName', '所属项目'],
['organizationName', '所属组织'],
['organizationName', '合同负责部门'],
['contractCategory', '合同类别'],
['signType', '签约类型'],
['partyA', '甲方'],
['partyB', '乙方'],
['handlerUserName', '经办人'],
['signDate', '签订日期'],
['applyDate', '申请日期'],
['contractFormat', '合同格式'],
['legalSealFlag', '是否需要加盖法人章'],
['copyCount', '一式(份)'],
@@ -76,7 +78,8 @@ export const config = {
['templateFlag', '是否范本'],
['originalContractNo', '原件合同编号'],
['electronicSealFlag', '是否电子章'],
['remark', '备注', 2],
['mainContent', '合同主要内容', 2],
['remark', '其他事项说明', 2],
],
},
{
@@ -209,7 +212,7 @@ export const config = {
label: '流程',
statusProp: 'approvalStatus',
status: ['approved', 'change_approved'],
stage: ['formal', 'terminated'],
stage: ['temporary', 'formal', 'terminated'],
permission: 'contract_manage_view',
},
{
@@ -294,17 +297,17 @@ export const option = createCrudOption([
display: false,
},
{
label: '所属组织',
label: '合同负责部门',
prop: 'organizationName',
search: true,
searchOrder: 3,
searchslot: true,
searchPlaceholder: '请选择 所属组织',
searchPlaceholder: '请选择 合同负责部门',
formslot: true,
order: 120,
dicData: [],
minWidth: 150,
rules: selectRule('所属组织'),
rules: selectRule('合同负责部门'),
},
{
label: '合同类别',
@@ -435,6 +438,17 @@ export const option = createCrudOption([
minWidth: 130,
rules: [{ required: true, message: '请选择签订日期', trigger: 'change' }],
},
{
label: '申请日期',
prop: 'applyDate',
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
sortable: true,
minWidth: 130,
addDisplay: false,
editDisplay: false,
},
{
label: '结算类型',
prop: 'settlementMode',
@@ -468,11 +482,10 @@ export const option = createCrudOption([
{ label: '否', value: 0 },
{ label: '是', value: 1 },
],
value: 0,
clearable: true,
hide: true,
order: 70,
minWidth: 100,
rules: selectRule('是否需要加盖法人章'),
},
{
label: '一式(份)',
@@ -500,6 +513,10 @@ export const option = createCrudOption([
hide: true,
order: 45,
minWidth: 130,
rules: [
{ required: true, message: '请输入合同金额', trigger: 'blur' },
nonNegativeRule('合同金额'),
],
},
{
label: '是否范本',
@@ -617,7 +634,19 @@ export const option = createCrudOption([
minWidth: 110,
},
{
label: '备注',
label: '合同主要内容',
prop: 'mainContent',
type: 'textarea',
span: 24,
minRows: 4,
hide: true,
order: 41,
maxlength: 1000,
showWordLimit: true,
rules: textRule('合同主要内容', 1000),
},
{
label: '其他事项说明',
prop: 'remark',
type: 'textarea',
span: 24,
@@ -626,7 +655,7 @@ export const option = createCrudOption([
order: 40,
maxlength: 2000,
showWordLimit: true,
rules: textRule('备注', 2000),
rules: textRule('其他事项说明', 2000),
},
{
label: '',
+1 -1
View File
@@ -71,7 +71,7 @@ export const config = {
action: 'mkAudit',
label: '流程',
statusProp: 'approvalStatus',
status: ['reviewing', 'change_reviewing'],
status: ['reviewing', 'change_reviewing', 'approved', 'change_approved'],
permission: 'project_apply_approve',
mkOnly: true,
},
-2
View File
@@ -400,7 +400,6 @@ export const option = {
span: 6,
order: 350,
minWidth: 150,
rules: [{ required: true, message: '请选择计划开始日期', trigger: 'change' }],
},
{
label: '计划结束日期',
@@ -411,7 +410,6 @@ export const option = {
span: 6,
order: 340,
minWidth: 150,
rules: [{ required: true, message: '请选择计划结束日期', trigger: 'change' }],
},
{
label: '数据来源',
+19
View File
@@ -98,6 +98,24 @@ const getProcessConfigNodes = row => {
const isAcceptProcessNode = node => node.key === 'accept' || node.name === '接单';
/**
* 过程节点列显示文案。
*
* 该列在库里存的是「原始值」,且历史上写入了两种格式:司机打卡推进写的是配置的
* key(unload / dispatch / load …),配载单新建默认值写的是中文名(接单)。
* 故这里按两种格式都认一次,认不出就原样显示,不把值吞掉。
*
* 运单创建后、司机首次打卡前该字段为空,属正常——显示「-」,避免留白看不出是没值还是没渲染。
*/
const formatCurrentProcessNode = row => {
const value = String(getFirstValue(row, ['currentProcessNode', 'currentNode']) || '').trim();
if (!value) return '-';
const node = getProcessConfigNodes(row).find(
item => item.key === value || item.name === value
);
return node?.name || node?.key || value;
};
const isTruthyFlag = value => {
if (value === true || value === 1) return true;
if (value === false || value === 0 || value === null || value === undefined || value === '') {
@@ -608,6 +626,7 @@ export const option = {
{
label: '过程节点',
prop: 'currentProcessNode',
formatter: row => formatCurrentProcessNode(row),
minWidth: 140,
addDisplay: false,
editDisplay: false,
-2
View File
@@ -13,8 +13,6 @@ export const invoiceApplicationTableColumns = [
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
{ prop: 'kingdeeLockStatus', label: '金蝶锁状态', minWidth: 150 },
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
];
export const invoiceApplicationDetailColumns = [
+2 -4
View File
@@ -11,8 +11,6 @@ export const invoiceReceiptTableColumns = [
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
{ prop: 'createUserName', label: '创建人', minWidth: 110 },
{ prop: 'createTime', label: '创建时间', minWidth: 170 },
{ prop: 'kingdeeReceivedInvoiceNo', label: '金蝶单据号', minWidth: 150 },
{ prop: 'kingdeePushStatusName', label: '金蝶状态', minWidth: 130 },
{ prop: 'kingdeePushMsg', label: '失败原因', minWidth: 160 },
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
];
-2
View File
@@ -17,6 +17,4 @@ export const paymentApplicationTableColumns = [
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 120 },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 130 },
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 130 },
{ prop: 'kingdeeLockStatus', label: '金蝶锁状态', minWidth: 150 },
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
];
-6
View File
@@ -8,11 +8,5 @@ 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 },
];
@@ -21,8 +21,6 @@ export const formalSettlementTableColumns = [
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 },
{ label: '金蝶单据状态', prop: 'kingdeeSyncStatus', minWidth: 135 },
{ label: '金蝶锁状态', prop: 'kingdeeLockStatus', minWidth: 150 },
{ label: '附件同步', prop: 'attachmentSyncStatusName', minWidth: 100 },
{ label: '创建人', prop: 'createUserName', minWidth: 110 },
{ label: '创建时间', prop: 'createTime', minWidth: 170 },
];
@@ -88,8 +88,6 @@ 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' },
+205
View File
@@ -0,0 +1,205 @@
const identityDic = [
{ label: '司机', value: 'driver' },
{ label: '管理员', value: 'admin' },
];
const statusDic = [
{ label: '启用', value: 1 },
{ label: '停用', value: 0 },
];
const validatePhone = (rule, value, callback) => {
const phone = String(value || '').trim();
if (!phone) {
callback(new Error('请输入手机号'));
return;
}
if (!/^1\d{10}$/.test(phone)) {
callback(new Error('请输入正确的手机号'));
return;
}
callback();
};
const validateRemark = (rule, value, callback) => {
if (String(value || '').length > 200) {
callback(new Error('备注不能超过200字'));
} else {
callback();
}
};
export default {
height: 'auto',
calcHeight: 22,
dialogWidth: 760,
labelPosition: 'right',
labelWidth: 'auto',
tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
saveBtnText: '提交',
updateBtnText: '提交',
searchShow: true,
searchIcon: true,
searchLabelWidth: 160,
searchMenuSpan: 24,
searchIndex: 4,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
indexWidth: 70,
viewBtn: false,
delBtn: false,
editBtn: false,
addBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 320,
column: [
{
label: '手机号',
prop: 'phone',
minWidth: 130,
search: true,
searchOrder: 4,
searchSpan: 6,
searchPlaceholder: '请输入',
maxlength: 11,
showWordLimit: true,
order: 100,
rules: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ validator: validatePhone, trigger: 'blur' },
],
},
{
label: '账号',
prop: 'account',
minWidth: 130,
search: true,
searchOrder: 3,
searchSpan: 6,
searchPlaceholder: '请输入',
maxlength: 45,
order: 90,
placeholder: '默认与手机号相同',
},
{
label: '姓名',
prop: 'realName',
minWidth: 100,
search: true,
searchOrder: 2,
searchSpan: 6,
searchPlaceholder: '请输入',
maxlength: 10,
showWordLimit: true,
order: 80,
},
{
label: '昵称',
prop: 'name',
minWidth: 100,
maxlength: 20,
showWordLimit: true,
order: 70,
},
{
label: '小程序身份',
prop: 'identities',
type: 'select',
multiple: true,
dataType: 'string',
dicData: identityDic,
minWidth: 140,
search: true,
searchOrder: 1,
searchSpan: 6,
searchPlaceholder: '请选择',
order: 60,
rules: [{ required: true, message: '请选择小程序身份', trigger: 'change' }],
},
{
label: '性别',
prop: 'sex',
type: 'select',
dicUrl: '/blade-system/dict/dictionary?code=sex',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
minWidth: 80,
hide: true,
order: 50,
},
{
label: 'openid',
prop: 'openid',
minWidth: 180,
overHidden: true,
hide: true,
addDisplay: false,
editDisplay: true,
viewDisplay: true,
order: 40,
placeholder: '可清空以解绑微信',
},
{
label: '状态',
prop: 'status',
type: 'select',
dicData: statusDic,
dataType: 'number',
minWidth: 90,
search: true,
searchOrder: 0,
searchSpan: 6,
addDisplay: false,
editDisplay: false,
value: 1,
order: 30,
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
span: 24,
minRows: 2,
maxlength: 200,
showWordLimit: true,
hide: true,
order: 20,
rules: [{ validator: validateRemark, trigger: 'blur' }],
},
{
label: '创建时间',
prop: 'createTime',
minWidth: 170,
addDisplay: false,
editDisplay: false,
viewDisplay: true,
order: 10,
},
{
label: '更新人',
prop: 'updateUserName',
minWidth: 100,
addDisplay: false,
editDisplay: false,
viewDisplay: true,
order: 5,
},
{
label: '更新时间',
prop: 'updateTime',
minWidth: 170,
addDisplay: false,
editDisplay: false,
viewDisplay: true,
order: 1,
},
],
};
+9 -2
View File
@@ -27,7 +27,8 @@ export const userOption = safe => {
index: true,
selection: true,
addBtn: false,
viewBtn: true,
viewBtn: false,
menuWidth: 160,
dialogType: 'dialog',
dialogWidth: '90%',
labelPosition: 'right',
@@ -37,8 +38,11 @@ export const userOption = safe => {
{
label: '账号名',
prop: 'account',
slot: true,
minWidth: 180,
search: true,
searchOrder: 2,
searchPlaceholder: '支持模糊搜索',
display: false,
},
{
@@ -46,6 +50,7 @@ export const userOption = safe => {
prop: 'realName',
search: true,
searchOrder: 4,
searchPlaceholder: '支持模糊搜索',
display: false,
},
{
@@ -53,6 +58,7 @@ export const userOption = safe => {
prop: 'phone',
search: true,
searchOrder: 3,
searchPlaceholder: '支持模糊搜索',
display: false,
},
{ label: '工号', prop: 'code', display: false },
@@ -60,7 +66,8 @@ export const userOption = safe => {
label: '所属部门',
prop: 'deptName',
slot: true,
overHidden: true,
minWidth: 200,
overHidden: false,
display: false,
},
{ label: '岗位', prop: 'postName', overHidden: true, display: false },
+51 -10
View File
@@ -1,7 +1,10 @@
// 一级 value 与二级 value 都用中文原文:库里 exception_type 存的是大类中文名、
// exception_reason 存子类中文名(小程序上报即如此),此处必须与之一致,
// 否则按异常类型做精确匹配的查询会一行都查不到。
export const exceptionCategoryOptions = [
{
label: '交通异常',
value: 'traffic',
value: '交通异常',
children: [
{ label: '道路拥堵', value: '道路拥堵' },
{ label: '交通事故', value: '交通事故' },
@@ -12,7 +15,7 @@ export const exceptionCategoryOptions = [
},
{
label: '车辆异常',
value: 'vehicle',
value: '车辆异常',
children: [
{ label: '车辆故障维修', value: '车辆故障维修' },
{ label: '车辆事故', value: '车辆事故' },
@@ -24,7 +27,7 @@ export const exceptionCategoryOptions = [
},
{
label: '货物异常',
value: 'cargo',
value: '货物异常',
children: [
{ label: '货损', value: '货损' },
{ label: '货差', value: '货差' },
@@ -35,7 +38,7 @@ export const exceptionCategoryOptions = [
},
{
label: '其他异常',
value: 'other',
value: '其他异常',
children: [
{ label: '变更路线地址', value: '变更路线地址' },
{ label: '其他', value: '其他异常-其他' },
@@ -43,6 +46,16 @@ export const exceptionCategoryOptions = [
},
];
/** 一级类型的中文名列表,供编辑表单的级联选择使用 */
export const exceptionTypeOptions = exceptionCategoryOptions.map(item => ({
label: item.label,
value: item.value,
}));
/** 按一级类型取二级选项 */
export const exceptionReasonOptions = typeValue =>
exceptionCategoryOptions.find(item => item.value === typeValue)?.children || [];
export const disposalStatusOptions = [
{ label: '待处理', value: 'pending' },
{ label: '处理中', value: 'processing' },
@@ -65,20 +78,30 @@ export const tableColumns = [
{ label: '项目', prop: 'projectName', minWidth: 160 },
{ label: '车辆', prop: 'vehicleNo', minWidth: 150, link: true },
{ label: '上报人', prop: 'reporterName', minWidth: 120 },
{ label: '上报时间', prop: 'reportTime', minWidth: 170 },
{ label: '异常类型', prop: 'exceptionTypeName', minWidth: 160 },
{ label: '上报说明', prop: 'reportDescription', minWidth: 180 },
{ label: '异常节点', prop: 'nodeName', minWidth: 120 },
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
{ label: '最新跟进说明', prop: 'latestFollowContent', minWidth: 200 },
{ label: '状态', prop: 'disposalStatusName', minWidth: 120 },
];
export const detailFields = [
{ label: '配载单号/总单号', prop: 'loadingOrMasterNo', link: true },
{ label: '运单号', prop: 'waybillNo', link: true },
{ label: '项目', prop: 'projectName', link: true },
{ label: '车牌号', prop: 'vehicleNo', link: true },
{ label: '上报人', prop: 'reporterName' },
// 详情/跟进弹窗的只读信息,按来源分两块:
// 「运单信息」是上报时从运单复制的字段,「异常信息」是司机上报的内容。
// 与编辑弹窗的分区口径一致,改动时两处要一起改。
export const detailWaybillFields = [
{ label: '运单号', prop: 'waybillNo' },
{ label: '配载单号/总单号', prop: 'loadingOrMasterNo' },
{ label: '车牌号', prop: 'vehicleNo' },
{ label: '项目', prop: 'projectName' },
{ label: '承运商', prop: 'carrierName' },
];
export const detailExceptionFields = [
{ label: '上报人', prop: 'reporterName' },
{ label: '上报时间', prop: 'reportTime' },
{ label: '异常节点', prop: 'nodeName' },
{ label: '异常类型', prop: 'exceptionTypeName' },
{ label: '异常原因', prop: 'exceptionReason' },
{ label: '上报说明', prop: 'reportDescription', span: 2 },
@@ -89,3 +112,21 @@ export const followColumns = [
{ label: '跟进人', prop: 'followUserName', minWidth: 130 },
{ label: '跟进时间', prop: 'followTime', minWidth: 170 },
];
// 编辑表单只开放上报四件套。车牌、项目、承运商、配载单号由上报时从运单回填,
// 允许手改会让记录与运单脱钩;处置状态由跟进与完成流转,不在此处。
export const editFormFields = [
{ label: '异常类型', prop: 'exceptionType', type: 'select', required: true },
{ label: '异常原因', prop: 'exceptionReason', type: 'select' },
{ label: '上报说明', prop: 'reportDescription', type: 'textarea', required: true, span: 2 },
];
// 编辑弹窗顶部的运单信息,只读展示,帮助运管员确认改的是哪一单。
// 上报人与上报时间属上报行为,放在「异常信息」里,不在此处重复。
export const editWaybillFields = [
{ label: '运单号', prop: 'waybillNo' },
{ label: '配载单号/总单号', prop: 'loadingOrMasterNo' },
{ label: '车牌号', prop: 'vehicleNo' },
{ label: '项目', prop: 'projectName' },
{ label: '承运商', prop: 'carrierName' },
];
@@ -0,0 +1,51 @@
export const option = {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 4,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
indexWidth: 70,
selection: false,
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: false,
columnBtn: false,
menuWidth: 110,
column: [
{
label: '司机姓名',
prop: 'driverName',
search: true,
searchOrder: 3,
minWidth: 120,
},
{
label: '司机手机号',
prop: 'driverMobile',
search: true,
searchOrder: 2,
minWidth: 130,
},
{
label: '审核状态',
prop: 'statusText',
slot: true,
width: 110,
align: 'center',
},
{
label: '提交时间',
prop: 'submitTime',
slot: true,
minWidth: 170,
align: 'center',
},
],
};
@@ -0,0 +1,58 @@
export const option = {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 4,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
indexWidth: 70,
selection: false,
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: false,
columnBtn: false,
menuWidth: 110,
column: [
{
label: '车牌号',
prop: 'bizKey',
search: true,
searchOrder: 3,
minWidth: 140,
},
{
label: '司机姓名',
prop: 'driverName',
search: true,
searchOrder: 2,
minWidth: 120,
},
{
label: '司机手机号',
prop: 'driverMobile',
search: true,
searchOrder: 1,
minWidth: 130,
},
{
label: '审核状态',
prop: 'statusText',
slot: true,
width: 110,
align: 'center',
},
{
label: '提交时间',
prop: 'submitTime',
slot: true,
minWidth: 170,
align: 'center',
},
],
};
@@ -95,6 +95,8 @@ export const option = {
align: 'center',
},
{
// 这里是「最新一次提交的状态」缓存,不等于该车当前是否已认证:
// 已认证由生效快照指针决定,曾审核通过即永久保持,重新提交不会让它退回审核中
label: '认证状态',
prop: 'certificationStatus',
slot: true,
@@ -104,9 +106,10 @@ export const option = {
type: 'select',
dicData: [
{ label: '全部', value: '' },
{ label: '认证中', value: 0 },
{ label: '已认证', value: 1 },
{ label: '审核中', value: 0 },
{ label: '已通过', value: 1 },
{ label: '已驳回', value: 2 },
{ label: '已作废', value: 3 },
],
minWidth: 110,
align: 'center',
@@ -12,6 +12,9 @@ export const option = {
height: 'auto',
calcHeight: 32,
dialogWidth: 1200,
// 弹窗根节点 class:avue-crud 弹窗默认 append-to-body 到 body,
// 页面内 scoped 样式命中不到弹窗内容,故用该 class 在全局样式里做兜底(见 styles/element-ui.scss)
dialogCustomClass: 'vehicle-dispatch-dialog',
labelPosition: 'right',
labelWidth: 'auto',
tip: false,
@@ -101,6 +104,7 @@ export const option = {
minWidth: 120,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '当前节点',
@@ -108,6 +112,7 @@ export const option = {
minWidth: 120,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '当前处理人',
@@ -115,6 +120,7 @@ export const option = {
minWidth: 130,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '创建人',
@@ -122,6 +128,7 @@ export const option = {
minWidth: 120,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '创建时间',
@@ -132,6 +139,7 @@ export const option = {
minWidth: 170,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
// ========== 新增/编辑表单专用(不进表格)==========
{
@@ -165,6 +173,7 @@ export const option = {
type: 'textarea',
hide: true,
span: 24,
minRows: 2,
maxlength: 200,
showWordLimit: true,
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
+103 -5
View File
@@ -173,7 +173,10 @@
}
.el-dialog .el-col {
// 取消列自身下边距,改由 form-item 统一控制行距;
// flow-root 避免子级 margin 折叠穿透导致换行后行距失效、校验提示被下一行遮挡
margin-bottom: 0;
display: flow-root;
}
.el-dialog .el-form-item,
@@ -181,7 +184,7 @@
.el-dialog .el-form-item--small.el-form-item,
.el-dialog .el-form-item--large.el-form-item,
.el-dialog .el-form-item--medium.el-form-item {
//margin-bottom: 20px;
margin-bottom: 24px;
}
/* 全站弹窗:底部操作栏浮动效果(参照 business/project-apply)
@@ -362,9 +365,14 @@
}
}
// 4) 弹窗内表单项默认上下间距 16px(覆盖默认 18px / 22px,与 customer-archive 对齐)
// 4) 弹窗内表单项上下间距:预留绝对定位校验提示(约 14~18px)的可视空间
.el-dialog .el-form-item {
//margin-bottom: 16px;
margin-bottom: 24px;
}
// 校验提示浮在控件下方,提高层级避免被下一行输入框背景遮挡
.el-form-item__error {
z-index: 2;
}
// 5) 通用分组白卡组件(<section-card>):标题融入卡头
@@ -446,7 +454,7 @@
.dialog-form--carded {
.el-form-item {
align-items: center;
margin-bottom: 14px;
margin-bottom: 24px;
}
.el-form-item__label {
@@ -644,6 +652,28 @@
background: #fff;
}
// 车辆调度新增/编辑/查看弹窗:使用部门 el-cascader 默认 display: inline-block、宽度按内容收缩,
// 会比同行的 el-input(--el-input-width 默认 100%)窄一大截,需显式拉满列宽。
// 两个必须注意的点(踩过):
// 1) avue-crud 弹窗默认 append-to-body 到 body,页面内 `.vehicle-dispatch-page :deep(...)`
// 这类以页面容器为祖先的选择器匹配不到弹窗内容,必须用弹窗自身 class(dialogCustomClass)在全局兜底;
// 2) avue 表单项在 el-form-item__content 与控件之间还有两层包裹 div
// (form/index3.js 渲染:content > div > [div|elTooltip] > 控件),
// 所以要用后代选择器,不能写 `>` 直接子级。
.vehicle-dispatch-dialog .el-form-item__content .el-cascader {
width: 100%;
max-width: 100%;
}
// 车辆调度弹窗 label 宽度统一:option 用 labelWidth: 'auto' 时 label 宽度随字数变化
// (3 字 64px / 2 字 40px / 4 字带星号 78px),控件可用宽度随之变化,
// 于是同一行两列控件宽度不一致(如车牌号 486px vs 使用部门 472px)。
// 本弹窗左右两列最长 label 均为 4 个汉字(所属组织 / 使用部门 / 申请时间),故统一按 4em + 24px 取宽。
.vehicle-dispatch-dialog .el-form-item__label {
flex: 0 0 calc(4em + 24px) !important;
width: calc(4em + 24px) !important;
}
// 过程配置新增/编辑/查看弹窗(手写 el-dialog,参照 vehicle/credit-score-quantification):
// 灰底弹窗体(全站 .el-dialog__body 已统一)内堆叠 <section-card> 分组白卡,
// 卡间 12px 间距由 section-card 自身 margin-bottom 保证,与项目其他弹窗风格统一
@@ -995,6 +1025,66 @@ td.el-table-fixed-column--right {
text-overflow: clip;
}
// 全站统一:内容型长文本列(文件名 / 单号 / 备注等)单元格内也放了 el-link / span.attachment-file-name,
// 会被上面两条「含 el-link 的单元格」规则一并命中,导致:
// ① 上面第 950 行的 display: flex 让链接成为 flex item,flex item 默认 min-width: auto 无法收缩,
// 超长文本(如未规范命名的附件名)会直接把列撑破;
// ② 豁免规则的 overflow: visible 让溢出内容不再被单元格裁切,直接压到相邻列上。
// 因此对「内容列」做显式兜底:单元格自身恢复块级 + 省略号截断,链接参与收缩并显示省略号。
// 用法:在 <el-table-column> 上配置 class-name="is-ellipsis-link"(动态列可 :class-name 绑定)。
// 操作列不要加此类,否则会失去自动换行能力。
.el-table .el-table__body .cell.is-ellipsis-link {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.el-link {
display: inline-block;
min-width: 0;
max-width: 100%;
overflow: hidden;
vertical-align: middle;
// Element Plus 的 el-link__inner 是 inline-flex,flex 容器上的 text-overflow 不生效,
// 必须降级为块级容器,省略号才会真正渲染出来。
.el-link__inner {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.attachment-file-name {
display: inline-block;
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
}
// 附件上传表格:文件名统一用 span,视觉与可点击行为对齐原 el-link primary
.attachment-file-name {
color: var(--el-color-primary, #409eff);
cursor: pointer;
&:hover,
&:focus-visible {
text-decoration: underline;
}
&.is-disabled {
color: var(--el-text-color-placeholder, #c0c4cc);
cursor: not-allowed;
text-decoration: none;
}
}
// 全站统一:Avue 内置行操作按钮(查看 / 编辑 / 删除 / 复制等)视觉对齐为 el-link 链接样式。
// 选择器仅命中表格行操作列 .avue-crud__menu 内的 is-text 按钮,不误伤顶部工具栏(.avue-crud__header)
// 与弹窗按钮(.avue-crud__dialog__menu)。事件(rowView/rowEdit/rowDel)与权限(permission 指令)
@@ -1073,7 +1163,12 @@ td.el-table-fixed-column--right {
align-items: normal;
}
.el-form-item--default {
margin-bottom: 14px;
margin-bottom: 24px;
}
// 独立表单页(非弹窗)同样避免 el-col 折叠吃掉行距
.el-form .el-col {
display: flow-root;
}
// 配套:星号必须保持「高度 auto + 继承行高」,stretch 才会按首行排布;
@@ -1248,3 +1343,6 @@ td.el-table-fixed-column--right {
cursor: not-allowed;
}
}
.is-ellipsis-link {
overflow: hidden;
}
+434
View File
@@ -0,0 +1,434 @@
/**
* 认证审核通用工具
*
* 认证对象(司机 / 车辆)每次提交产生一条不可变快照记录,「审核」针对的是**一条记录**。
* 记录分两侧:本次**待审快照**与该对象**当前生效快照**(最后一次审核通过的那条)。
* 待审内容在审核通过前不影响生效值,所以审核页必须逐字段对比两侧,而不是只看一条当前值。
*
* 本文件提供三件事:
* 1. 审核状态文案与标签(与后端 statusText 同一套词表);
* 2. 快照字段的展示元数据(中文名、控件类型、分组);
* 3. 两侧快照的字段级 diff 计算,含「从未认证通过、无对比基准」的处理。
*
* 两侧快照字段名完全一致(同一套结构),故可逐键比较。
*/
const EMPTY_TEXT = '—';
const TYPE_IMAGE = 'image';
const TYPE_DATE = 'date';
const TYPE_LONG_TERM = 'longTerm';
/** 审核状态取值 */
export const CERT_RECORD_STATUS = {
PENDING: 0,
APPROVED: 1,
REJECTED: 2,
VOIDED: 3,
};
/** 状态文案统一为:审核中 / 已通过 / 已驳回 / 已作废 */
const CERT_STATUS_TAG = {
0: { label: '审核中', type: 'warning' },
1: { label: '已通过', type: 'success' },
2: { label: '已驳回', type: 'danger' },
3: { label: '已作废', type: 'info' },
};
/** 变更类型 → 差异标签 */
export const CERT_CHANGE_TAG = {
added: { label: '新增', type: 'success' },
modified: { label: '已修改', type: 'warning' },
cleared: { label: '已清空', type: 'danger' },
};
/**
* 「最新提交状态」展示缓存 → 标签
*
* 缓存为空表示该对象从未提交过认证(即没有生效快照)。
* 注意它只是最近一次提交的状态,不等于「是否已认证」——后者由生效快照指针决定。
*/
export const certStatusTag = status => {
if (status === null || status === undefined || status === '') {
return { label: '未认证', type: 'info' };
}
return CERT_STATUS_TAG[status] || { label: EMPTY_TEXT, type: 'info' };
};
const pad = value => String(value).padStart(2, '0');
/** 后端若改用时间戳毫秒序列化日期,也能落成日期文案 */
const EPOCH_PATTERN = /^\d{13}$/;
const isEpochText = text => EPOCH_PATTERN.test(text);
const toDate = text => {
const date = new Date(Number(text));
return Number.isNaN(date.getTime()) ? null : date;
};
/** 提交时间等日期时间展示,兼容 `yyyy-MM-dd HH:mm:ss`、ISO 串与时间戳毫秒 */
export const formatCertDateTime = value => {
if (value === null || value === undefined || value === '') return EMPTY_TEXT;
const text = String(value).trim();
const matched = text.match(
/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})[T ](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?/
);
if (matched) {
return (
`${matched[1]}-${pad(matched[2])}-${pad(matched[3])} ` +
`${pad(matched[4])}:${pad(matched[5])}:${pad(matched[6] || 0)}`
);
}
const date = isEpochText(text) ? toDate(text) : null;
if (!date) return text;
return (
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
);
};
/**
* 快照字段元数据
*
* 字段名与后端提交 DTO 的 JSON 键一致(司机 PersonCertSubmitRequest / 车辆 VehicleCertSubmitRequest),
* 两侧快照共用同一套结构,故同时用于标签与比较规则。
*
* `longTermKey` 只挂在**被长期有效作废掉的那个字段**上,也就是各组的「有效期止」与强制报废日期:
* 勾了长期有效,它们显示「长期有效」、比较时按同一个值算(避免每次审核都报「已变更」)。
* 「有效期起」不挂:起期是一个独立的日期,长期有效并不会让它失效,
* 挂上去等于把它自己的值藏起来、还让审核员看不到起期的改动。
*/
const CERT_SNAPSHOT_GROUPS = {
driver: [
{
title: '身份信息',
fields: [
{ key: 'realName', label: '真实姓名' },
{ key: 'idCardNo', label: '身份证号' },
{ key: 'idCardFrontUrl', label: '身份证正面照', type: TYPE_IMAGE },
{ key: 'idCardBackUrl', label: '身份证反面照', type: TYPE_IMAGE },
{ key: 'idCardValidStart', label: '身份证有效期起', type: TYPE_DATE },
{
key: 'idCardValidEnd',
label: '身份证有效期止',
type: TYPE_DATE,
longTermKey: 'idCardLongTerm',
},
{ key: 'idCardLongTerm', label: '身份证长期有效', type: TYPE_LONG_TERM },
{ key: 'headPhoto', label: '大头照', type: TYPE_IMAGE },
],
},
{
title: '驾驶证信息',
fields: [
{ key: 'driverFileNo', label: '驾驶证档案编号' },
{ key: 'driverClass', label: '准驾车型' },
{ key: 'driverLicenseFrontUrl', label: '驾驶证主页照片', type: TYPE_IMAGE },
{ key: 'driverLicenseBackUrl', label: '驾驶证副页照片', type: TYPE_IMAGE },
{ key: 'driverValidStart', label: '驾驶证有效期起', type: TYPE_DATE },
{
key: 'driverValidEnd',
label: '驾驶证有效期止',
type: TYPE_DATE,
longTermKey: 'driverLongTerm',
},
{ key: 'driverLongTerm', label: '驾驶证长期有效', type: TYPE_LONG_TERM },
],
},
{
title: '从业资格证信息',
fields: [
{ key: 'qualificationType', label: '从业资格认证类型' },
{ key: 'qualificationNo', label: '从业资格证号' },
{
key: 'qualificationFront',
label: '从业资格证封面页',
type: TYPE_IMAGE,
legacyKeys: ['qualificationUrl'],
},
{ key: 'qualificationBack', label: '从业资格证内容页', type: TYPE_IMAGE },
{ key: 'qualificationValidStart', label: '从业资格证有效期起', type: TYPE_DATE },
{
key: 'qualificationValidEnd',
label: '从业资格证有效期止',
type: TYPE_DATE,
longTermKey: 'qualificationLongTerm',
},
{ key: 'qualificationLongTerm', label: '从业资格证长期有效', type: TYPE_LONG_TERM },
],
},
],
/*
* 车辆字段名以 VehicleCertSubmitRequest 为准,与 PersonCertSubmitRequest 不同。
* 键名对不上不报错——每个字段都算「没有值」,两组全显示「—」且 diff 不出变更。
*/
vehicle: [
{
title: '车辆基本信息',
fields: [
{ key: 'plateNo', label: '车牌号' },
{ key: 'vehicleType', label: '车辆类型' },
{ key: 'plateColor', label: '车牌颜色' },
{ key: 'ownerName', label: '所有人' },
{ key: 'vin', label: '车辆识别代号(车架号)' },
{ key: 'outerLength', label: '外廓长度(mm)' },
{ key: 'outerWidth', label: '外廓宽度(mm)' },
{ key: 'outerHeight', label: '外廓高度(mm)' },
{ key: 'approvedLoadKg', label: '核定载质量(KG)' },
{ key: 'tractionMassKg', label: '准牵引总质量(KG)' },
{ key: 'energyType', label: '能源类型' },
{ key: 'businessRelation', label: '业务关系' },
],
},
{
title: '行驶证信息',
fields: [
{ key: 'compulsoryScrapDate', label: '强制报废日期', type: TYPE_DATE, longTermKey: 'compulsoryScrapLongTerm' },
{ key: 'compulsoryScrapLongTerm', label: '强制报废长期有效', type: TYPE_LONG_TERM },
{ key: 'drivingLicenseNo', label: '行驶证档案编号' },
{ key: 'drivingLicenseImage', label: '行驶证主页正面', type: TYPE_IMAGE },
{ key: 'drivingLicenseMainBack', label: '行驶证主页反面', type: TYPE_IMAGE },
{ key: 'drivingLicenseViceFront', label: '行驶证副页正面', type: TYPE_IMAGE },
{ key: 'drivingLicenseViceBack', label: '行驶证副页反面', type: TYPE_IMAGE },
{ key: 'drivingLicenseStartDate', label: '行驶证有效期起', type: TYPE_DATE },
{
key: 'drivingLicenseEndDate',
label: '行驶证有效期止',
type: TYPE_DATE,
longTermKey: 'drivingLicenseLongTerm',
},
{ key: 'drivingLicenseLongTerm', label: '行驶证长期有效', type: TYPE_LONG_TERM },
],
},
{
title: '道路运输证信息',
fields: [
{ key: 'roadTransportCertNo', label: '道路运输证号' },
{ key: 'roadTransportCertImage', label: '道路运输证照片', type: TYPE_IMAGE },
{ key: 'roadTransportCertStartDate', label: '道路运输证有效期起', type: TYPE_DATE },
{
key: 'roadTransportCertEndDate',
label: '道路运输证有效期止',
type: TYPE_DATE,
longTermKey: 'roadTransportCertLongTerm',
},
{ key: 'roadTransportCertLongTerm', label: '道路运输证长期有效', type: TYPE_LONG_TERM },
],
},
{
title: '机动车登记本信息',
fields: [
{ key: 'registrationNo', label: '机动车登记编号' },
{ key: 'registrationImage', label: '机动车登记本照片', type: TYPE_IMAGE },
],
},
],
};
/** 提交元信息与主键不参与对比(两侧必相同,展示出来只是噪音) */
const IGNORED_KEYS = [
'id',
'bizType',
'bizId',
'bizKey',
'certificationStatus',
'snapshotJson',
'pendingSnapshotJson',
'effectiveSnapshotJson',
];
/**
* 解析快照 JSON
*
* 后端把 null 序列化为空串,故空串 / 'null' 都按「没有快照」处理,不算解析失败。
*/
const readSnapshot = json => {
if (json && typeof json === 'object') {
return { value: Object.keys(json).length ? json : null, invalid: false };
}
const text = String(json === null || json === undefined ? '' : json).trim();
if (!text || text === 'null') {
return { value: null, invalid: false };
}
try {
const parsed = JSON.parse(text);
if (parsed && typeof parsed === 'object') {
return { value: parsed, invalid: false };
}
return { value: null, invalid: true };
} catch (error) {
return { value: null, invalid: true };
}
};
/** 解析快照 JSON,无快照或解析失败时返回 null */
export const parseSnapshot = json => readSnapshot(json).value;
const isEmptyValue = value => value === null || value === undefined || value === '';
const isLongTermOn = value => value === 1 || value === '1' || value === true;
/** 日期归一化到 yyyy-MM-dd,便于 `2026-01-01` 与 `2026-01-01T00:00:00` 判等 */
const normalizeDate = value => {
if (isEmptyValue(value)) return '';
const text = String(value).trim();
const matched = text.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/);
if (matched) return `${matched[1]}-${pad(matched[2])}-${pad(matched[3])}`;
const date = isEpochText(text) ? toDate(text) : null;
if (date) {
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
return text;
};
/**
* 取字段值,兼容字段改名前的旧键
*
* 后端把某字段改名后,改名之前提交的记录里存的仍是旧键(例:从业资格证封面页
* 由 `qualificationUrl` 改为 `qualificationFront`)。只读新键会得到空,
* 审核页就把已填的封面页显示成「—」,误导审核员以为对方没传。
* 新旧键都存在时以新键为准。
*/
const readFieldValue = (snapshot, field) => {
if (!snapshot) return undefined;
const value = snapshot[field.key];
if (!isEmptyValue(value)) return value;
const legacyKeys = field.legacyKeys || [];
for (let i = 0; i < legacyKeys.length; i += 1) {
if (!isEmptyValue(snapshot[legacyKeys[i]])) return snapshot[legacyKeys[i]];
}
return value;
};
/** 比较用的归一化取值:展示相同即视为未变更 */
const canonicalOf = (snapshot, field) => {
const value = readFieldValue(snapshot, field);
if (field.type === TYPE_LONG_TERM) {
if (isEmptyValue(value)) return '';
return isLongTermOn(value) ? '1' : '0';
}
if (field.type === TYPE_DATE) {
if (field.longTermKey && isLongTermOn(snapshot && snapshot[field.longTermKey])) {
return '长期有效';
}
return normalizeDate(value);
}
return isEmptyValue(value) ? '' : String(value).trim();
};
/** 展示文案:日期型受「长期有效」影响,图片型返回 URL 由组件渲染缩略图 */
const textOf = (snapshot, field) => {
const value = readFieldValue(snapshot, field);
if (field.type === TYPE_IMAGE) {
return isEmptyValue(value) ? '' : String(value);
}
if (field.type === TYPE_LONG_TERM) {
if (isEmptyValue(value)) return EMPTY_TEXT;
return isLongTermOn(value) ? '是' : '否';
}
if (field.type === TYPE_DATE) {
if (field.longTermKey && isLongTermOn(snapshot && snapshot[field.longTermKey])) {
return '长期有效';
}
return normalizeDate(value) || EMPTY_TEXT;
}
return isEmptyValue(value) ? EMPTY_TEXT : String(value);
};
const buildRow = (field, pendingSnapshot, effectiveSnapshot, hasBaseline) => {
const type = field.type || 'text';
const target = { ...field, type };
const pendingCanonical = canonicalOf(pendingSnapshot, target);
const effectiveCanonical = canonicalOf(effectiveSnapshot, target);
let changeType = '';
if (hasBaseline && pendingCanonical !== effectiveCanonical) {
if (!effectiveCanonical) {
changeType = 'added';
} else if (!pendingCanonical) {
changeType = 'cleared';
} else {
changeType = 'modified';
}
}
return {
key: field.key,
label: field.label,
type,
effective: {
raw: readFieldValue(effectiveSnapshot, target),
text: textOf(effectiveSnapshot, target),
},
pending: {
raw: readFieldValue(pendingSnapshot, target),
text: textOf(pendingSnapshot, target),
},
changed: Boolean(changeType),
changeType,
};
};
const unionKeys = (pendingSnapshot, effectiveSnapshot) => {
const keys = [];
[pendingSnapshot, effectiveSnapshot].forEach(snapshot => {
Object.keys(snapshot || {}).forEach(key => {
if (!keys.includes(key)) keys.push(key);
});
});
return keys;
};
/**
* 构造「待审快照 vs 当前生效值」的字段级 diff
*
* @param {string} bizType 认证对象类型:driver / vehicle
* @param {string} pendingJson 本次待审快照 JSON
* @param {string} effectiveJson 该对象当前生效快照 JSON,从未认证通过时为空
* @returns {Object} { hasBaseline, groups, totalCount, changedCount, ... }
* hasBaseline 为 false 表示无对比基准(从未认证通过),此时不产出任何变更标记
*/
export const buildSnapshotDiff = (bizType, pendingJson, effectiveJson) => {
const pending = readSnapshot(pendingJson);
const effective = readSnapshot(effectiveJson);
const hasBaseline = effective.value !== null;
const groupsConfig = CERT_SNAPSHOT_GROUPS[bizType] || [];
const knownKeys = [];
const groups = groupsConfig.map(group => {
const rows = group.fields.map(field => {
// 旧键已由所属字段并入展示,不能再作为独立字段出现,否则同一张封面页会出现两行
knownKeys.push(field.key, ...(field.legacyKeys || []));
return buildRow(field, pending.value, effective.value, hasBaseline);
});
return {
title: group.title,
rows,
changedCount: rows.filter(row => row.changed).length,
};
});
// 元数据未登记的字段照样展示,避免后端新增字段时审核员看不到变化
const extraKeys = unionKeys(pending.value, effective.value).filter(
key => !knownKeys.includes(key) && !IGNORED_KEYS.includes(key)
);
if (extraKeys.length) {
const rows = extraKeys.map(key =>
buildRow({ key, label: key }, pending.value, effective.value, hasBaseline)
);
groups.push({
title: '其他字段',
rows,
changedCount: rows.filter(row => row.changed).length,
});
}
const rows = groups.reduce((all, group) => all.concat(group.rows), []);
return {
hasBaseline,
pendingInvalid: pending.invalid,
effectiveInvalid: effective.invalid,
groups,
totalCount: rows.length,
changedCount: rows.filter(row => row.changed).length,
};
};
+14
View File
@@ -0,0 +1,14 @@
/**
* 合同下拉/选择展示:合同编号/合同名称
*
* @param {Object} contract 合同对象
* @returns {string}
*/
export function formatContractOptionLabel(contract = {}) {
const contractNo = String(contract?.contractNo ?? '').trim();
const contractName = String(contract?.contractName ?? '').trim();
if (contractNo && contractName) {
return `${contractNo}/${contractName}`;
}
return contractNo || contractName || '';
}
+86
View File
@@ -13,6 +13,92 @@ const coordinateRanges = {
},
};
/** WGS-84 → GCJ-02(高德)偏移算法常量 */
const WGS84_TO_GCJ02_PI = Math.PI;
const WGS84_TO_GCJ02_A = 6378245.0;
const WGS84_TO_GCJ02_EE = 0.00669342162296594323;
const isOutOfChina = (longitude, latitude) =>
longitude < 72.004 || longitude > 137.8347 || latitude < 0.8293 || latitude > 55.8271;
const transformLatitude = (longitude, latitude) => {
let result =
-100.0 +
2.0 * longitude +
3.0 * latitude +
0.2 * latitude * latitude +
0.1 * longitude * latitude +
0.2 * Math.sqrt(Math.abs(longitude));
result +=
((20.0 * Math.sin(6.0 * longitude * WGS84_TO_GCJ02_PI) +
20.0 * Math.sin(2.0 * longitude * WGS84_TO_GCJ02_PI)) *
2.0) /
3.0;
result +=
((20.0 * Math.sin(latitude * WGS84_TO_GCJ02_PI) +
40.0 * Math.sin((latitude / 3.0) * WGS84_TO_GCJ02_PI)) *
2.0) /
3.0;
result +=
((160.0 * Math.sin((latitude / 12.0) * WGS84_TO_GCJ02_PI) +
320 * Math.sin((latitude * WGS84_TO_GCJ02_PI) / 30.0)) *
2.0) /
3.0;
return result;
};
const transformLongitude = (longitude, latitude) => {
let result =
300.0 +
longitude +
2.0 * latitude +
0.1 * longitude * longitude +
0.1 * longitude * latitude +
0.1 * Math.sqrt(Math.abs(longitude));
result +=
((20.0 * Math.sin(6.0 * longitude * WGS84_TO_GCJ02_PI) +
20.0 * Math.sin(2.0 * longitude * WGS84_TO_GCJ02_PI)) *
2.0) /
3.0;
result +=
((20.0 * Math.sin(longitude * WGS84_TO_GCJ02_PI) +
40.0 * Math.sin((longitude / 3.0) * WGS84_TO_GCJ02_PI)) *
2.0) /
3.0;
result +=
((150.0 * Math.sin((longitude / 12.0) * WGS84_TO_GCJ02_PI) +
300.0 * Math.sin((longitude / 30.0) * WGS84_TO_GCJ02_PI)) *
2.0) /
3.0;
return result;
};
/**
* WGS-84(GPS / LBS)转 GCJ-02(高德/国测局),供高德地图绘制使用。
* 中国境外坐标不做偏移,原样返回。
* @param {number} longitude
* @param {number} latitude
* @returns {[number, number]} [经度, 纬度]
*/
export const wgs84ToGcj02 = (longitude, latitude) => {
const lng = Number(longitude);
const lat = Number(latitude);
if (!Number.isFinite(lng) || !Number.isFinite(lat) || isOutOfChina(lng, lat)) {
return [lng, lat];
}
let dLat = transformLatitude(lng - 105.0, lat - 35.0);
let dLng = transformLongitude(lng - 105.0, lat - 35.0);
const radLat = (lat / 180.0) * WGS84_TO_GCJ02_PI;
let magic = Math.sin(radLat);
magic = 1 - WGS84_TO_GCJ02_EE * magic * magic;
const sqrtMagic = Math.sqrt(magic);
dLat =
(dLat * 180.0) /
(((WGS84_TO_GCJ02_A * (1 - WGS84_TO_GCJ02_EE)) / (magic * sqrtMagic)) * WGS84_TO_GCJ02_PI);
dLng = (dLng * 180.0) / ((WGS84_TO_GCJ02_A / sqrtMagic) * Math.cos(radLat) * WGS84_TO_GCJ02_PI);
return [lng + dLng, lat + dLat];
};
export const normalizeCoordinateInput = value => {
const rawValue = String(value ?? '').replace(/[^\d.-]/g, '');
const isNegative = rawValue.startsWith('-');
+109
View File
@@ -0,0 +1,109 @@
const CURRENCY_NAME_MAP = {
CNY: '人民币',
RMB: '人民币',
USD: '美元',
EUR: '欧元',
JPY: '日元',
GBP: '英镑',
};
const CURRENCY_UNIT_MAP = {
CNY: '元',
RMB: '元',
USD: '美元',
EUR: '欧元',
JPY: '日元',
GBP: '英镑',
};
const CURRENCY_NAME_TO_UNIT = {
人民币: '元',
美元: '美元',
欧元: '欧元',
日元: '日元',
英镑: '英镑',
};
/**
* 归一化币种代码(CNY → RMB)
*/
export function normalizeCurrencyCode(value, fallback = 'RMB') {
const raw =
typeof value === 'object'
? value?.dictKey || value?.value || value?.code || value?.dictValue || ''
: value;
const normalized = String(raw || fallback)
.trim()
.toUpperCase();
const code = normalized.includes(' - ') ? normalized.split(' - ')[0].trim() : normalized;
if (!code) return fallback;
return code === 'CNY' ? 'RMB' : code;
}
/**
* 币种中文名称
*/
export function currencyName(value) {
const text = String(value || '');
if (text.includes(' - ')) return text.slice(text.indexOf(' - ') + 3);
return CURRENCY_NAME_MAP[text.toUpperCase()] || text;
}
/**
* 金额单位文案:RMB/CNY → 元,USD → 美元
*/
export function currencyUnit(value) {
const text = String(value || '');
let code = text;
if (text.includes(' - ')) {
code = text.split(' - ')[0].trim();
}
const upperCode = code.toUpperCase();
if (CURRENCY_UNIT_MAP[upperCode]) {
return CURRENCY_UNIT_MAP[upperCode];
}
if (CURRENCY_NAME_TO_UNIT[code]) {
return CURRENCY_NAME_TO_UNIT[code];
}
return text || '元';
}
/**
* 将计价单位前缀替换为当前币种单位,如 元/吨 → 美元/吨
*/
export function remapPriceUnitCurrency(priceUnit, currency) {
const text = String(priceUnit || '').trim();
if (!text || !text.includes('/')) return text;
const suffix = text.slice(text.indexOf('/'));
return `${currencyUnit(currency)}${suffix}`;
}
/**
* 按数量单位生成默认计价单位
*/
export function resolvePriceUnitByQuantityUnit(quantityUnit, currency = 'RMB') {
const unit = currencyUnit(currency);
return (
{
吨: `${unit}/吨`,
方: `${unit}/立方`,
件: `${unit}/件`,
}[String(quantityUnit || '').trim()] || ''
);
}
/**
* 将字典中的计价单位选项映射到当前币种
*/
export function mapFeeUnitOptions(options = [], currency = 'RMB') {
const unit = currencyUnit(currency);
return (options || []).map(item => {
const label = String(item.label || item.value || '');
const value = String(item.value || item.label || '');
return {
...item,
label: label.replace(/^元/, unit).replace(/^人民币/, unit),
value: value.replace(/^元/, unit).replace(/^人民币/, unit),
};
});
}
+99 -22
View File
@@ -6,7 +6,31 @@ import 'nprogress/nprogress.css';
const excelPattern = /\.(xls|xlsx)$/;
/** 是否为浏览器 Blob(含跨 realm 的 Blob-like) */
const isBlobLike = value =>
value != null &&
typeof value === 'object' &&
typeof value.size === 'number' &&
typeof value.type === 'string' &&
(typeof value.text === 'function' || typeof value.arrayBuffer === 'function');
/** axios 拦截器已把 JSON Blob 解析成的业务对象 */
const isBusinessJson = value =>
value != null &&
typeof value === 'object' &&
!isBlobLike(value) &&
!(value instanceof ArrayBuffer) &&
('code' in value || 'success' in value || 'msg' in value);
const isExcelResponse = res => {
// 已是业务 JSON(导入成功/失败提示)时,绝不能当失败明细 Excel 处理
if (isBusinessJson(res?.data)) {
return false;
}
// 只有文件流才可能是失败明细;普通对象没有 .text/.arrayBuffer
if (!isBlobLike(res?.data)) {
return false;
}
const headers = res.headers || {};
const contentType = String(headers['content-type'] || res.data?.type || '').toLowerCase();
const contentDisposition = String(headers['content-disposition'] || '').toLowerCase();
@@ -23,8 +47,34 @@ const isExcelResponse = res => {
);
};
const parseBlobJson = async blob => {
const text = await blob.text();
/**
* 解析导入接口响应体为 JSON 对象。
* <p>
* axios 拦截器在 Content-Type 为 JSON 时,会把 `responseType: 'blob'` 的 Blob
* 提前解析成普通对象;此处需同时兼容 Blob / 字符串 / 已解析对象,避免再调
* `blob.text()` 时出现「blob.text is not a function」。
*/
const parseBlobJson = async data => {
if (data == null) {
return {};
}
if (isBusinessJson(data)) {
return data;
}
if (typeof data === 'object' && !isBlobLike(data) && !(data instanceof ArrayBuffer)) {
return data;
}
if (typeof data === 'string') {
return data ? JSON.parse(data) : {};
}
let text = '';
if (typeof data.text === 'function') {
text = await data.text();
} else if (typeof data.arrayBuffer === 'function') {
text = new TextDecoder().decode(await data.arrayBuffer());
} else if (data instanceof ArrayBuffer) {
text = new TextDecoder().decode(data);
}
return text ? JSON.parse(text) : {};
};
@@ -39,18 +89,21 @@ const parseBlobJson = async blob => {
* 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。
*/
const resolveErrorMessage = async error => {
const blob = error?.response?.data ?? error?.data;
if (blob instanceof Blob) {
const payload = error?.response?.data ?? error?.data;
if (isBusinessJson(payload) && payload.msg) {
return payload.msg;
}
if (isBlobLike(payload) || typeof payload === 'string') {
try {
const result = await parseBlobJson(blob);
const result = await parseBlobJson(payload);
if (result?.msg) return result.msg;
} catch (ignored) {
// 响应体不是 JSON(如网关返回的 HTML 错误页),退回通用文案
}
}
const fallback = error?.response?.data?.msg || error?.message;
const fallback = payload?.msg || error?.message;
// axios 的英文通用文案对用户没有意义,统一换成中文提示
if (!fallback || /Request failed with status code/i.test(fallback)) {
if (!fallback || /Request failed with status code|\.text is not a function/i.test(fallback)) {
return '导入失败,请检查导入文件后重试';
}
return fallback;
@@ -68,8 +121,11 @@ const resolveErrorMessage = async error => {
*/
const decorateFailExcel = async (blob, businessName, failDetailDecorator) => {
if (typeof failDetailDecorator !== 'function') return blob;
if (!isBlobLike(blob) && !(blob instanceof ArrayBuffer)) {
return blob;
}
const ExcelJS = await import('exceljs');
const buf = await blob.arrayBuffer();
const buf = typeof blob.arrayBuffer === 'function' ? await blob.arrayBuffer() : blob;
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(buf);
await failDetailDecorator(workbook, { businessName });
@@ -77,6 +133,31 @@ const decorateFailExcel = async (blob, businessName, failDetailDecorator) => {
return new Blob([out], { type: 'application/vnd.ms-excel' });
};
/** 通知上传组件成功:只传业务响应体,避免其误调 axios 响应上的 .text() */
const notifyUploadSuccess = (option, res) => {
if (typeof option?.onSuccess !== 'function') {
return;
}
try {
const body = isBusinessJson(res?.data) ? res.data : { code: 200, success: true };
option.onSuccess(body);
} catch (callbackError) {
console.warn('upload onSuccess callback failed', callbackError);
}
};
/** 通知上传组件失败 */
const notifyUploadError = (option, error) => {
if (typeof option?.onError !== 'function') {
return;
}
try {
option.onError(error instanceof Error ? error : new Error(String(error?.message || error || '导入失败')));
} catch (callbackError) {
console.warn('upload onError callback failed', callbackError);
}
};
/**
* 打开导入弹窗
* @param {Object} vm 组件实例
@@ -108,9 +189,7 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
const fileName = file && file.name ? file.name.toLowerCase() : '';
if (!excelPattern.test(fileName)) {
vm.$message.error('请上传 .xls,.xlsx 标准格式文件');
if (typeof option.onError === 'function') {
option.onError(new Error('请上传 .xls,.xlsx 标准格式文件'));
}
notifyUploadError(option, new Error('请上传 .xls,.xlsx 标准格式文件'));
return;
}
@@ -130,12 +209,11 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
);
vm.$message.warning('部分数据导入失败,已下载失败明细');
} else {
const result = await parseBlobJson(res.data);
if (result.code !== 200) {
// 成功体常被 axios 拦截器提前解析成对象;优先走业务 JSON,避免再调 Blob.text
const result = isBusinessJson(res?.data) ? res.data : await parseBlobJson(res?.data);
if (Number(result.code) !== 200) {
vm.$message.error(result.msg || '导入失败');
if (typeof option.onError === 'function') {
option.onError(new Error(result.msg || '导入失败'));
}
notifyUploadError(option, new Error(result.msg || '导入失败'));
return;
}
vm.$message.success('导入完成');
@@ -149,18 +227,17 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
} else if (typeof vm.initTree === 'function') {
vm.initTree();
}
if (typeof option.onSuccess === 'function') {
option.onSuccess(res);
}
// 上传组件 onSuccess 约定接收「响应体」,不是完整 axios 响应。
// 若传入 axios 响应对象,部分环境下会按 Fetch Response 调用 .text(),
// 从而在导入已成功后仍弹出「e.text is not a function」。
notifyUploadSuccess(option, res);
} catch (error) {
// 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次,
// 只保留一次清晰的中文提示。
if (!error?._notified) {
vm.$message.error(await resolveErrorMessage(error));
}
if (typeof option.onError === 'function') {
option.onError(error);
}
notifyUploadError(option, error);
} finally {
loading.close();
NProgress.done();
-91
View File
@@ -1,91 +0,0 @@
/**
* 金蝶侧发票附件查看/下载公共工具
*
* 票池发票附件为 {source:'piaozone', path, name}:path 是金蝶侧文件 id(非 http 直链),
* 须由后端代理 A801-2 流式回源;OSS 直链附件(url/link/fileUrl)仍由前端直连下载/预览。
* 入口共用:票池弹窗、正式结算认领发票行、付款申请发票附件行、收票登记附件表。
*/
import { ElMessage } from 'element-plus';
import { downloadFile } from '@/utils/util';
import { downloadInvoicePoolAttachment } from '@/api/payment/invoiceReceipt';
/**
* 金蝶侧附件判定:source=piaozone,或有 path 且非 http(s) 直链
* @param {Object|Null} attachment 附件对象(attachmentsJson/attachmentJson 行内首项)
* @returns {Boolean}
*/
export const isKingdeePathAttachment = attachment => {
if (!attachment) return false;
if (attachment.source === 'piaozone') return true;
return Boolean(attachment.path) && !/^https?:\/\//i.test(attachment.path);
};
/**
* 请求金蝶文件流:成功返回 Blob;业务失败为 HTTP 200 + R.fail JSON(响应通道未写流时后端即失败),
* 按 Content-Type 识别并抛出带 msg 的错误;网络级失败拦截器已提示,标记 silent 避免重复提示。
* 供查看(弹窗内预览)/下载两类入口复用
* @param {String} invoiceNo 发票号码
* @returns {Promise<Blob>}
*/
export const requestKingdeeFile = async invoiceNo => {
let res;
try {
res = await downloadInvoicePoolAttachment(invoiceNo);
} catch (error) {
throw Object.assign(new Error(error?.message || '附件下载失败'), { silent: true });
}
const blob = res?.data;
if (!(blob instanceof Blob)) {
throw new Error('响应不是文件流');
}
if (blob.type && blob.type.includes('application/json')) {
let message = '附件下载失败';
try {
const parsed = JSON.parse(await blob.text());
message = parsed.msg || parsed.message || message;
} catch {
// 错误体非 JSON 时按默认提示
}
throw new Error(message);
}
return blob;
};
/**
* 下载金蝶侧发票附件(经 A801-2 后端代理),失败统一 ElMessage 提示
* @param {String} invoiceNo 发票号码
* @param {String} fileName 下载文件名(缺省按 发票号.pdf)
*/
export const downloadKingdeeInvoiceFile = async (invoiceNo, fileName) => {
if (!invoiceNo) {
ElMessage.warning('当前发票缺少发票号码,无法下载金蝶侧附件');
return;
}
try {
const blob = await requestKingdeeFile(invoiceNo);
downloadFile(blob, fileName || `${invoiceNo}.pdf`);
} catch (error) {
if (!error?.silent) ElMessage.error(error?.message || '金蝶附件下载失败');
}
};
/**
* 新窗口预览金蝶侧发票附件:拉取 Blob 转 objectURL(PDF 可预览;OFD 等浏览器不可预览的格式由浏览器触发下载)
* @param {String} invoiceNo 发票号码
* @param {String} fileName 预览文件名(缺省按 发票号.pdf)
*/
export const viewKingdeeInvoiceFile = async (invoiceNo, fileName) => {
if (!invoiceNo) {
ElMessage.warning('当前发票缺少发票号码,无法预览金蝶侧附件');
return;
}
try {
const blob = await requestKingdeeFile(invoiceNo);
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
// 延迟释放 objectURL:立即回收可能导致新窗口尚未加载完成即失效
setTimeout(() => URL.revokeObjectURL(url), 60000);
} catch (error) {
if (!error?.silent) ElMessage.error(error?.message || '金蝶附件预览失败');
}
};
+27
View File
@@ -0,0 +1,27 @@
import { getList as getMeasurementUnitList } from '@/api/base/measurement-unit';
/**
* 加载启用状态的计量单位选项,展示/取值均为「计量单位」字段(unitName)。
* @returns {Promise<Array<{ label: string, value: string, dimension: string, id: string|number }>>}
*/
export const loadMeasurementUnitOptions = () => {
return getMeasurementUnitList(1, 9999, { status: 1 }).then(res => {
const data = res?.data?.data || res?.data || {};
const records = Array.isArray(data) ? data : data.records || [];
const names = new Set();
return records
.filter(item => item.status === undefined || item.status === null || Number(item.status) === 1)
.map(item => {
const name = String(item.unitName || '').trim();
if (!name || names.has(name)) return null;
names.add(name);
return {
id: item.id,
label: name,
value: name,
dimension: String(item.dimension || '').trim(),
};
})
.filter(Boolean);
});
};
+312 -35
View File
@@ -144,7 +144,7 @@
<script>
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/airport-master';
import { getLazyTree } from '@/api/base/region';
import { getDetail as getRegionDetail, getLazyTree } from '@/api/base/region';
import { exportBlob } from '@/api/common';
import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
@@ -322,6 +322,20 @@ export default {
prop: 'shortName',
minWidth: 130,
},
{
label: '详细地址',
prop: 'detailAddress',
formslot: true,
span: 24,
minWidth: 220,
overHidden: false,
maxlength: 255,
showWordLimit: true,
rules: [
{ required: true, message: '请选择详细地址', trigger: 'change' },
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
],
},
{
label: '所属省份',
prop: 'provinceCode',
@@ -388,20 +402,6 @@ export default {
addDisplay: false,
editDisplay: false,
},
{
label: '详细地址',
prop: 'detailAddress',
formslot: true,
span: 24,
minWidth: 220,
overHidden: false,
maxlength: 255,
showWordLimit: true,
rules: [
{ required: true, message: '请选择详细地址', trigger: 'change' },
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
],
},
{
label: '经度',
prop: 'longitude',
@@ -578,33 +578,51 @@ export default {
return targetColumns.find(item => item.prop === targetProp) || null;
},
loadProvinceOptions() {
getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
return getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
this.provinceOptions = (res.data.data || []).map(this.normalizeRegionOption);
this.findColumn('provinceCode').dicData = this.provinceOptions;
const provinceColumn = this.findColumn('provinceCode');
if (provinceColumn) {
provinceColumn.dicData = this.provinceOptions;
}
return this.provinceOptions;
});
},
loadCityOptions(provinceCode) {
if (!provinceCode) {
this.cityOptions = [];
this.findColumn('cityCode').dicData = [];
return Promise.resolve();
const cityColumn = this.findColumn('cityCode');
if (cityColumn) {
cityColumn.dicData = [];
}
return Promise.resolve([]);
}
return getLazyTree(provinceCode).then(res => {
this.cityOptions = (res.data.data || []).map(this.normalizeRegionOption);
this.findColumn('cityCode').dicData = this.cityOptions;
const cityColumn = this.findColumn('cityCode');
if (cityColumn) {
cityColumn.dicData = this.cityOptions;
}
return this.cityOptions;
});
},
loadDistrictOptions(cityCode) {
if (!cityCode) {
this.districtOptions = [];
this.findColumn('districtCode').dicData = [];
return Promise.resolve();
const districtColumn = this.findColumn('districtCode');
if (districtColumn) {
districtColumn.dicData = [];
}
return Promise.resolve([]);
}
this.districtLoading = true;
return getLazyTree(cityCode)
.then(res => {
this.districtOptions = (res.data.data || []).map(this.normalizeRegionOption);
this.findColumn('districtCode').dicData = this.districtOptions;
const districtColumn = this.findColumn('districtCode');
if (districtColumn) {
districtColumn.dicData = this.districtOptions;
}
return this.districtOptions;
})
.finally(() => {
this.districtLoading = false;
@@ -631,6 +649,22 @@ export default {
: String(row.districtCode),
};
},
ensureCityOption(row) {
if (!row.cityCode || !row.cityName) return;
const exists = this.cityOptions.some(item => item.id === row.cityCode);
if (exists) return;
this.cityOptions = [
...this.cityOptions,
{
id: row.cityCode,
title: row.cityName,
},
];
const cityColumn = this.findColumn('cityCode');
if (cityColumn) {
cityColumn.dicData = this.cityOptions;
}
},
ensureDistrictOption(row) {
if (!row.districtCode || !row.districtName) return;
const exists = this.districtOptions.some(item => item.id === row.districtCode);
@@ -642,7 +676,10 @@ export default {
title: row.districtName,
},
];
this.findColumn('districtCode').dicData = this.districtOptions;
const districtColumn = this.findColumn('districtCode');
if (districtColumn) {
districtColumn.dicData = this.districtOptions;
}
},
handleIataChange(value) {
this.form.iataCode = String(value || '')
@@ -698,25 +735,265 @@ export default {
openAddressMapPicker() {
this.addressMapPickerVisible = true;
},
formatMapCoordinate(value) {
if (value === undefined || value === null || value === '') {
return '';
}
const number = Number(value);
if (!Number.isFinite(number)) {
return '';
}
return normalizeCoordinateInput(number.toFixed(6));
},
patchDialogForm(patch = {}) {
Object.assign(this.form, patch);
const tableForm = this.$refs.crud?.tableForm;
if (tableForm && tableForm !== this.form) {
Object.assign(tableForm, patch);
}
},
handleAddressMapConfirm(payload) {
const selection = typeof payload === 'string' ? { address: payload } : payload || {};
const longitude = this.formatMapCoordinate(selection.longitude);
const latitude = this.formatMapCoordinate(selection.latitude);
const patch = {};
if (selection.address) {
this.form.detailAddress = selection.address;
patch.detailAddress = selection.address;
}
if (longitude !== '' && latitude !== '') {
patch.longitude = longitude;
patch.latitude = latitude;
}
if (Object.keys(patch).length) {
this.patchDialogForm(patch);
}
this.applyMapRegionSelection(selection);
},
normalizeAmapCityName(city, province) {
if (Array.isArray(city)) {
return String(province || '').trim();
}
const cityName = String(city || '').trim();
return cityName || String(province || '').trim();
},
buildRegionLookupCodes(adcode) {
const code = String(adcode || '').trim();
if (!code) {
return [];
}
const codes = [code];
if (/^\d{6}$/.test(code)) {
if (code.endsWith('00')) {
const cityCode = code.slice(0, 4);
if (cityCode && !codes.includes(cityCode)) {
codes.push(cityCode);
}
}
if (code.endsWith('0000')) {
const provinceCode = code.slice(0, 2);
if (provinceCode && !codes.includes(provinceCode)) {
codes.push(provinceCode);
}
}
}
return codes;
},
fetchRegionDetailByCodes(codes = []) {
const tryNext = index => {
if (index >= codes.length) {
return Promise.resolve(null);
}
return getRegionDetail(codes[index])
.then(res => {
const detail = res.data && res.data.data;
if (detail && detail.code) {
return detail;
}
return tryNext(index + 1);
})
.catch(() => tryNext(index + 1));
};
return tryNext(0);
},
resolveProvinceCode(provinceName) {
const name = String(provinceName || '').trim();
if (!name) {
return '';
}
const province = this.provinceOptions.find(
item => item.title === name || item.id === name || String(item.title).includes(name)
);
return province ? province.id : '';
},
resolveCityCode(cityName, regionCode) {
const name = String(cityName || '').trim();
const code = String(regionCode || '').trim();
const city = this.cityOptions.find(
item => item.title === name || item.id === code || item.id === name
);
return city ? city.id : '';
},
resolveDistrictCode(districtName, regionCode) {
const name = String(districtName || '').trim();
const code = String(regionCode || '').trim();
const district = this.districtOptions.find(
item => item.title === name || item.id === code || item.id === name
);
return district ? district.id : code || '';
},
applyRegionDetail(detail = {}) {
const provinceName = detail.provinceName || '';
const cityName = detail.cityName || '';
const districtName = detail.districtName || detail.name || '';
return this.loadProvinceOptions()
.then(() => {
const provinceCode =
String(detail.provinceCode || '').trim() || this.resolveProvinceCode(provinceName);
const province = this.provinceOptions.find(item => item.id === provinceCode);
this.patchDialogForm({
provinceCode: provinceCode || undefined,
provinceName: province ? province.title : provinceName,
});
if (!provinceCode) {
return null;
}
return this.loadCityOptions(provinceCode);
})
.then(cities => {
if (!cities) {
return null;
}
const cityCode =
String(detail.cityCode || '').trim() ||
this.resolveCityCode(cityName, detail.districtCode || detail.code);
const city = this.cityOptions.find(item => item.id === cityCode);
this.patchDialogForm({
cityCode: cityCode || undefined,
cityName: city ? city.title : cityName,
});
this.ensureCityOption({
cityCode,
cityName: city ? city.title : cityName,
});
if (!cityCode) {
return null;
}
return this.loadDistrictOptions(cityCode);
})
.then(districts => {
if (!districts) {
return;
}
const districtCode = this.resolveDistrictCode(
districtName,
detail.districtCode || detail.code
);
this.patchDialogForm({
districtCode: districtCode || undefined,
});
this.handleDistrictChange(districtCode);
this.ensureDistrictOption({
districtCode,
districtName: this.form.districtName || districtName,
});
});
},
applyMapRegionByNames(selection = {}) {
const provinceName = String(selection.province || selection.provinceName || '').trim();
const cityName = this.normalizeAmapCityName(
selection.city || selection.cityName,
provinceName
);
const districtName = String(selection.district || selection.districtName || '').trim();
const adcode = String(selection.adcode || selection.regionCode || '').trim();
// 本页无国家字段:国内 adcode 走中国省份树;无匹配则不强制回填
if (
selection.longitude !== undefined &&
selection.longitude !== null &&
selection.longitude !== ''
!provinceName &&
!cityName &&
!districtName &&
!(/^\d{6}$/.test(adcode) || String(selection.country || selection.countryName || '').includes('中国'))
) {
this.form.longitude = Number(selection.longitude).toFixed(6);
return Promise.resolve();
}
if (
selection.latitude !== undefined &&
selection.latitude !== null &&
selection.latitude !== ''
) {
this.form.latitude = Number(selection.latitude).toFixed(6);
return this.loadProvinceOptions()
.then(() => {
const provinceCode = this.resolveProvinceCode(provinceName);
const province = this.provinceOptions.find(item => item.id === provinceCode);
this.patchDialogForm({
provinceCode: provinceCode || undefined,
provinceName: province ? province.title : provinceName,
});
if (!provinceCode) {
return null;
}
return this.loadCityOptions(provinceCode);
})
.then(cities => {
if (!cities) {
return null;
}
const cityCode = this.resolveCityCode(cityName, adcode);
const city = this.cityOptions.find(item => item.id === cityCode);
this.patchDialogForm({
cityCode: cityCode || undefined,
cityName: city ? city.title : cityName,
});
this.ensureCityOption({
cityCode,
cityName: city ? city.title : cityName,
});
if (!cityCode) {
return null;
}
return this.loadDistrictOptions(cityCode);
})
.then(districts => {
if (!districts) {
return;
}
const districtCode = this.resolveDistrictCode(districtName, adcode);
this.patchDialogForm({
districtCode: districtCode || undefined,
});
this.handleDistrictChange(districtCode);
this.ensureDistrictOption({
districtCode,
districtName: this.form.districtName || districtName,
});
});
},
applyMapRegionSelection(selection = {}) {
const adcode = String(selection.adcode || selection.regionCode || '').trim();
const hasNameHint = Boolean(
selection.country ||
selection.countryName ||
selection.province ||
selection.provinceName ||
selection.city ||
selection.cityName ||
selection.district ||
selection.districtName
);
if (!adcode && !hasNameHint) {
return Promise.resolve();
}
this.regionInitializing = true;
const lookupCodes = this.buildRegionLookupCodes(adcode);
return this.fetchRegionDetailByCodes(lookupCodes)
.then(detail => {
if (detail) {
return this.applyRegionDetail(detail);
}
return this.applyMapRegionByNames(selection);
})
.catch(() => this.applyMapRegionByNames(selection))
.finally(() => {
this.$nextTick(() => {
window.setTimeout(() => {
this.regionInitializing = false;
}, 0);
});
});
},
normalizeRow(row) {
row.code = row.iataCode ? `JC-${row.iataCode}` : row.code;
+30 -7
View File
@@ -344,15 +344,21 @@ export default {
getDeptTree(this.userInfo.tenantId).then(res => {
this.deptOptions = this.flattenDept(res.data.data || []);
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
deptColumn.dicData = this.deptOptions;
if (deptColumn) {
deptColumn.dicData = this.deptOptions;
}
});
},
flattenDept(tree, level = 0) {
const result = [];
tree.forEach(item => {
// 雪花 ID 必须保持字符串,避免 JS Number 精度丢失导致筛错组织。
const id =
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
if (!id) return;
result.push({
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
value: item.id,
value: id,
rawLabel: item.title || item.deptName || item.name,
});
if (item.children && item.children.length) {
@@ -361,6 +367,19 @@ export default {
});
return result;
},
normalizeListQuery(params = {}) {
// 搜索条件优先于缓存的 query,避免旧组织 ID 覆盖用户新选择的组织。
const query = { ...this.query, ...params };
const rawDeptId = query.deptId ?? query.searchDeptId;
delete query.searchDeptId;
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
query.deptId = String(rawDeptId).split(',')[0].trim();
} else {
delete query.deptId;
}
query.allDept = this.isAdmin ? 1 : 0;
return query;
},
getRecords(res) {
const data = res && res.data && res.data.data;
if (Array.isArray(data)) return data;
@@ -967,13 +986,15 @@ export default {
},
searchReset() {
this.query = {};
this.allDept = this.isAdmin ? 1 : 0;
this.page.currentPage = 1;
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.query = this.normalizeListQuery(params);
this.allDept = this.query.allDept;
this.page.currentPage = 1;
this.onLoad(this.page, params);
this.onLoad(this.page, this.query);
done();
},
selectionChange(list) {
@@ -994,7 +1015,10 @@ export default {
},
onLoad(page, params = {}) {
this.loading = true;
getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: this.allDept })
const query = this.normalizeListQuery(params);
this.query = query;
this.allDept = query.allDept;
getList(page.currentPage, page.pageSize, query)
.then(res => {
const data = res.data.data;
this.page.total = data.total;
@@ -1027,8 +1051,7 @@ export default {
},
buildExportParams() {
return {
...this.query,
allDept: this.allDept,
...this.normalizeListQuery(this.query),
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
+326 -83
View File
@@ -264,7 +264,7 @@ import {
changeStatus,
getPortSelect,
} from '@/api/base/port-terminal';
import { getLazyTree } from '@/api/base/region';
import { getDetail as getRegionDetail, getLazyTree } from '@/api/base/region';
import { exportBlob } from '@/api/common';
import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
@@ -584,7 +584,7 @@ export default {
span: 12,
minWidth: 220,
overHidden: false,
order: 85,
order: 90,
placeholder: '点击选择地图地址',
maxlength: 255,
showWordLimit: true,
@@ -841,10 +841,60 @@ export default {
},
handleCountryNameChange(value) {
const country = this.countryOptions.find(item => item.title === value);
this.setProvinceOptions([]);
this.setCityOptions([]);
this.setDistrictOptions([]);
this.loadProvinceOptions(country ? country.id : '');
this.clearSearchCityAndDistrict();
// 搜索栏为国家 → 城市 → 区县(无省份),选中国家后需直接加载该国下全部城市
this.loadCityOptionsByCountry(country ? country.id : '');
},
clearSearchCityAndDistrict() {
const searchForm = this.$refs.crud && this.$refs.crud.searchForm;
if (searchForm) {
searchForm.city = undefined;
searchForm.districtName = undefined;
}
if (this.query) {
this.query.city = undefined;
this.query.districtName = undefined;
}
},
loadCityOptionsByCountry(countryCode) {
if (!countryCode) {
this.setCityOptions([]);
return Promise.resolve([]);
}
this.cityLoading = true;
return this.loadProvinceOptions(countryCode)
.then(provinces =>
Promise.all((provinces || []).map(province => this.fetchCityOptions(province.id)))
)
.then(cityGroups => {
const cityMap = new Map();
cityGroups.flat().forEach(city => {
if (city && city.id && !cityMap.has(city.id)) {
cityMap.set(city.id, city);
}
});
const cities = Array.from(cityMap.values());
this.setCityOptions(cities);
return cities;
})
.finally(() => {
this.cityLoading = false;
});
},
fetchCityOptions(provinceCode) {
if (!provinceCode) {
return Promise.resolve([]);
}
if (this.cityOptionCache[provinceCode]) {
return Promise.resolve(this.cityOptionCache[provinceCode]);
}
return getLazyTree(provinceCode).then(res => {
const options = (res.data.data || []).map(this.normalizeRegionOption);
this.cityOptionCache[provinceCode] = options;
return options;
});
},
syncCountryName(countryCode) {
const country = this.countryOptions.find(item => item.id === countryCode);
@@ -925,10 +975,8 @@ export default {
return Promise.resolve(this.cityOptionCache[provinceCode]);
}
this.cityLoading = true;
return getLazyTree(provinceCode)
.then(res => {
const options = (res.data.data || []).map(this.normalizeRegionOption);
this.cityOptionCache[provinceCode] = options;
return this.fetchCityOptions(provinceCode)
.then(options => {
this.setCityOptions(options);
return options;
})
@@ -1047,6 +1095,13 @@ export default {
handleCityNameChange(value) {
const city = this.cityOptions.find(item => item.title === value);
this.setDistrictOptions([]);
const searchForm = this.$refs.crud && this.$refs.crud.searchForm;
if (searchForm) {
searchForm.districtName = undefined;
}
if (this.query) {
this.query.districtName = undefined;
}
this.loadDistrictOptions(city ? city.id : '');
},
handleDistrictChange(value) {
@@ -1060,17 +1115,224 @@ export default {
openAddressMapPicker() {
this.addressMapPickerVisible = true;
},
formatMapCoordinate(value) {
if (value === undefined || value === null || value === '') {
return '';
}
const number = Number(value);
if (!Number.isFinite(number)) {
return '';
}
return normalizeCoordinateInput(number.toFixed(6));
},
patchDialogForm(patch = {}) {
// 同步页面 form 与 Avue 弹窗内部 tableForm,避免只改了一边导致经纬度不刷新
Object.assign(this.form, patch);
const tableForm = this.$refs.crud?.tableForm;
if (tableForm && tableForm !== this.form) {
Object.assign(tableForm, patch);
}
},
handleAddressMapConfirm(payload) {
const selection = typeof payload === 'string' ? { address: payload } : payload || {};
const longitude = this.formatMapCoordinate(selection.longitude);
const latitude = this.formatMapCoordinate(selection.latitude);
const patch = {};
if (selection.address) {
this.form.detailAddress = selection.address;
patch.detailAddress = selection.address;
}
if (selection.longitude !== undefined && selection.longitude !== null && selection.longitude !== '') {
this.form.longitude = Number(selection.longitude).toFixed(6);
// 换地址时必须同步覆盖经纬度,不能保留上一次选点
if (longitude !== '' && latitude !== '') {
patch.longitude = longitude;
patch.latitude = latitude;
}
if (selection.latitude !== undefined && selection.latitude !== null && selection.latitude !== '') {
this.form.latitude = Number(selection.latitude).toFixed(6);
if (Object.keys(patch).length) {
this.patchDialogForm(patch);
}
this.applyMapRegionSelection(selection);
},
normalizeAmapCityName(city, province) {
if (Array.isArray(city)) {
return String(province || '').trim();
}
const cityName = String(city || '').trim();
return cityName || String(province || '').trim();
},
buildRegionLookupCodes(adcode) {
const code = String(adcode || '').trim();
if (!code) {
return [];
}
const codes = [code];
if (/^\d{6}$/.test(code)) {
if (code.endsWith('00')) {
const cityCode = code.slice(0, 4);
if (cityCode && !codes.includes(cityCode)) {
codes.push(cityCode);
}
}
if (code.endsWith('0000')) {
const provinceCode = code.slice(0, 2);
if (provinceCode && !codes.includes(provinceCode)) {
codes.push(provinceCode);
}
}
}
return codes;
},
fetchRegionDetailByCodes(codes = []) {
const tryNext = index => {
if (index >= codes.length) {
return Promise.resolve(null);
}
return getRegionDetail(codes[index])
.then(res => {
const detail = res.data && res.data.data;
if (detail && detail.code) {
return detail;
}
return tryNext(index + 1);
})
.catch(() => tryNext(index + 1));
};
return tryNext(0);
},
resolveCountryFromRegionDetail(detail = {}) {
const ancestors = String(detail.ancestors || '')
.split(',')
.map(item => String(item || '').trim())
.filter(Boolean);
for (let index = 0; index < ancestors.length; index += 1) {
const ancestorCode = ancestors[index];
if (this.countryOptions.some(item => item.id === ancestorCode)) {
return ancestorCode;
}
}
if (
detail.parentCode &&
Number(detail.regionLevel) === 1 &&
this.countryOptions.some(item => item.id === String(detail.parentCode))
) {
return String(detail.parentCode);
}
if (/^\d{6}$/.test(String(detail.code || ''))) {
return DEFAULT_COUNTRY_CODE;
}
return '';
},
applyRegionDetail(detail = {}) {
const countryHint = this.resolveCountryFromRegionDetail(detail);
return this.resolveCountryCode(countryHint || DEFAULT_COUNTRY_CODE)
.then(countryCode => {
this.form.countryCode = countryCode;
this.syncCountryName(countryCode);
return this.loadProvinceOptions(countryCode);
})
.then(() => {
this.form.provinceCode =
String(detail.provinceCode || '') || this.resolveProvinceCode(detail.provinceName);
const province = this.provinceOptions.find(item => item.id === this.form.provinceCode);
this.form.provinceName = province ? province.title : detail.provinceName || '';
return this.loadCityOptions(this.form.provinceCode);
})
.then(() => {
this.form.cityCode =
String(detail.cityCode || '') ||
this.resolveCityCode(detail.cityName, detail.districtCode || detail.code);
const city = this.cityOptions.find(item => item.id === this.form.cityCode);
this.form.city = city ? city.title : detail.cityName || '';
return this.loadDistrictOptions(this.form.cityCode);
})
.then(() => {
this.form.districtCode = this.resolveDistrictCode(
detail.districtName || detail.name,
detail.districtCode || detail.code
);
this.handleDistrictChange(this.form.districtCode);
});
},
applyMapRegionByNames(selection = {}) {
const provinceName = String(selection.province || selection.provinceName || '').trim();
const cityName = this.normalizeAmapCityName(
selection.city || selection.cityName,
provinceName
);
const districtName = String(selection.district || selection.districtName || '').trim();
const countryName = String(selection.country || selection.countryName || '').trim();
const adcode = String(selection.adcode || selection.regionCode || '').trim();
const countryPromise = countryName
? this.resolveCountryCode(countryName)
: Promise.resolve(/^\d{6}$/.test(adcode) ? DEFAULT_COUNTRY_CODE : '');
return countryPromise
.then(countryCode => {
if (!countryCode) {
return null;
}
this.form.countryCode = countryCode;
this.syncCountryName(countryCode);
return this.loadProvinceOptions(countryCode);
})
.then(provinces => {
if (!provinces) {
return null;
}
this.form.provinceCode = this.resolveProvinceCode(provinceName);
const province = this.provinceOptions.find(item => item.id === this.form.provinceCode);
this.form.provinceName = province ? province.title : provinceName;
if (!this.form.provinceCode) {
this.clearCityValue();
return null;
}
return this.loadCityOptions(this.form.provinceCode);
})
.then(cities => {
if (!cities) {
return null;
}
this.form.cityCode = this.resolveCityCode(cityName, adcode);
const city = this.cityOptions.find(item => item.id === this.form.cityCode);
this.form.city = city ? city.title : cityName;
if (!this.form.cityCode) {
this.clearDistrictValue();
return null;
}
return this.loadDistrictOptions(this.form.cityCode);
})
.then(districts => {
if (!districts) {
return;
}
this.form.districtCode = this.resolveDistrictCode(districtName, adcode);
this.handleDistrictChange(this.form.districtCode);
});
},
applyMapRegionSelection(selection = {}) {
if (this.isParentRegionLocked) {
return Promise.resolve();
}
const adcode = String(selection.adcode || selection.regionCode || '').trim();
const hasNameHint = Boolean(
selection.country ||
selection.countryName ||
selection.province ||
selection.provinceName ||
selection.city ||
selection.cityName ||
selection.district ||
selection.districtName
);
if (!adcode && !hasNameHint) {
return Promise.resolve();
}
const lookupCodes = this.buildRegionLookupCodes(adcode);
return this.fetchRegionDetailByCodes(lookupCodes)
.then(detail => {
if (detail) {
return this.applyRegionDetail(detail);
}
return this.applyMapRegionByNames(selection);
})
.catch(() => this.applyMapRegionByNames(selection));
},
resolveCountryCode(country) {
if (!country) {
@@ -1251,13 +1513,50 @@ export default {
this.$message({ type: 'success', message: '操作成功!' });
});
},
createEmptyPortTerminalForm() {
return {
category: '港口',
dataSource: '手动录入',
status: 1,
parentId: undefined,
parentCode: '',
parentName: '',
name: '',
code: '',
terminalCode: '',
country: '',
countryCode: '',
provinceCode: undefined,
provinceName: '',
cityCode: undefined,
city: '',
districtCode: undefined,
districtName: '',
regionCode: '',
detailAddress: '',
longitude: '',
latitude: '',
remark: '',
};
},
resetAddForm() {
// 新增必须整表重置,避免沿用上一次编辑/查看残留
this.form = this.createEmptyPortTerminalForm();
this.setProvinceOptions([]);
this.setCityOptions([]);
this.setDistrictOptions([]);
},
beforeOpen(done, type) {
if (type === 'add') {
this.form.category = this.form.category || '港口';
this.form.dataSource = this.form.dataSource || '手动录入';
this.form.status = this.form.status || 1;
this.resetAddForm();
this.handleCategoryChange('港口', false);
done();
this.$nextTick(() => {
this.$refs.crud?.clearValidate?.();
setTimeout(() => this.disableDialogAutocomplete(), 50);
});
return;
}
this.handleCategoryChange(this.form.category || '港口');
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
@@ -1292,35 +1591,7 @@ export default {
setTimeout(() => this.disableDialogAutocomplete(), 50);
});
});
return;
}
this.resolveCountryCode(this.form.country)
.then(countryCode => {
this.form.countryCode = countryCode;
this.syncCountryName(countryCode);
return this.loadProvinceOptions(countryCode);
})
.then(() => {
this.form.provinceCode =
String(this.form.provinceCode || '') ||
this.resolveProvinceCode(this.form.provinceName);
return this.loadCityOptions(this.form.provinceCode);
})
.then(() => {
this.form.cityCode = this.resolveCityCode(this.form.city, this.form.regionCode);
return this.loadDistrictOptions(this.form.cityCode);
})
.then(() => {
this.form.districtCode = this.resolveDistrictCode(
this.form.districtName,
this.form.regionCode
);
if (!this.form.regionCode) {
this.handleDistrictChange(this.form.districtCode);
}
done();
setTimeout(() => this.disableDialogAutocomplete(), 50);
});
},
disableDialogAutocomplete() {
this.$nextTick(() => {
@@ -1443,42 +1714,14 @@ export default {
</style>
<style lang="scss">
/* 港口码头编辑/新增弹窗:对齐 railway-station 统一风格(灰底 + 白卡表单 + 浮动底栏,padding 24px 与全局弹窗标准一致) */
//.port-terminal-edit-dialog {
// /* 灰底内容区 */
// .el-dialog__body {
// background-color: #f2f2f2;
// padding: 8px;
// }
// /* 表单白卡(与全局 .el-dialog .avue-form 一致,padding 24px 对齐 customer-archive / railway-station) */
// .avue-form {
// background: #fff;
// border-radius: 6px;
// box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
// padding: 24px !important;
// margin-bottom: 0;
// }
// /* 底部按钮区白卡(Avue 弹窗 footer 在 body 内,接续白卡) */
// .avue-dialog__footer {
// background: #fff;
// border-radius: 0 0 6px 6px;
// box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
// padding: 16px 24px !important;
// display: flex;
// justify-content: flex-end;
// }
// /* label 宽度统一 108px(对齐 railway-station,标签对齐更整齐) */
// .el-form-item__label {
// width: 108px !important;
// }
// /* 所有 input 文本框、select 下拉控件统一宽度 250px */
// .el-input,
// .el-select {
// width: 250px !important;
// }
// /* 备注宽度 85% */
// .remark-input {
// width: 85% !important;
// }
//}
/* 港口码头编辑/新增弹窗:与全局表单项行距保持一致 */
.port-terminal-edit-dialog {
.el-form-item,
.el-form-item--default.el-form-item,
.el-form-item--small.el-form-item,
.el-form-item--large.el-form-item,
.el-form-item--medium.el-form-item {
margin-bottom: 24px;
}
}
</style>
+307 -45
View File
@@ -144,7 +144,7 @@
<script>
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/railway-station';
import { getLazyTree } from '@/api/base/region';
import { getDetail as getRegionDetail, getLazyTree } from '@/api/base/region';
import { exportBlob } from '@/api/common';
import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
@@ -171,7 +171,12 @@ export default {
}
};
const validateTelegraphCode = (rule, value, callback) => {
if (!/^[A-Z]{3}$/.test(String(value || '').toUpperCase())) {
const telegraphCode = String(value || '').trim();
if (!telegraphCode) {
callback();
return;
}
if (!/^[A-Z]{3}$/.test(telegraphCode.toUpperCase())) {
callback(new Error('电报略码格式不正确或已存在'));
} else {
callback();
@@ -273,10 +278,7 @@ export default {
prop: 'telegraphCode',
minWidth: 100,
maxlength: 3,
rules: [
{ required: true, message: '电报略码格式不正确或已存在', trigger: 'blur' },
{ validator: validateTelegraphCode, trigger: 'blur' },
],
rules: [{ validator: validateTelegraphCode, trigger: 'blur' }],
change: ({ value }) => this.handleTelegraphChange(value),
blur: ({ value }) => this.handleTelegraphChange(value),
},
@@ -293,6 +295,20 @@ export default {
// prop: 'railwayLine',
// minWidth: 130,
// },
{
label: '详细地址',
prop: 'detailAddress',
formslot: true,
span: 24,
minWidth: 220,
overHidden: false,
maxlength: 255,
showWordLimit: true,
rules: [
{ required: true, message: '请选择详细地址', trigger: 'change' },
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
],
},
{
label: '所属省份',
prop: 'provinceCode',
@@ -360,20 +376,6 @@ export default {
addDisplay: false,
editDisplay: false,
},
{
label: '详细地址',
prop: 'detailAddress',
formslot: true,
span: 24,
minWidth: 220,
overHidden: false,
maxlength: 255,
showWordLimit: true,
rules: [
{ required: true, message: '请选择详细地址', trigger: 'change' },
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
],
},
{
label: '经度',
prop: 'longitude',
@@ -549,33 +551,51 @@ export default {
return targetColumns.find(item => item.prop === targetProp) || null;
},
loadProvinceOptions() {
getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
return getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
this.provinceOptions = (res.data.data || []).map(this.normalizeRegionOption);
this.findColumn('provinceCode').dicData = this.provinceOptions;
const provinceColumn = this.findColumn('provinceCode');
if (provinceColumn) {
provinceColumn.dicData = this.provinceOptions;
}
return this.provinceOptions;
});
},
loadCityOptions(provinceCode) {
if (!provinceCode) {
this.cityOptions = [];
this.findColumn('cityCode').dicData = [];
return Promise.resolve();
const cityColumn = this.findColumn('cityCode');
if (cityColumn) {
cityColumn.dicData = [];
}
return Promise.resolve([]);
}
return getLazyTree(provinceCode).then(res => {
this.cityOptions = (res.data.data || []).map(this.normalizeRegionOption);
this.findColumn('cityCode').dicData = this.cityOptions;
const cityColumn = this.findColumn('cityCode');
if (cityColumn) {
cityColumn.dicData = this.cityOptions;
}
return this.cityOptions;
});
},
loadDistrictOptions(cityCode) {
if (!cityCode) {
this.districtOptions = [];
this.findColumn('districtCode').dicData = [];
return Promise.resolve();
const districtColumn = this.findColumn('districtCode');
if (districtColumn) {
districtColumn.dicData = [];
}
return Promise.resolve([]);
}
this.districtLoading = true;
return getLazyTree(cityCode)
.then(res => {
this.districtOptions = (res.data.data || []).map(this.normalizeRegionOption);
this.findColumn('districtCode').dicData = this.districtOptions;
const districtColumn = this.findColumn('districtCode');
if (districtColumn) {
districtColumn.dicData = this.districtOptions;
}
return this.districtOptions;
})
.finally(() => {
this.districtLoading = false;
@@ -681,28 +701,269 @@ export default {
openAddressMapPicker() {
this.addressMapPickerVisible = true;
},
formatMapCoordinate(value) {
if (value === undefined || value === null || value === '') {
return '';
}
const number = Number(value);
if (!Number.isFinite(number)) {
return '';
}
return normalizeCoordinateInput(number.toFixed(6));
},
patchDialogForm(patch = {}) {
Object.assign(this.form, patch);
const tableForm = this.$refs.crud?.tableForm;
if (tableForm && tableForm !== this.form) {
Object.assign(tableForm, patch);
}
},
handleAddressMapConfirm(payload) {
const selection = typeof payload === 'string' ? { address: payload } : payload || {};
const longitude = this.formatMapCoordinate(selection.longitude);
const latitude = this.formatMapCoordinate(selection.latitude);
const patch = {};
if (selection.address) {
this.form.detailAddress = selection.address;
patch.detailAddress = selection.address;
}
if (longitude !== '' && latitude !== '') {
patch.longitude = longitude;
patch.latitude = latitude;
}
if (Object.keys(patch).length) {
this.patchDialogForm(patch);
}
this.applyMapRegionSelection(selection);
},
normalizeAmapCityName(city, province) {
if (Array.isArray(city)) {
return String(province || '').trim();
}
const cityName = String(city || '').trim();
return cityName || String(province || '').trim();
},
buildRegionLookupCodes(adcode) {
const code = String(adcode || '').trim();
if (!code) {
return [];
}
const codes = [code];
if (/^\d{6}$/.test(code)) {
if (code.endsWith('00')) {
const cityCode = code.slice(0, 4);
if (cityCode && !codes.includes(cityCode)) {
codes.push(cityCode);
}
}
if (code.endsWith('0000')) {
const provinceCode = code.slice(0, 2);
if (provinceCode && !codes.includes(provinceCode)) {
codes.push(provinceCode);
}
}
}
return codes;
},
fetchRegionDetailByCodes(codes = []) {
const tryNext = index => {
if (index >= codes.length) {
return Promise.resolve(null);
}
return getRegionDetail(codes[index])
.then(res => {
const detail = res.data && res.data.data;
if (detail && detail.code) {
return detail;
}
return tryNext(index + 1);
})
.catch(() => tryNext(index + 1));
};
return tryNext(0);
},
resolveProvinceCode(provinceName) {
const name = String(provinceName || '').trim();
if (!name) {
return '';
}
const province = this.provinceOptions.find(
item => item.title === name || item.id === name || String(item.title).includes(name)
);
return province ? province.id : '';
},
resolveCityCode(cityName, regionCode) {
const name = String(cityName || '').trim();
const code = String(regionCode || '').trim();
const city = this.cityOptions.find(
item => item.title === name || item.id === code || item.id === name
);
return city ? city.id : '';
},
resolveDistrictCode(districtName, regionCode) {
const name = String(districtName || '').trim();
const code = String(regionCode || '').trim();
const district = this.districtOptions.find(
item => item.title === name || item.id === code || item.id === name
);
return district ? district.id : code || '';
},
applyRegionDetail(detail = {}) {
const provinceName = detail.provinceName || '';
const cityName = detail.cityName || '';
const districtName = detail.districtName || detail.name || '';
return this.loadProvinceOptions()
.then(() => {
const provinceCode =
String(detail.provinceCode || '').trim() || this.resolveProvinceCode(provinceName);
const province = this.provinceOptions.find(item => item.id === provinceCode);
this.patchDialogForm({
provinceCode: provinceCode || undefined,
provinceName: province ? province.title : provinceName,
});
if (!provinceCode) {
return null;
}
return this.loadCityOptions(provinceCode);
})
.then(cities => {
if (!cities) {
return null;
}
const cityCode =
String(detail.cityCode || '').trim() ||
this.resolveCityCode(cityName, detail.districtCode || detail.code);
const city = this.cityOptions.find(item => item.id === cityCode);
this.patchDialogForm({
cityCode: cityCode || undefined,
cityName: city ? city.title : cityName,
});
this.ensureCityOption({
cityCode,
cityName: city ? city.title : cityName,
});
if (!cityCode) {
return null;
}
return this.loadDistrictOptions(cityCode);
})
.then(districts => {
if (!districts) {
return;
}
const districtCode = this.resolveDistrictCode(
districtName,
detail.districtCode || detail.code
);
this.patchDialogForm({
districtCode: districtCode || undefined,
});
this.handleDistrictChange(districtCode);
this.ensureDistrictOption({
districtCode,
districtName: this.form.districtName || districtName,
});
});
},
applyMapRegionByNames(selection = {}) {
const provinceName = String(selection.province || selection.provinceName || '').trim();
const cityName = this.normalizeAmapCityName(
selection.city || selection.cityName,
provinceName
);
const districtName = String(selection.district || selection.districtName || '').trim();
const adcode = String(selection.adcode || selection.regionCode || '').trim();
// 本页无国家字段:国内 adcode 走中国省份树;无匹配则不强制回填
if (
selection.longitude !== undefined &&
selection.longitude !== null &&
selection.longitude !== ''
!provinceName &&
!cityName &&
!districtName &&
!(/^\d{6}$/.test(adcode) || String(selection.country || selection.countryName || '').includes('中国'))
) {
this.form.longitude = Number(selection.longitude).toFixed(6);
return Promise.resolve();
}
if (
selection.latitude !== undefined &&
selection.latitude !== null &&
selection.latitude !== ''
) {
this.form.latitude = Number(selection.latitude).toFixed(6);
return this.loadProvinceOptions()
.then(() => {
const provinceCode = this.resolveProvinceCode(provinceName);
const province = this.provinceOptions.find(item => item.id === provinceCode);
this.patchDialogForm({
provinceCode: provinceCode || undefined,
provinceName: province ? province.title : provinceName,
});
if (!provinceCode) {
return null;
}
return this.loadCityOptions(provinceCode);
})
.then(cities => {
if (!cities) {
return null;
}
const cityCode = this.resolveCityCode(cityName, adcode);
const city = this.cityOptions.find(item => item.id === cityCode);
this.patchDialogForm({
cityCode: cityCode || undefined,
cityName: city ? city.title : cityName,
});
this.ensureCityOption({
cityCode,
cityName: city ? city.title : cityName,
});
if (!cityCode) {
return null;
}
return this.loadDistrictOptions(cityCode);
})
.then(districts => {
if (!districts) {
return;
}
const districtCode = this.resolveDistrictCode(districtName, adcode);
this.patchDialogForm({
districtCode: districtCode || undefined,
});
this.handleDistrictChange(districtCode);
this.ensureDistrictOption({
districtCode,
districtName: this.form.districtName || districtName,
});
});
},
applyMapRegionSelection(selection = {}) {
const adcode = String(selection.adcode || selection.regionCode || '').trim();
const hasNameHint = Boolean(
selection.country ||
selection.countryName ||
selection.province ||
selection.provinceName ||
selection.city ||
selection.cityName ||
selection.district ||
selection.districtName
);
if (!adcode && !hasNameHint) {
return Promise.resolve();
}
this.regionInitializing = true;
const lookupCodes = this.buildRegionLookupCodes(adcode);
return this.fetchRegionDetailByCodes(lookupCodes)
.then(detail => {
if (detail) {
return this.applyRegionDetail(detail);
}
return this.applyMapRegionByNames(selection);
})
.catch(() => this.applyMapRegionByNames(selection))
.finally(() => {
this.$nextTick(() => {
window.setTimeout(() => {
this.regionInitializing = false;
}, 0);
});
});
},
normalizeRow(row) {
row.code = row.tmisCode ? `TL${row.tmisCode}` : row.code;
row.telegraphCode = String(row.telegraphCode || '').trim() || undefined;
row.regionCode = String(row.regionCode || '').trim();
row.regionName = String(row.regionName || '').trim();
row.districtCode = String(row.districtCode || '').trim();
@@ -735,14 +996,15 @@ export default {
return records.some(item => item[prop] === row[prop] && String(item.id || '') !== rowId);
});
};
return Promise.all([
hasDuplicate({ tmisCode: row.tmisCode }, 'tmisCode'),
hasDuplicate({ telegraphCode: row.telegraphCode }, 'telegraphCode'),
]).then(([tmisExists, telegraphExists]) => {
if (tmisExists) {
const uniqueChecks = [hasDuplicate({ tmisCode: row.tmisCode }, 'tmisCode')];
if (row.telegraphCode) {
uniqueChecks.push(hasDuplicate({ telegraphCode: row.telegraphCode }, 'telegraphCode'));
}
return Promise.all(uniqueChecks).then(results => {
if (results[0]) {
return Promise.reject(new Error('该TMIS编码已存在'));
}
if (telegraphExists) {
if (results[1]) {
return Promise.reject(new Error('电报略码格式不正确或已存在'));
}
return Promise.resolve();
+1 -1
View File
@@ -787,6 +787,6 @@ export default {
margin-bottom: 12px !important;
}
.el-form-item{
margin-bottom: 9px !important;
margin-bottom: 24px !important;
}
</style>
+2 -1
View File
@@ -352,7 +352,8 @@ export default {
return result;
},
normalizeListQuery(params = {}) {
const query = { ...params, ...this.query };
// 搜索条件优先于缓存的 query,避免默认组织 ID 覆盖用户新选择的组织。
const query = { ...this.query, ...params };
const rawDeptId = query.deptId ?? query.searchDeptId;
delete query.searchDeptId;
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
+41 -17
View File
@@ -415,19 +415,27 @@ export default {
});
},
buildDeptTree(tree) {
return tree.map(item => ({
label: item.title || item.deptName || item.name,
value: item.id,
children:
item.children && item.children.length ? this.buildDeptTree(item.children) : undefined,
}));
return tree.map(item => {
// 雪花 ID 必须保持字符串,避免 JS Number 精度丢失导致筛错组织。
const id =
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
return {
label: item.title || item.deptName || item.name,
value: id,
children:
item.children && item.children.length ? this.buildDeptTree(item.children) : undefined,
};
}).filter(item => item.value);
},
flattenDept(tree, level = 0) {
const result = [];
tree.forEach(item => {
const id =
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
if (!id) return;
result.push({
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
value: item.id,
value: id,
});
if (item.children && item.children.length) {
result.push(...this.flattenDept(item.children, level + 1));
@@ -436,7 +444,26 @@ export default {
return result;
},
defaultDeptId() {
return (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
const raw = (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
return (
String(raw)
.split(',')
.map(item => item.trim())
.filter(Boolean)[0] || ''
);
},
normalizeListQuery(params = {}) {
// 搜索条件优先于缓存的 query,避免默认组织 ID 覆盖用户新选择的组织。
const query = { ...this.query, ...params };
const rawDeptId = query.deptId ?? query.searchDeptId;
delete query.searchDeptId;
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
query.deptId = String(rawDeptId).split(',')[0].trim();
} else {
delete query.deptId;
}
query.allDept = this.isAdmin ? 1 : 0;
return query;
},
applyDefaultDeptSearch() {
const deptId = this.defaultDeptId();
@@ -999,9 +1026,9 @@ export default {
this.onLoad(this.page, this.query);
},
searchChange(params, done) {
this.query = params;
this.query = this.normalizeListQuery(params);
this.page.currentPage = 1;
this.onLoad(this.page, params);
this.onLoad(this.page, this.query);
done();
},
selectionChange(list) {
@@ -1024,12 +1051,10 @@ export default {
},
onLoad(page, params = {}) {
this.loading = true;
const query = this.normalizeListQuery(params);
this.query = query;
api
.getList(page.currentPage, page.pageSize, {
...params,
...this.query,
allDept: this.isAdmin ? 1 : 0,
})
.getList(page.currentPage, page.pageSize, query)
.then(res => {
const result = res.data.data;
this.page.total = result.total;
@@ -1042,8 +1067,7 @@ export default {
},
buildExportParams() {
return {
...this.query,
allDept: this.isAdmin ? 1 : 0,
...this.normalizeListQuery(this.query),
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
@@ -171,7 +171,7 @@
:label="item.label"
:value="item.value" /></el-select></template
></el-table-column>
<el-table-column label="单价(元)" width="180"
<el-table-column :label="unitPriceColumnLabel" width="180"
><template #default="{ row }"
><div v-if="readonly && usesRangeUnitPrice(row)" class="limit-list">
<span v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex">{{
@@ -379,6 +379,7 @@ import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
import { InfoFilled } from '@element-plus/icons-vue';
import { getDictionary } from '@/api/system/dictbiz';
import SectionCard from '@/components/section-card/main.vue';
import { currencyUnit } from '@/utils/currency';
const clone = value => JSON.parse(JSON.stringify(value));
/** 计费要素与计量单位维度的对应关系(这些要素可选计费单位) */
@@ -386,12 +387,15 @@ const BILLING_ELEMENT_DIMENSION_MAP = {
按重量: '重量',
按体积: '体积',
按数量: '数量',
按里程: '里程',
'按吨·公里': '吨·公里',
'固定金额(整单一口价)': '整车',
};
const BILLING_UNIT_SELECTABLE_ELEMENTS = Object.keys(BILLING_ELEMENT_DIMENSION_MAP);
/** 固定计费单位(不可编辑) */
const FIXED_BILLING_UNIT_MAP = {
/** 对应维度下无计量单位时的默认计费单位 */
const DEFAULT_BILLING_UNIT_MAP = {
按里程: '公里',
'按吨·公里': '吨公里',
'按吨·公里': '吨·公里',
'固定金额(整单一口价)': '单',
};
const defaultRule = () => ({
@@ -428,6 +432,7 @@ export default {
value: { type: Object, default: () => ({}) },
index: { type: Number, default: -1 },
readonly: Boolean,
settlementCurrency: { type: String, default: 'RMB' },
},
emits: ['update:modelValue', 'save'],
data() {
@@ -497,6 +502,9 @@ export default {
emitPath: true,
};
},
unitPriceColumnLabel() {
return `单价(${currencyUnit(this.settlementCurrency)})`;
},
},
mounted() {
this.loadDictionaries();
@@ -652,58 +660,61 @@ export default {
}),
]).finally(() => {
this.unitLoading = false;
(this.draft.rules || []).forEach(row => this.syncBillingUnit(row));
});
},
measurementDimension(billingElement) {
return BILLING_ELEMENT_DIMENSION_MAP[billingElement] || '';
},
fixedBillingUnit(billingElement) {
return FIXED_BILLING_UNIT_MAP[String(billingElement || '').trim()] || '';
defaultBillingUnit(billingElement) {
return DEFAULT_BILLING_UNIT_MAP[String(billingElement || '').trim()] || '';
},
canSelectBillingUnit(row) {
return BILLING_UNIT_SELECTABLE_ELEMENTS.includes(String(row?.billingElement || '').trim());
},
billingUnitPlaceholder(row) {
if (!row?.billingElement) return '请先选择计费要素';
if (this.fixedBillingUnit(row.billingElement)) return this.fixedBillingUnit(row.billingElement);
if (!this.canSelectBillingUnit(row)) return '当前计费要素无需选择';
return '请选择';
},
unitOptionsFor(row) {
const fixedUnit = this.fixedBillingUnit(row?.billingElement);
if (fixedUnit) {
return [{ id: fixedUnit, label: fixedUnit, value: fixedUnit }];
}
if (!this.canSelectBillingUnit(row)) {
return [];
}
const dimension = this.measurementDimension(row?.billingElement);
if (dimension) {
return this.measurementUnits
.filter(item => String(item.dimension || '').trim() === dimension)
.map(item => ({
id: item.id,
label: item.unitName,
value: item.unitName,
}))
.filter(item => item.value);
const defaultUnit = this.defaultBillingUnit(row?.billingElement);
const options = dimension
? this.measurementUnits
.filter(item => String(item.dimension || '').trim() === dimension)
.map(item => ({
id: item.id,
label: item.unitName,
value: item.unitName,
}))
.filter(item => item.value)
: [];
// 对应维度未配置计量单位时,下拉仅展示默认值
if (!options.length && defaultUnit) {
return [{ id: `default-${defaultUnit}`, label: defaultUnit, value: defaultUnit }];
}
return [];
return options;
},
syncBillingUnit(row) {
const fixedUnit = this.fixedBillingUnit(row?.billingElement);
if (fixedUnit) {
row.billingUnit = fixedUnit;
return;
}
if (!this.canSelectBillingUnit(row)) {
row.billingUnit = '';
return;
}
const options = this.unitOptionsFor(row);
if (!options.some(item => String(item.value) === String(row.billingUnit || ''))) {
row.billingUnit = '';
const defaultUnit = this.defaultBillingUnit(row?.billingElement);
const current = String(row.billingUnit || '').trim();
if (options.some(item => String(item.value) === current)) {
return;
}
if (defaultUnit && options.some(item => String(item.value) === defaultUnit)) {
row.billingUnit = defaultUnit;
return;
}
row.billingUnit = options[0]?.value || defaultUnit || '';
},
feeTypeKey(row) {
const option = this.feeCategories.find(
@@ -215,7 +215,7 @@
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="item.contractName"
:label="formatContractOptionLabel(item)"
:value="item.id"
/>
</el-select>
@@ -2065,11 +2065,22 @@
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="文件名" min-width="200" align="left" show-overflow-tooltip>
<el-table-column
label="文件名"
min-width="200"
align="left"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<el-link type="primary" @click="previewAttachment(row)">
{{ row.originalName || row.name }}
</el-link>
<span
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(row)"
@keydown.enter="previewAttachment(row)"
@keydown.space.prevent="previewAttachment(row)"
>{{ row.originalName || row.name }}</span>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220" align="center">
@@ -2338,14 +2349,16 @@
<div class="business-crud-page__transport-plan-detail-field is-attachments">
<span>附件</span>
<div class="business-crud-page__transport-plan-detail-attachments">
<el-link
<span
v-for="file in attachmentRows"
:key="file.url || file.name || file.originalName"
type="primary"
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(file)"
>
{{ attachmentName(file) }}
</el-link>
@keydown.enter="previewAttachment(file)"
@keydown.space.prevent="previewAttachment(file)"
>{{ attachmentName(file) }}</span>
<span v-if="!attachmentRows.length">-</span>
</div>
</div>
@@ -2531,14 +2544,16 @@
<div class="business-crud-page__waybill-attachments">
<span class="business-crud-page__waybill-label">附件</span>
<div>
<el-link
<span
v-for="file in attachmentRows"
:key="file.url || file.name || file.originalName"
type="primary"
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(file)"
>
{{ file.originalName || file.name || '-' }}
</el-link>
@keydown.enter="previewAttachment(file)"
@keydown.space.prevent="previewAttachment(file)"
>{{ file.originalName || file.name || '-' }}</span>
<span v-if="!attachmentRows.length">-</span>
</div>
</div>
@@ -2820,11 +2835,21 @@
</div>
<el-table :data="attachmentRows" empty-text="暂无附件">
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="文件名" min-width="260">
<el-table-column
label="文件名"
min-width="260"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }}
</el-link>
<span
class="attachment-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
@@ -3056,7 +3081,7 @@
<el-option
v-for="item in transportPlanImportContractOptions"
:key="item.id"
:label="item.contractName"
:label="formatContractOptionLabel(item)"
:value="item.id"
/>
</el-select>
@@ -4225,11 +4250,22 @@
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="文件名" min-width="200" align="center" show-overflow-tooltip>
<el-table-column
label="文件名"
min-width="200"
align="center"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<el-link type="primary" @click="previewAttachment(row, dispatchItemAttachmentRows)">
{{ attachmentName(row) }}
</el-link>
<span
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(row, dispatchItemAttachmentRows)"
@keydown.enter="previewAttachment(row, dispatchItemAttachmentRows)"
@keydown.space.prevent="previewAttachment(row, dispatchItemAttachmentRows)"
>{{ attachmentName(row) }}</span>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220" align="center">
@@ -5242,8 +5278,10 @@ import { addressTypeOptions } from '@/option/base/common-address';
import { packageOptions } from '@/option/business/common';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken, isAdministrator } from '@/utils/auth';
import { formatContractOptionLabel } from '@/utils/contract';
import { openImportDialog } from '@/utils/import-excel';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate';
@@ -5822,7 +5860,7 @@ export default {
billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'],
reconciliationModeOptions: ['明细逐行核对', '按账单汇总核对', '跳过自动核对'],
packageOptions,
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
quantityUnitOptions: [],
attachmentFileTypes: [
'pdf',
'bmp',
@@ -5865,7 +5903,6 @@ export default {
this.ensureBillingUnitOptions();
}
if (this.config.enableTaskInfoForm) {
this.loadTaskQuantityUnitOptions();
this.loadTaskFeeUnitOptions();
this.ensureBillingCargoTypeOptions();
}
@@ -5886,6 +5923,7 @@ export default {
} else {
this.consumeTemplateCreatePayload();
}
this.loadTaskQuantityUnitOptions();
},
computed: {
...mapGetters(['permission', 'userInfo']),
@@ -6589,6 +6627,7 @@ export default {
},
},
methods: {
formatContractOptionLabel,
initStandaloneFormPage() {
if (!this.isStandaloneFormPage) return;
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
@@ -7789,8 +7828,6 @@ export default {
['contractName', '客户合同'],
['planName', '计划名称'],
['transportType', '运输方式'],
['planStartDate', '计划开始日期'],
['planEndDate', '计划结束日期'],
['departureAddress', '发货地址'],
['arrivalAddress', '收货地址'],
];
@@ -9270,9 +9307,10 @@ export default {
return Promise.resolve(this.taskQuantityUnitOptions);
}
this.taskQuantityUnitLoading = true;
return getDictionary({ code: 'quantity_unit' })
.then(res => {
this.taskQuantityUnitOptions = this.normalizeDictOptions(res.data.data || []);
return loadMeasurementUnitOptions()
.then(options => {
this.taskQuantityUnitOptions = options;
this.quantityUnitOptions = options.map(item => item.value);
return this.taskQuantityUnitOptions;
})
.finally(() => {
@@ -14111,7 +14149,7 @@ export default {
&__dispatch-shipping-form {
:deep(.el-form-item) {
margin-bottom: 14px;
margin-bottom: 24px;
}
}
@@ -14634,7 +14672,7 @@ export default {
width: 100%;
:deep(.el-form-item) {
margin-bottom: 14px;
margin-bottom: 24px;
}
:deep(.el-select),
@@ -16251,7 +16289,7 @@ export default {
}
:global(.business-crud-page__mileage-dialog .business-crud-page__mileage-form .el-form-item) {
margin-bottom: 16px;
margin-bottom: 24px;
}
:global(.business-crud-page__mileage-dialog .business-crud-page__mileage-form .el-input),
@@ -16,7 +16,7 @@
class="contract-manage-form__attachment-table"
@selection-change="selected = $event"
>
<el-table-column v-if="!readonly" type="selection" width="55" align="center" />
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column v-if="attachmentType" label="附件类型" min-width="160" align="center">
<template #default="{ row, $index }">
@@ -45,11 +45,17 @@
align="left"
header-align="left"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<el-link type="primary" @click="preview?.(row, rows)">{{
attachmentName(row)
}}</el-link>
<span
class="attachment-file-name"
role="button"
tabindex="0"
@click="preview?.(row, rows)"
@keydown.enter="preview?.(row, rows)"
@keydown.space.prevent="preview?.(row, rows)"
>{{ attachmentName(row) }}</span>
</template>
</el-table-column>
<el-table-column v-if="description" label="附件描述" min-width="220">
@@ -173,13 +179,41 @@ import { downloadFileByUrl } from '@/utils/util';
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
const CONTRACT_ATTACHMENT_TYPE_OTHER = '其他文件';
/** 用印材料历史取值兼容别名 */
export const CONTRACT_SEAL_MATERIAL_LOCATION = '用印材料';
export const CONTRACT_SEAL_MATERIAL_TYPE_ALIASES = {
合同文件: CONTRACT_SEAL_MATERIAL_LOCATION,
};
/** 佐证材料附件位置(历史:其它附件) */
export const CONTRACT_SUPPORTING_ATTACHMENT_LOCATION = '附件(佐证材料等)';
export const CONTRACT_ATTACHMENT_TYPE_DICT = 'contract_attachment_type';
export const CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT = 'contract_supporting_attachment_type';
export const isContractSupportingAttachmentLocation = location => {
const text = String(location || '');
return (
text === CONTRACT_SUPPORTING_ATTACHMENT_LOCATION ||
text === '其它附件' ||
text === '其他附件'
);
};
export const resolveAttachmentTypeDictCode = (location, fallback = CONTRACT_ATTACHMENT_TYPE_DICT) =>
isContractSupportingAttachmentLocation(location)
? CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT
: fallback || CONTRACT_ATTACHMENT_TYPE_DICT;
export const defaultContractAttachmentTypeOptions = [
{ label: '合同文件', value: '合同文件' },
{ label: '用印材料', value: '用印材料' },
{ label: '双章归档文件', value: '双章归档文件' },
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
];
/** @deprecated 兼容旧引用,实际选项由业务字典 contract_attachment_type 动态加载 */
export const defaultContractSupportingAttachmentTypeOptions = [
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
];
/** @deprecated 兼容旧引用,实际选项由业务字典动态加载 */
export const contractAttachmentTypeOptions = defaultContractAttachmentTypeOptions;
export const isContractSealMaterialLocation = location => {
const text = String(location || '');
return text === CONTRACT_SEAL_MATERIAL_LOCATION || text === '合同文件';
};
export const attachmentFileTypes = [
'pdf',
'bmp',
@@ -234,8 +268,11 @@ const isOtherAttachmentTypeOption = item => {
const attachmentTypeKeywords = fileType => {
const text = String(fileType || '');
if (text.includes('双章')) return ['双章归档文件', '双章归档', '双章'];
if (text.includes('合同') && !text.includes('其他') && !text.includes('其它')) {
return ['合同文件', '合同'];
if (
text.includes('用印') ||
(text.includes('合同') && !text.includes('其他') && !text.includes('其它'))
) {
return ['用印材料', '合同文件', '用印', '合同'];
}
if (isOtherAttachmentTypeOption({ label: text, value: text })) return [];
return text ? [text] : [];
@@ -254,8 +291,13 @@ export const resolveContractAttachmentType = (
const list = options.length ? options : defaultContractAttachmentTypeOptions;
const otherType = resolveOtherAttachmentType(list);
const values = list.map(item => item.value);
if (values.includes(row.fileType)) return row.fileType;
const matchedByLabel = list.find(item => String(item.label) === String(row.fileType));
const rawFileType = String(row.fileType || '');
const aliasedFileType = CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawFileType] || rawFileType;
if (values.includes(aliasedFileType)) return aliasedFileType;
if (values.includes(rawFileType)) return rawFileType;
const matchedByLabel = list.find(
item => String(item.label) === aliasedFileType || String(item.label) === rawFileType
);
if (matchedByLabel) return matchedByLabel.value;
const fileName = normalizeAttachmentTypeText(attachmentName(row));
if (!fileName) return otherType;
@@ -283,17 +325,32 @@ export const normalizeContractFileRows = (rows, options = defaultContractAttachm
description: row.description || '',
}));
const mapDictOptions = response => {
const mapDictOptions = (response, dictCode = CONTRACT_ATTACHMENT_TYPE_DICT) => {
const data = response?.data?.data || response?.data || [];
const records = Array.isArray(data) ? data : data.records || [];
const applySealAlias = dictCode === CONTRACT_ATTACHMENT_TYPE_DICT;
return records
.map(item => ({
label: item.dictValue,
value: item.dictKey,
}))
.map(item => {
const rawLabel = String(item.dictValue || '');
const rawValue = String(item.dictKey || '');
const label = applySealAlias
? CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawLabel] ||
(rawLabel === '合同文件' ? CONTRACT_SEAL_MATERIAL_LOCATION : rawLabel)
: rawLabel;
const value = applySealAlias
? CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawValue] ||
(rawValue === '合同文件' ? CONTRACT_SEAL_MATERIAL_LOCATION : rawValue)
: rawValue;
return { label, value };
})
.filter(item => item.label && item.value);
};
const defaultOptionsByDictCode = dictCode =>
dictCode === CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT
? [...defaultContractSupportingAttachmentTypeOptions]
: [...defaultContractAttachmentTypeOptions];
export default {
name: 'ContractAttachmentSection',
components: { UploadFilled },
@@ -307,8 +364,13 @@ export default {
markApprovedOnUpload: Boolean,
useUploadDialog: Boolean,
dialogOnly: Boolean,
attachmentLocation: { type: String, default: '合同文件' },
attachmentLocation: { type: String, default: CONTRACT_SEAL_MATERIAL_LOCATION },
attachmentLocationOptions: { type: Array, default: null },
/** 附件类型业务字典 code,默认用印材料字典;佐证材料传 contract_supporting_attachment_type */
attachmentTypeDictCode: {
type: String,
default: CONTRACT_ATTACHMENT_TYPE_DICT,
},
preview: { type: Function, default: null },
},
emits: ['update:rows', 'upload-to-location', 'upload-confirmed'],
@@ -317,8 +379,9 @@ export default {
selected: [],
attachmentFileTypes,
contractAttachmentTypeOptions: [...defaultContractAttachmentTypeOptions],
activeAttachmentTypeDictCode: CONTRACT_ATTACHMENT_TYPE_DICT,
uploadDialogVisible: false,
uploadDialogLocation: '合同文件',
uploadDialogLocation: CONTRACT_SEAL_MATERIAL_LOCATION,
uploadDialogType: resolveOtherAttachmentType(defaultContractAttachmentTypeOptions),
uploadDialogFiles: [],
uploadDialogFileList: [],
@@ -337,48 +400,91 @@ export default {
defaultAttachmentType() {
return resolveOtherAttachmentType(this.contractAttachmentTypeOptions);
},
resolvedAttachmentTypeDictCode() {
return this.attachmentTypeDictCode || CONTRACT_ATTACHMENT_TYPE_DICT;
},
resolvedLocationOptions() {
const options = (this.attachmentLocationOptions || []).filter(Boolean);
if (options.length) return options;
return ['合同文件', '其它附件'];
if (options.length) {
return options.map(item => {
if (item === '合同文件') return CONTRACT_SEAL_MATERIAL_LOCATION;
if (item === '其它附件' || item === '其他附件') {
return CONTRACT_SUPPORTING_ATTACHMENT_LOCATION;
}
return item;
});
}
return [CONTRACT_SEAL_MATERIAL_LOCATION, CONTRACT_SUPPORTING_ATTACHMENT_LOCATION];
},
},
watch: {
attachmentTypeDictCode: {
immediate: false,
handler() {
this.loadAttachmentTypeOptions(this.resolvedAttachmentTypeDictCode, true);
},
},
},
created() {
this.loadAttachmentTypeOptions();
this.loadAttachmentTypeOptions(this.resolvedAttachmentTypeDictCode, true);
},
methods: {
attachmentName,
loadAttachmentTypeOptions() {
return getBizDictionary({ code: 'contract_attachment_type' })
currentUploadUserName() {
const raw = this.$store.getters.userInfo;
const userInfo = raw && !Array.isArray(raw) ? raw : {};
// 与顶部栏一致优先 real_name;用印材料 / 佐证材料共用
return (
userInfo.real_name ||
userInfo.realName ||
userInfo.nick_name ||
userInfo.nickName ||
userInfo.user_name ||
userInfo.userName ||
userInfo.account ||
''
);
},
loadAttachmentTypeOptions(dictCode = this.resolvedAttachmentTypeDictCode, alignRows = false) {
const code = dictCode || CONTRACT_ATTACHMENT_TYPE_DICT;
this.activeAttachmentTypeDictCode = code;
const fallbackOptions = defaultOptionsByDictCode(code);
return getBizDictionary({ code })
.then(res => {
const options = mapDictOptions(res);
// 切换字典期间若已发起更新请求,忽略过期响应
if (this.activeAttachmentTypeDictCode !== code) return;
const options = mapDictOptions(res, code);
if (options.length) {
// 字典缺少「其他文件」时补上,保证未匹配文件名可默认选中
this.contractAttachmentTypeOptions = options.some(isOtherAttachmentTypeOption)
? options
: [...options, { label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER }];
if (
!this.contractAttachmentTypeOptions.some(
item => String(item.value) === String(this.uploadDialogType)
)
) {
this.uploadDialogType = this.defaultAttachmentType;
}
// 字典加载后,把已有行的中文类型名对齐到 dictKey,保证下拉可选中
if (this.attachmentType && (this.rows || []).length) {
const aligned = (this.rows || []).map(row => ({
...row,
fileType: resolveContractAttachmentType(row, this.contractAttachmentTypeOptions),
}));
const changed = aligned.some(
(row, index) => row.fileType !== this.rows[index]?.fileType
);
if (changed) this.update(aligned);
}
} else {
this.contractAttachmentTypeOptions = fallbackOptions;
}
if (
!this.contractAttachmentTypeOptions.some(
item => String(item.value) === String(this.uploadDialogType)
)
) {
this.uploadDialogType = this.defaultAttachmentType;
}
// 字典加载后,把已有行的中文类型名对齐到 dictKey,保证下拉可选中
if (alignRows && this.attachmentType && (this.rows || []).length) {
const aligned = (this.rows || []).map(row => ({
...row,
fileType: resolveContractAttachmentType(row, this.contractAttachmentTypeOptions),
}));
const changed = aligned.some(
(row, index) => row.fileType !== this.rows[index]?.fileType
);
if (changed) this.update(aligned);
}
})
.catch(() => {
// 字典加载失败时保留默认选项
if (this.activeAttachmentTypeDictCode !== code) return;
this.contractAttachmentTypeOptions = fallbackOptions;
this.uploadDialogType = this.defaultAttachmentType;
});
},
isRowLocked(row) {
@@ -388,13 +494,14 @@ export default {
this.$emit('update:rows', Array.isArray(rows) ? rows.map(item => ({ ...item })) : []);
},
enrichUploadedRow(row, existing) {
const uploadUserName = this.$store.getters.userInfo?.realName || '';
const uploadUserName = this.currentUploadUserName();
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
const next = {
...existing,
...row,
description: row.description || existing?.description || '',
uploadUserName: existing?.uploadUserName || row.uploadUserName || uploadUserName,
// 新上传行始终写入当前用户;已有行保留原上传人
uploadUserName: existing?.uploadUserName || uploadUserName || row.uploadUserName || '',
uploadTime: existing?.uploadTime || row.uploadTime || uploadTime,
approved: existing?.approved || row.approved || false,
};
@@ -434,20 +541,38 @@ export default {
resolveTypeByLocation(location) {
const options = this.contractAttachmentTypeOptions || [];
const locationText = String(location || '');
if (locationText.includes('其它') || locationText.includes('其他')) {
if (
isContractSupportingAttachmentLocation(locationText) ||
locationText.includes('佐证') ||
locationText.includes('其它') ||
locationText.includes('其他')
) {
return this.defaultAttachmentType;
}
const matched = options.find(
item =>
String(item.value) === locationText ||
String(item.label) === locationText ||
String(item.value).includes('用印') ||
String(item.label).includes('用印') ||
String(item.value).includes('合同') ||
String(item.label).includes('合同')
);
return matched?.value || options[0]?.value || this.defaultAttachmentType;
},
syncUploadDialogTypeByLocation() {
this.uploadDialogType = this.resolveTypeByLocation(this.uploadDialogLocation);
const dictCode = resolveAttachmentTypeDictCode(
this.uploadDialogLocation,
this.resolvedAttachmentTypeDictCode
);
const applyType = () => {
this.uploadDialogType = this.resolveTypeByLocation(this.uploadDialogLocation);
};
if (dictCode !== this.activeAttachmentTypeDictCode) {
this.loadAttachmentTypeOptions(dictCode, false).finally(applyType);
return;
}
applyType();
},
openUploadDialog() {
const options = this.resolvedLocationOptions;
@@ -544,13 +669,13 @@ export default {
this.$message.warning('请选择附件类型');
return;
}
const uploadUserName = this.$store.getters.userInfo?.realName || '';
const uploadUserName = this.currentUploadUserName();
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
const appended = this.uploadDialogFiles.map(row => ({
...row,
fileType: this.uploadDialogType,
description: row.description || '',
uploadUserName: row.uploadUserName || uploadUserName,
uploadUserName: uploadUserName || row.uploadUserName || '',
uploadTime: row.uploadTime || uploadTime,
...(this.markApprovedOnUpload ? { approved: true } : {}),
}));
@@ -607,7 +732,15 @@ export default {
downloadFileByUrl(url, attachmentName(row));
},
batchDownload() {
(this.selected.length ? this.selected : this.rows || []).forEach(this.download);
const sourceRows = this.selected.length ? this.selected : this.rows || [];
const downloadableRows = sourceRows.filter(row => attachmentUrl(row));
if (!downloadableRows.length) {
this.$message.warning(this.selected.length ? '所选附件暂无可下载地址' : '暂无可下载附件');
return;
}
downloadableRows.forEach((row, index) => {
window.setTimeout(() => this.download(row), index * 400);
});
},
formatSize(size) {
const value = Number(size);
@@ -44,13 +44,16 @@
<dt>附件</dt>
<dd>
<template v-if="attachments.length">
<el-link
<span
v-for="file in attachments"
:key="file.url || file.link || file.name || file.originalName"
type="primary"
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(file)"
>{{ attachmentName(file) }}</el-link
>
@keydown.enter="previewAttachment(file)"
@keydown.space.prevent="previewAttachment(file)"
>{{ attachmentName(file) }}</span>
</template>
<span v-else>-</span>
</dd>
@@ -330,7 +333,7 @@ export default {
.transport-flow-tag {
margin-left: 8px;
}
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #303133; font-size: 14px; word-break: break-all; } :deep(.el-link) { font-size: 14px; } }
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #303133; font-size: 14px; word-break: break-all; } .attachment-file-name { font-size: 14px; max-width: 100%; } }
.route-map { display: flex; align-items: flex-start; padding: 16px 24px 20px; border-top: 1px solid #eff1f7; overflow-x: auto; }
.route-map__node { display: grid; flex: 0 0 150px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } }
.route-map__progress { color: #303133 !important; }
@@ -266,7 +266,7 @@
<el-col :span="6">
<el-form-item label="单价">
<el-input :model-value="freightGroupUnitPrice(group)" inputmode="decimal" placeholder="请输入" @input="value => handleFreightGroupUnitPriceInput(group, value)" @clear="() => handleFreightGroupUnitPriceInput(group, '')">
<template #append><el-select :model-value="freightGroupPriceUnit(group)" class="freight-unit-select" @change="value => handleFreightGroupPriceUnitChange(group, value)"><el-option label="元/吨" value="元/吨" /><el-option label="元/件" value="元/件" /><el-option label="元/方" value="元/方" /></el-select></template>
<template #append><el-select :model-value="freightGroupPriceUnit(group)" class="freight-unit-select" @change="value => handleFreightGroupPriceUnitChange(group, value)"><el-option v-for="item in priceUnitOptions(route)" :key="item.value" :label="item.label" :value="item.value" /></el-select></template>
</el-input>
</el-form-item>
</el-col>
@@ -317,6 +317,13 @@
<script>
import * as api from '@/api/business/master-order';
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
import { formatContractOptionLabel } from '@/utils/contract';
import {
currencyUnit,
remapPriceUnitCurrency,
resolvePriceUnitByQuantityUnit,
} from '@/utils/currency';
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
import { isMobile } from '@/utils/validate';
const unwrapRecords = res => {
@@ -347,7 +354,8 @@ export default {
carrierLoading: false,
driverLoading: false,
escortLoading: false,
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
quantityUnitOptions: [],
quantityUnitLoading: false,
};
},
computed: {
@@ -389,10 +397,24 @@ export default {
},
},
async mounted() {
await this.loadQuantityUnitOptions();
await Promise.all([this.load(), this.loadCarrierOptions()]);
this.applySelfOperatedCarrierDefaults();
},
methods: {
loadQuantityUnitOptions() {
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
this.quantityUnitLoading = true;
this.quantityUnitLoadPromise = loadMeasurementUnitOptions()
.then(options => {
this.quantityUnitOptions = options.map(item => item.value);
return this.quantityUnitOptions;
})
.finally(() => {
this.quantityUnitLoading = false;
});
return this.quantityUnitLoadPromise;
},
async load() {
if (!this.id) return;
this.loading = true;
@@ -412,8 +434,10 @@ export default {
},
async loadContractCurrency() {
const directCurrency = this.master?.settlementCurrency || this.master?.settlementCurrencyCode || this.master?.currency;
const previousCurrency = this.master?.settlementCurrency;
if (!this.master?.projectId || !this.master?.contractId) {
this.master.settlementCurrency = this.normalizeCurrencyValue(directCurrency || 'RMB');
this.syncRouteCurrency(previousCurrency, this.master.settlementCurrency);
return;
}
try {
@@ -427,6 +451,20 @@ export default {
this.master.customerContractPartyB = '';
this.master.settlementCurrency = 'RMB';
}
this.syncRouteCurrency(previousCurrency, this.master.settlementCurrency);
},
syncRouteCurrency(_previousCurrency, nextCurrency) {
const currency = this.normalizeCurrencyValue(nextCurrency || 'RMB');
(this.routes || []).forEach(route => {
route.currency = currency;
(route.freightItems || []).forEach(item => {
if (item.priceUnit) {
item.priceUnit = remapPriceUnitCurrency(item.priceUnit, currency);
} else if (item.quantityUnit) {
item.priceUnit = resolvePriceUnitByQuantityUnit(item.quantityUnit, currency);
}
});
});
},
dispatchRouteNodes() {
const routes = Array.isArray(this.master?.routes) ? this.master.routes : [];
@@ -601,8 +639,23 @@ export default {
const prices = (group.rows || []).map(item => String(item.unitPrice ?? '').trim()).filter(Boolean);
return !prices.length || prices.some(price => price !== prices[0]) ? '' : prices[0];
},
freightGroupPriceUnit(group = {}) { return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : ''); },
freightGroupPriceUnit(group = {}) {
if (group.rows?.[0]?.priceUnit) return group.rows[0].priceUnit;
if (!group.quantityUnit) return '';
return (
resolvePriceUnitByQuantityUnit(group.quantityUnit, this.master?.settlementCurrency || 'RMB') ||
`${currencyUnit(this.master?.settlementCurrency || 'RMB')}/${group.quantityUnit}`
);
},
freightGroupAmount(group = {}) { return (group.rows || []).reduce((sum, item) => sum + this.freightAmount(item), 0); },
priceUnitOptions(route = {}) {
const unit = currencyUnit(route.currency || this.master?.settlementCurrency || 'RMB');
return [
{ label: `${unit}/吨`, value: `${unit}/吨` },
{ label: `${unit}/件`, value: `${unit}/件` },
{ label: `${unit}/方`, value: `${unit}/方` },
];
},
handleFreightGroupUnitPriceInput(group, value) {
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
const normalized = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
@@ -612,6 +665,10 @@ export default {
handleFreightGroupQuantityUnitChange(route, group, value) {
(group.rows || []).forEach(item => {
item.quantityUnit = value || '';
item.priceUnit = resolvePriceUnitByQuantityUnit(
value,
route.currency || this.master?.settlementCurrency || 'RMB'
) || item.priceUnit;
const goods = (route.goods || []).find(row => String(row.sourceIndex) === String(item.sourceIndex));
if (goods) goods.quantityUnit = value || '';
});
@@ -634,7 +691,7 @@ export default {
})),
});
},
currencyLabel(value) { return ({ CNY: '元', RMB: '元', USD: '美元', EUR: '欧元' })[value] || value || '元'; },
currencyLabel(value) { return currencyUnit(value); },
quantityNumber(value) { return Number(value || 0); },
goodsKey(goods = {}, includeUnit = true) {
return [
@@ -824,6 +881,7 @@ export default {
removeGoods(route, index) { route.goods.splice(index, 1); this.syncFreightItems(route); },
syncFreightItems(route) {
const oldItems = route.freightItems || [];
const currency = route.currency || this.master?.settlementCurrency || 'RMB';
route.freightItems = (route.goods || [])
.filter(item => item.sourceIndex !== undefined && item.sourceIndex !== '')
.map(item => {
@@ -835,7 +893,10 @@ export default {
quantity: item.dispatchQuantity || '',
quantityUnit: item.quantityUnit || '',
unitPrice: old.unitPrice || '',
priceUnit: old.priceUnit || `元/${item.quantityUnit || '吨'}`,
priceUnit:
old.priceUnit ||
resolvePriceUnitByQuantityUnit(item.quantityUnit || '吨', currency) ||
`${currencyUnit(currency)}/${item.quantityUnit || '吨'}`,
};
});
},
@@ -888,9 +949,11 @@ export default {
},
carrierOptionLabel(item) {
const duplicateCount = this.carrierOptions.filter(option => option.carrierName === item.carrierName).length;
return duplicateCount > 1
? `${item.carrierName}(${item.contractName || item.contractId})`
: item.carrierName;
if (duplicateCount <= 1) {
return item.carrierName;
}
const contractLabel = formatContractOptionLabel(item);
return contractLabel ? `${item.carrierName}(${contractLabel})` : item.carrierName;
},
driverOptionLabel(item) { return item.driverName || item.name || ''; },
driverOptionKey(item) { return item.id || item.driverId || this.driverOptionLabel(item) || item.mobile; },
@@ -1197,7 +1260,7 @@ export default {
.route-overview__line { flex: 0 1 56px; min-width: 24px; height: 32px; margin-top: 0; border-bottom: 6px solid #e4e7ed; }
.segment-header { min-height: 46px; padding: 0 20px; border-bottom: 1px solid #eff1f7; font-weight: 600; em { color: #409eff; font-style: normal; } }
.segment-content { padding: 16px 24px; }
.dispatch-form { :deep(.el-form-item) { margin-bottom: 14px; } :deep(.el-input), :deep(.el-select), :deep(.el-date-editor), :deep(.el-input-number) { width: 100%; } :deep(.el-input-group__append .el-select) { width: auto; } }
.dispatch-form { :deep(.el-form-item) { margin-bottom: 24px; } :deep(.el-input), :deep(.el-select), :deep(.el-date-editor), :deep(.el-input-number) { width: 100%; } :deep(.el-input-group__append .el-select) { width: auto; } }
/* 短标签按自身宽度,避免被「预计发货日期」等长标签把左侧撑开 */
.dispatch-form__compact-label {
:deep(.el-form-item__label) {
@@ -27,7 +27,7 @@
><el-option
v-for="contract in contracts"
:key="contract.id"
:label="contract.contractName"
:label="formatContractOptionLabel(contract)"
:value="contract.id" /></el-select></el-form-item></el-col
><el-col :span="6"
><el-form-item label="总单号" required
@@ -261,11 +261,22 @@
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="文件名" min-width="240" align="center" show-overflow-tooltip>
<el-table-column
label="文件名"
min-width="240"
align="center"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }}
</el-link>
<span
class="attachment-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" align="center">
@@ -589,6 +600,7 @@ import { getList as getCargoTypeList } from '@/api/base/cargo-type';
import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
import { exportBlob, importBlob } from '@/api/common';
import { formatContractOptionLabel } from '@/utils/contract';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { OfficeBuilding, Search } from '@element-plus/icons-vue';
import { ElImageViewer } from 'element-plus';
@@ -597,10 +609,10 @@ import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from
import '@open-file-viewer/core/style.css';
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
import { mapGetters } from 'vuex';
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
const quantityUnitOptions = ['吨', '件', '方'];
const attachmentViewerPlugins = [
imagePlugin(),
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
@@ -703,10 +715,11 @@ export default {
'zip',
],
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
quantityUnitOptions,
quantityUnitOptions: [],
quantityUnitLoading: false,
form: {
routes: [{ segmentNo: '段1', transportType: '' }],
goods: [{ quantityUnit: quantityUnitOptions[0] }],
goods: [{ quantityUnit: '吨' }],
attachmentsJson: '[]',
transportOrganizationType: '多式联运',
},
@@ -773,6 +786,7 @@ export default {
async mounted() {
this.loading = true;
try {
await this.loadQuantityUnitOptions();
await Promise.all([this.loadProjects(), this.loadCargoTypeOptions(), this.loadRegionOptions()]);
if (this.id) {
await this.loadDetail(this.id);
@@ -782,6 +796,23 @@ export default {
}
},
methods: {
formatContractOptionLabel,
defaultQuantityUnit() {
return this.quantityUnitOptions[0] || '吨';
},
loadQuantityUnitOptions() {
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
this.quantityUnitLoading = true;
this.quantityUnitLoadPromise = loadMeasurementUnitOptions()
.then(options => {
this.quantityUnitOptions = options.map(item => item.value);
return this.quantityUnitOptions;
})
.finally(() => {
this.quantityUnitLoading = false;
});
return this.quantityUnitLoadPromise;
},
responseData(response) {
return response?.data?.data || response?.data || response || {};
},
@@ -958,7 +989,7 @@ export default {
finalTransportType: hasFinalSegment
? finalSegment.transportType
: detail.finalTransportType || '',
goods: detail.goods || [{ quantityUnit: quantityUnitOptions[0] }],
goods: detail.goods || [{ quantityUnit: this.defaultQuantityUnit() }],
attachmentsJson: detail.attachmentsJson || '[]',
transportOrganizationType: '多式联运',
};
@@ -1659,7 +1690,7 @@ export default {
'',
cargoTypePath,
quantity: row.quantity || '',
quantityUnit: row.quantityUnit || quantityUnitOptions[0],
quantityUnit: row.quantityUnit || this.defaultQuantityUnit(),
packageType: row.packageType || row.package || '',
brand: row.brand || '',
specification: row.specification || row.spec || '',
@@ -1785,7 +1816,7 @@ export default {
},
addGoods(index) {
this.form.goods.splice(index + 1, 0, {
quantityUnit: quantityUnitOptions[0],
quantityUnit: this.defaultQuantityUnit(),
materialCode: '',
deviceCode: '',
remark: '',
@@ -2047,7 +2078,7 @@ export default {
font-size: 16px;
}
.el-form-item {
margin-bottom: 14px;
margin-bottom: 24px;
}
}
.master-editor__quantity-field {
@@ -0,0 +1,305 @@
<template>
<div v-if="groups.length" class="punch-record-photos">
<section v-for="group in groups" :key="group.type" class="punch-record-photos__group">
<div class="punch-record-photos__head">
<span class="punch-record-photos__title">{{ group.type }}</span>
<span v-if="editable" class="punch-record-photos__count">{{ countText(group) }}</span>
</div>
<div
class="punch-record-photos__grid"
:class="{ 'punch-record-photos__grid--edit': editable }"
>
<div
v-for="(item, groupIndex) in group.items"
:key="`${item.photo.url}-${item.index}`"
class="punch-record-photos__item"
@click="handlePreview(item.index)"
>
<el-image
:src="item.photo.url"
:preview-src-list="nativePreview ? previewUrls : []"
:initial-index="nativePreview ? item.index : 0"
:preview-teleported="nativePreview"
:lazy="lazy"
fit="cover"
/>
<!-- 主图 = 该类型的第一张,按上传顺序取;没有独立字段,也不可切换 -->
<div v-if="groupIndex === 0" class="punch-record-photos__badge">主图</div>
<el-link
v-if="editable"
class="punch-record-photos__remove"
type="danger"
:underline="false"
@click.stop="$emit('remove', item.index)"
>删除</el-link
>
</div>
<el-upload
v-if="editable && !group.full"
class="punch-record-photos__upload"
:action="uploadAction"
name="file"
:headers="uploadHeaders"
:show-file-list="false"
:before-upload="file => handleBeforeUpload(file, group.type)"
:on-success="res => $emit('upload-success', res, group.type)"
:on-error="error => $emit('upload-error', error, group.type)"
>
<div class="punch-record-photos__add">+ 添加</div>
</el-upload>
<div v-else-if="editable" class="punch-record-photos__add punch-record-photos__add--full">
已满 {{ maxPerType }} 张
</div>
</div>
</section>
</div>
</template>
<script setup>
import { computed } from 'vue';
// 打卡照片是每个打卡节点必有的一个照片类型,不由过程配置的凭证类型决定;
// 它与各凭证类型各自最多 9 张,同一类型下第一张即界面上的「主图」。
const PUNCH_PHOTO_TYPE = '打卡照片';
// 照片没带类型时的兜底名称,与后端落库时的兜底口径一致
const PHOTO_TYPE_FALLBACK = '凭证';
const MAX_PHOTOS_PER_TYPE = 9;
const props = defineProps({
// 扁平照片数组:管理端回显用 voucherType,司机端回显与旧数据用 type
photos: {
type: Array,
default: () => [],
},
// 恒定展示的照片类型:「打卡照片」+ 该节点过程配置勾选的凭证类型。
// 留空则只按照片自身带的类型分组(只读回显用)。
groupTypes: {
type: Array,
default: () => [],
},
// 编辑态:每个类型自带「添加」格与删除链接,并显示已选/上限
editable: {
type: Boolean,
default: false,
},
// 每个类型的张数上限;0 表示不限制(在途打卡只有一个照片字段,张数由「只保留第一张」约束)
maxPerType: {
type: Number,
default: MAX_PHOTOS_PER_TYPE,
},
// 编辑态用 el-image 自带预览;只读态不发预览事件,把点击交给调用方开自己的看图器
nativePreview: {
type: Boolean,
default: false,
},
lazy: {
type: Boolean,
default: false,
},
uploadAction: {
type: String,
default: '',
},
uploadHeaders: {
type: Object,
default: () => ({}),
},
// 返回 false 即中止本次上传;类型、体积、张数校验都在调用方
beforeUpload: {
type: Function,
default: null,
},
});
const emit = defineEmits(['preview', 'remove', 'upload-success', 'upload-error']);
const photoType = photo =>
String(photo?.voucherType || photo?.type || '').trim() || PHOTO_TYPE_FALLBACK;
const groups = computed(() => {
const photos = Array.isArray(props.photos) ? props.photos : [];
const names = [];
const buckets = new Map();
const ensure = name => {
if (!buckets.has(name)) {
buckets.set(name, []);
names.push(name);
}
return buckets.get(name);
};
// 恒定展示的类型先占位:一张照片都没有的类型也要出现,否则编辑态下没有添加入口
props.groupTypes.forEach(type => {
const name = String(type || '').trim();
if (name) ensure(name);
});
// 按上传顺序入组,组间顺序即类型首次出现的顺序,不重排照片
photos.forEach((photo, index) => {
if (!photo?.url) return;
ensure(photoType(photo)).push({ photo, index });
});
const list = names.map(type => {
const items = buckets.get(type);
return {
type,
items,
full: props.editable && props.maxPerType > 0 && items.length >= props.maxPerType,
};
});
// 编辑态至少要有一个分组,否则记录里还没有照片时就没有添加入口
if (props.editable && !list.length) {
return [{ type: props.groupTypes[0] || PUNCH_PHOTO_TYPE, items: [], full: false }];
}
return list;
});
// 预览列表按原始下标对齐,删除/分组都不改变点击某张时打开的是哪一张
const previewUrls = computed(() =>
(Array.isArray(props.photos) ? props.photos : []).map(photo => photo?.url || '')
);
const handlePreview = index => {
if (props.nativePreview) return;
emit('preview', index);
};
const handleBeforeUpload = (file, type) => {
if (typeof props.beforeUpload !== 'function') return true;
return props.beforeUpload(file, type);
};
const countText = group =>
props.maxPerType > 0 ? `${group.items.length}/${props.maxPerType}` : String(group.items.length);
</script>
<style lang="scss" scoped>
.punch-record-photos {
// 弹窗里本组件挂在 el-form-item__content 下,那是个 flex 容器:
// 不给宽度就会收缩成内容宽,里面的网格跟着塌成一小块
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 4px;
&__group {
width: 100%;
display: flex;
flex-direction: column;
gap: 6px;
}
&__head {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
line-height: 20px;
}
&__title {
font-weight: 600;
color: #303133;
word-break: break-all;
}
&__count {
font-size: 12px;
color: #909399;
}
// 只读回显:撑满容器宽度的五列网格
&__grid {
width: 100%;
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 10px;
}
// 编辑态沿用弹窗原本的固定 96px 方图 + 自动换行:尺寸不随容器宽窄变化
&__grid--edit {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 10px;
}
&__item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
cursor: pointer;
.el-image {
display: block;
width: 100%;
aspect-ratio: 1;
border: 1px solid #eff1f7;
border-radius: 4px;
overflow: hidden;
background: #f5f7fa;
}
}
&__grid--edit &__item {
width: 96px;
.el-image {
width: 96px;
height: 96px;
aspect-ratio: auto;
}
}
&__badge {
position: absolute;
top: 0;
left: 0;
padding: 0 6px;
border-radius: 4px 0 4px 0;
background: rgba(64, 158, 255, 0.9);
font-size: 12px;
line-height: 18px;
color: #fff;
}
&__remove {
align-self: center;
font-size: 12px;
}
&__upload {
width: 100%;
}
&__grid--edit &__upload {
width: 96px;
}
&__add {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 100%;
aspect-ratio: 1;
border: 1px dashed #dcdfe6;
border-radius: 4px;
font-size: 13px;
color: #909399;
cursor: pointer;
}
&__grid--edit &__add {
width: 96px;
height: 96px;
aspect-ratio: auto;
}
&__add--full {
color: #c0c4cc;
cursor: not-allowed;
}
}
</style>
@@ -113,7 +113,7 @@
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="item.contractName"
:label="formatContractOptionLabel(item)"
:value="item.id"
/>
</el-select>
@@ -454,11 +454,20 @@
>
<el-table-column type="selection" width="55" />
<el-table-column type="index" label="序号" width="70" />
<el-table-column label="文件名" min-width="220"
<el-table-column
label="文件名"
min-width="220"
show-overflow-tooltip
class-name="is-ellipsis-link"
><template #default="{ row }"
><el-link type="primary" @click="previewAttachment(row)">{{
attachmentName(row)
}}</el-link></template
><span
class="attachment-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"
@@ -721,11 +730,21 @@
</div>
<el-table :data="detailAttachmentRows" border empty-text="暂无附件">
<el-table-column type="index" label="序号" width="70" />
<el-table-column label="文件名" min-width="220">
<el-table-column
label="文件名"
min-width="220"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<el-link type="primary" @click="previewAttachment(row)">{{
attachmentName(row)
}}</el-link>
<span
class="attachment-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">
@@ -1098,6 +1117,8 @@ import {
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { getDictionary } from '@/api/system/dictbiz';
import { packageOptions } from '@/option/business/common';
import { formatContractOptionLabel } from '@/utils/contract';
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate';
@@ -1278,7 +1299,8 @@ export default {
cargoTypeRequest: null,
transportCargoRows: [],
packageOptions,
quantityUnitOptions: ['吨', '千克', '立方米', '立方', '公里', '单', '件', '车', '箱', '托盘'],
quantityUnitOptions: [],
quantityUnitLoading: false,
feeUnitOptions: [],
currencyOptions: [],
currencyLoading: false,
@@ -1525,11 +1547,26 @@ export default {
created() {
this.loadProjectOptions();
this.ensureCargoTypeOptions();
this.loadQuantityUnitOptions();
this.loadFeeUnitOptions();
this.loadCurrencyOptions();
if (this.isStandaloneFormPage) this.$nextTick(() => this.initStandaloneFormPage());
},
methods: {
formatContractOptionLabel,
loadQuantityUnitOptions() {
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
this.quantityUnitLoading = true;
this.quantityUnitLoadPromise = loadMeasurementUnitOptions()
.then(options => {
this.quantityUnitOptions = options.map(item => item.value);
return this.quantityUnitOptions;
})
.finally(() => {
this.quantityUnitLoading = false;
});
return this.quantityUnitLoadPromise;
},
buildOption(option) {
const next = { ...option, column: (option.column || []).map(column => ({ ...column })) };
const tableOrder = this.config.tableColumnOrder || [];
@@ -3506,7 +3543,7 @@ export default {
}
.shipping-template-page__detail-form :deep(.el-form-item) {
min-width: 0;
margin-bottom: 16px;
margin-bottom: 24px;
}
.shipping-template-page__detail-form :deep(.el-form-item__label) {
color: #909399;
@@ -3695,7 +3732,7 @@ export default {
}
:global(.shipping-template-form-page-dialog .avue-group .el-form-item) {
margin-bottom: 16px;
margin-bottom: 24px;
}
:global(.shipping-template-form-page-dialog .avue-form__menu) {
@@ -149,7 +149,7 @@
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="item.contractName"
:label="formatContractOptionLabel(item)"
:value="item.id"
/>
</el-select>
@@ -492,11 +492,22 @@
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="文件名" min-width="200" align="left" show-overflow-tooltip>
<el-table-column
label="文件名"
min-width="200"
align="left"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<el-link type="primary" @click="previewAttachment(row)">
{{ row.originalName || row.name }}
</el-link>
<span
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(row)"
@keydown.enter="previewAttachment(row)"
@keydown.space.prevent="previewAttachment(row)"
>{{ row.originalName || row.name }}</span>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220" align="center">
@@ -673,14 +684,16 @@
<div class="transport-plan-page__transport-plan-detail-field is-attachments">
<span>附件</span>
<div class="transport-plan-page__transport-plan-detail-attachments">
<el-link
<span
v-for="file in attachmentRows"
:key="file.url || file.name || file.originalName"
type="primary"
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(file)"
>
{{ attachmentName(file) }}
</el-link>
@keydown.enter="previewAttachment(file)"
@keydown.space.prevent="previewAttachment(file)"
>{{ attachmentName(file) }}</span>
<span v-if="!attachmentRows.length">-</span>
</div>
</div>
@@ -855,11 +868,21 @@
</div>
<el-table :data="attachmentRows" empty-text="暂无附件">
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="文件名" min-width="260">
<el-table-column
label="文件名"
min-width="260"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }}
</el-link>
<span
class="attachment-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
@@ -980,7 +1003,7 @@
<el-option
v-for="item in transportPlanImportContractOptions"
:key="item.id"
:label="item.contractName"
:label="formatContractOptionLabel(item)"
:value="item.id"
/>
</el-select>
@@ -1744,11 +1767,11 @@
><el-select
:model-value="dispatchFreightGroupPriceUnit(group)"
class="transport-plan-page__dispatch-unit-select"
placeholder="元/吨"
:placeholder="defaultPriceUnit"
@change="value => handleDispatchFreightGroupPriceUnitChange(group, value)"
@visible-change="visible => visible && loadTaskFeeUnitOptions()"
><el-option
v-for="item in taskFeeUnitOptions"
v-for="item in taskFeeUnitSelectOptions"
:key="item.value"
:label="item.label"
:value="item.value" /></el-select
@@ -1969,17 +1992,17 @@
<el-select
v-model="dispatchItemForm.priceUnit"
class="transport-plan-page__dispatch-unit-select"
placeholder="元/吨"
:placeholder="defaultPriceUnit"
:loading="taskFeeUnitLoading"
@visible-change="visible => visible && loadTaskFeeUnitOptions()"
>
<el-option
v-if="!taskFeeUnitOptions.some(item => item.value === '元/吨')"
label="元/吨"
value="元/吨"
v-if="!taskFeeUnitSelectOptions.some(item => item.value === defaultPriceUnit)"
:label="defaultPriceUnit"
:value="defaultPriceUnit"
/>
<el-option
v-for="item in taskFeeUnitOptions"
v-for="item in taskFeeUnitSelectOptions"
:key="item.value"
:label="item.label"
:value="item.value"
@@ -2201,11 +2224,22 @@
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="文件名" min-width="200" align="center" show-overflow-tooltip>
<el-table-column
label="文件名"
min-width="200"
align="center"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<el-link type="primary" @click="previewAttachment(row, dispatchItemAttachmentRows)">
{{ attachmentName(row) }}
</el-link>
<span
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(row, dispatchItemAttachmentRows)"
@keydown.enter="previewAttachment(row, dispatchItemAttachmentRows)"
@keydown.space.prevent="previewAttachment(row, dispatchItemAttachmentRows)"
>{{ attachmentName(row) }}</span>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220" align="center">
@@ -2741,7 +2775,15 @@ import { addressTypeOptions } from '@/option/base/common-address';
import { packageOptions } from '@/option/business/common';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken, isAdministrator } from '@/utils/auth';
import {
currencyUnit,
mapFeeUnitOptions,
remapPriceUnitCurrency,
resolvePriceUnitByQuantityUnit,
} from '@/utils/currency';
import { formatContractOptionLabel } from '@/utils/contract';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate';
@@ -3206,7 +3248,7 @@ export default {
billingCargoTypeLoading: false,
billingCargoTypeRequest: null,
packageOptions,
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
quantityUnitOptions: [],
attachmentFileTypes: [
'pdf',
'bmp',
@@ -3245,6 +3287,7 @@ export default {
} else {
this.consumeTemplateCreatePayload();
}
this.loadTaskQuantityUnitOptions();
},
computed: {
...mapGetters(['permission', 'userInfo']),
@@ -3609,6 +3652,22 @@ export default {
const value = this.dispatchItemForm.freightCurrency || 'RMB';
return this.currencyRemark(value);
},
taskFeeUnitSelectOptions() {
const currency =
this.dispatchItemForm.freightCurrency ||
this.dispatchContractCurrency ||
this.form.settlementCurrency ||
'RMB';
return mapFeeUnitOptions(this.taskFeeUnitOptions, currency);
},
defaultPriceUnit() {
return `${currencyUnit(
this.dispatchItemForm.freightCurrency ||
this.dispatchContractCurrency ||
this.form.settlementCurrency ||
'RMB'
)}/吨`;
},
isAdmin() {
return isAdministrator(this.userInfo);
},
@@ -3719,6 +3778,7 @@ export default {
},
},
methods: {
formatContractOptionLabel,
initStandaloneFormPage() {
if (!this.isStandaloneFormPage) return;
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
@@ -4307,8 +4367,6 @@ export default {
['contractName', '客户合同'],
['planName', '计划名称'],
['transportType', '运输方式'],
['planStartDate', '计划开始日期'],
['planEndDate', '计划结束日期'],
['departureAddress', '发货地址'],
['arrivalAddress', '收货地址'],
];
@@ -4804,19 +4862,49 @@ export default {
contract.customerNames ||
this.form.customerName ||
'';
const nextCurrency = this.getContractCurrency(contract);
this.remapTransportPlanPriceUnits(nextCurrency);
this.$refs.crud?.validateField?.('contractName');
},
remapTransportPlanPriceUnits(currency) {
if (this.form.priceUnit) {
this.form.priceUnit = remapPriceUnitCurrency(this.form.priceUnit, currency);
}
if (this.dispatchItemForm.priceUnit) {
this.dispatchItemForm.priceUnit = remapPriceUnitCurrency(
this.dispatchItemForm.priceUnit,
currency
);
}
this.dispatchItemForm.freightCurrency = currency;
(this.dispatchItemCargoRows || []).forEach(cargo => {
if (cargo.priceUnit) {
cargo.priceUnit = remapPriceUnitCurrency(cargo.priceUnit, currency);
}
});
(this.transportCargoRows || []).forEach(cargo => {
if (cargo.priceUnit) {
cargo.priceUnit = remapPriceUnitCurrency(cargo.priceUnit, currency);
}
});
},
loadDispatchContractCurrency(plan = {}) {
const findContract = () =>
this.contractOptions.find(item => String(item.id) === String(plan.contractId)) ||
this.contractOptions.find(item => item.contractName === plan.contractName);
const applyCurrency = contract => {
const previousCurrency = this.dispatchContractCurrency;
this.dispatchContractCurrency = this.getContractCurrency(
contract &&
(contract.settlementCurrency || contract.settlementCurrencyCode || contract.currency)
? contract
: { settlementCurrency: plan.settlementCurrency }
);
if (previousCurrency && previousCurrency !== this.dispatchContractCurrency) {
this.remapTransportPlanPriceUnits(this.dispatchContractCurrency);
} else {
this.dispatchItemForm.freightCurrency = this.dispatchContractCurrency;
}
return this.dispatchContractCurrency;
};
const current = findContract();
@@ -5076,15 +5164,40 @@ export default {
return Promise.resolve(this.taskQuantityUnitOptions);
}
this.taskQuantityUnitLoading = true;
return getDictionary({ code: 'quantity_unit' })
.then(res => {
this.taskQuantityUnitOptions = this.normalizeDictOptions(res.data.data || []);
return loadMeasurementUnitOptions()
.then(options => {
this.taskQuantityUnitOptions = options;
this.quantityUnitOptions = options.map(item => item.value);
this.ensureQuantityUnitOptions([
this.dispatchItemForm?.quantityUnit,
...(this.dispatchItemCargoRows || []).map(row => row.quantityUnit),
...(this.transportCargoRows || []).map(row => row.quantityUnit),
]);
return this.taskQuantityUnitOptions;
})
.finally(() => {
this.taskQuantityUnitLoading = false;
});
},
ensureQuantityUnitOptions(units = []) {
const pending = [...new Set(
(Array.isArray(units) ? units : [units])
.map(item => String(item || '').trim())
.filter(Boolean)
)];
if (!pending.length) return;
pending.forEach(unit => {
if (!this.quantityUnitOptions.includes(unit)) {
this.quantityUnitOptions = [...this.quantityUnitOptions, unit];
}
if (!this.taskQuantityUnitOptions.some(item => item.value === unit)) {
this.taskQuantityUnitOptions = [
...this.taskQuantityUnitOptions,
{ label: unit, value: unit },
];
}
});
},
loadTaskFeeUnitOptions() {
if (this.taskFeeUnitOptions.length || this.taskFeeUnitLoading) {
return Promise.resolve(this.taskFeeUnitOptions);
@@ -5146,6 +5259,9 @@ export default {
});
},
normalizeTransportCargoRow(row = {}) {
const quantityUnit = String(
row.quantityUnit || row.goodsQuantityUnit || row.unit || ''
).trim();
const cargo = {
...defaultTransportCargo(),
...row,
@@ -5162,10 +5278,14 @@ export default {
packageType: row.packageType || row.package || '',
specification: row.specification || row.spec || '',
freightAmount: row.freightAmount ?? row.amount ?? row.totalAmount ?? '',
quantityUnit,
};
['quantity', 'mileage', 'unitPrice', 'freightAmount'].forEach(prop => {
if (Number(cargo[prop]) === -1) cargo[prop] = '';
});
if (quantityUnit) {
this.ensureQuantityUnitOptions(quantityUnit);
}
return cargo;
},
hasMultipleConfiguredGoods(source = {}, rows = []) {
@@ -5183,7 +5303,7 @@ export default {
},
addTransportCargoRow(index = -1, row = {}) {
const nextRow = this.normalizeTransportCargoRow({
priceUnit: this.form.priceUnit || '元/吨',
priceUnit: this.form.priceUnit || this.defaultPriceUnit,
...row,
});
if (
@@ -7594,6 +7714,9 @@ export default {
...baseRow,
...(index >= 0 ? row : {}),
};
this.dispatchItemForm.quantityUnit =
this.dispatchItemForm.quantityUnit || defaultUnit || TRANSPORT_PLAN_QUANTITY_UNIT;
this.ensureQuantityUnitOptions(this.dispatchItemForm.quantityUnit);
this.dispatchItemForm.otherFeeTotal = this.normalizeDispatchFeeValue(
this.dispatchItemForm.otherFeeTotal
);
@@ -7603,7 +7726,11 @@ export default {
});
const goodsRows = this.parseJsonArray(this.dispatchItemForm.goodsJson);
this.dispatchItemCargoRows = (goodsRows.length ? goodsRows : [this.dispatchItemForm]).map(
item => this.normalizeTransportCargoRow(item)
item =>
this.normalizeTransportCargoRow({
...item,
quantityUnit: item.quantityUnit || this.dispatchItemForm.quantityUnit || defaultUnit,
})
);
const hasMultiplePlanGoods = this.dispatchPlanGoodsRows.length > 1;
const shouldAutoFillFullGoods =
@@ -7613,11 +7740,18 @@ export default {
const unscheduledGoodsRows = this.getDispatchUnscheduledGoodsRows();
this.dispatchItemForm.taskEntryMode = 'full';
this.dispatchItemForm.quantity = '';
this.dispatchItemCargoRows = unscheduledGoodsRows.length
? unscheduledGoodsRows
: this.dispatchPlanGoodsRows.map(goods =>
this.normalizeTransportCargoRow({ ...goods, quantity: '' })
);
this.dispatchItemCargoRows = (
unscheduledGoodsRows.length
? unscheduledGoodsRows
: this.dispatchPlanGoodsRows.map(goods =>
this.normalizeTransportCargoRow({ ...goods, quantity: '' })
)
).map(goods =>
this.normalizeTransportCargoRow({
...goods,
quantityUnit: goods.quantityUnit || defaultUnit || TRANSPORT_PLAN_QUANTITY_UNIT,
})
);
}
if (!this.dispatchItemCargoRows.length) {
this.dispatchItemCargoRows = [this.normalizeTransportCargoRow()];
@@ -7632,7 +7766,10 @@ export default {
this.dispatchItemCargoRows = this.dispatchItemCargoRows.map((cargo, itemIndex) => ({
...cargo,
unitPrice: cargo.unitPrice || freightInfo.freightItems[itemIndex]?.unitPrice || '',
priceUnit: cargo.priceUnit || freightInfo.freightItems[itemIndex]?.priceUnit || '元/吨',
priceUnit:
cargo.priceUnit ||
freightInfo.freightItems[itemIndex]?.priceUnit ||
this.defaultPriceUnit,
freightAmount:
cargo.freightAmount || freightInfo.freightItems[itemIndex]?.freightAmount || '',
}));
@@ -7881,7 +8018,7 @@ export default {
return value;
},
addDispatchCargoRow(index = -1) {
const nextRow = this.normalizeTransportCargoRow({ priceUnit: '元/吨' });
const nextRow = this.normalizeTransportCargoRow({ priceUnit: this.defaultPriceUnit });
if (index > -1) this.dispatchItemCargoRows.splice(index + 1, 0, nextRow);
else this.dispatchItemCargoRows.push(nextRow);
},
@@ -7925,7 +8062,14 @@ export default {
return prices[0];
},
dispatchFreightGroupPriceUnit(group = {}) {
return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : '');
if (group.rows?.[0]?.priceUnit) return group.rows[0].priceUnit;
if (!group.quantityUnit) return '';
return (
resolvePriceUnitByQuantityUnit(
group.quantityUnit,
this.dispatchItemForm.freightCurrency || this.dispatchContractCurrency || 'RMB'
) || `${currencyUnit(this.dispatchContractCurrency || 'RMB')}/${group.quantityUnit}`
);
},
dispatchFreightGroupAmount(group = {}) {
const amount = (group.rows || []).reduce(
@@ -8309,7 +8453,10 @@ export default {
this.dispatchItemForm.cargoName ||
'',
quantity: quantityText,
quantityUnit: firstGoods.quantityUnit || this.dispatchItemForm.quantityUnit || '',
quantityUnit:
firstGoods.quantityUnit ||
this.dispatchItemForm.quantityUnit ||
TRANSPORT_PLAN_QUANTITY_UNIT,
unitPrice: firstGoods.unitPrice || '',
priceUnit: firstGoods.priceUnit || '',
freightTotal: this.dispatchItemFreightTotal,
@@ -8324,7 +8471,7 @@ export default {
unitPrice: cargo.unitPrice || '',
priceUnit: cargo.priceUnit || '',
quantity: cargo.quantity || '',
quantityUnit: cargo.quantityUnit || '',
quantityUnit: cargo.quantityUnit || firstGoods.quantityUnit || TRANSPORT_PLAN_QUANTITY_UNIT,
freightAmount: this.dispatchCargoFreightAmount(cargo),
})),
}),
@@ -8413,26 +8560,40 @@ export default {
return {
id: this.dispatchRow.id,
mode,
waybills: rows.map(row => ({
...row,
taskEntryMode: row.taskEntryMode || 'simple',
taskRemark: row.remark || '',
estimatedStartTime: this.formatDispatchDate(row.estimatedStartTime),
estimatedEndTime: this.formatDispatchDate(row.estimatedEndTime),
goodsJson:
row.goodsJson ||
JSON.stringify([
waybills: rows.map(row => {
const quantityUnit = this.getDispatchQuantityUnit(row);
let goodsJson = row.goodsJson;
const goodsRows = this.parseJsonArray(goodsJson);
if (goodsRows.length) {
goodsJson = JSON.stringify(
goodsRows.map(goods => ({
...goods,
quantityUnit: goods.quantityUnit || quantityUnit,
}))
);
} else {
goodsJson = JSON.stringify([
{
cargoName: row.cargoName,
cargoType: row.cargoType,
quantity: row.quantity,
quantityUnit: row.quantityUnit,
quantityUnit,
specification: row.specification,
model: row.model,
packageType: row.packageType,
},
]),
})),
]);
}
return {
...row,
quantityUnit,
taskEntryMode: row.taskEntryMode || 'simple',
taskRemark: row.remark || '',
estimatedStartTime: this.formatDispatchDate(row.estimatedStartTime),
estimatedEndTime: this.formatDispatchDate(row.estimatedEndTime),
goodsJson,
};
}),
};
},
formatDispatchDate(value) {
@@ -8680,7 +8841,7 @@ export default {
&__dispatch-shipping-form {
:deep(.el-form-item) {
margin-bottom: 14px;
margin-bottom: 24px;
}
}
@@ -9871,7 +10032,7 @@ export default {
}
:global(.transport-plan-form-page-dialog .avue-group .el-form-item) {
margin-bottom: 16px;
margin-bottom: 24px;
}
:global(.transport-plan-form-page-dialog .avue-group .dialog-section-title) {
@@ -270,15 +270,22 @@
prop="mileage"
label="里程(km)"
width="100"
/><el-table-column prop="unitPrice" label="单价" width="90" /><el-table-column
prop="freight"
label="运费"
width="100"
/><el-table-column prop="otherFeeTotal" label="其他费用合计" width="120" /><el-table-column
prop="freightTotal"
label="运费合计"
width="100"
/><el-table-column
>
<template #default="{ row }">{{ formatOptionalNumber(row.mileage) }}</template>
</el-table-column>
<el-table-column prop="unitPrice" label="单价" width="90">
<template #default="{ row }">{{ formatOptionalNumber(row.unitPrice) }}</template>
</el-table-column>
<el-table-column prop="freight" label="运费" width="100">
<template #default="{ row }">{{ formatOptionalNumber(row.freight) }}</template>
</el-table-column>
<el-table-column prop="otherFeeTotal" label="其他费用合计" width="120">
<template #default="{ row }">{{ formatOptionalNumber(row.otherFeeTotal) }}</template>
</el-table-column>
<el-table-column prop="freightTotal" label="运费合计" width="100">
<template #default="{ row }">{{ formatOptionalNumber(row.freightTotal) }}</template>
</el-table-column>
<el-table-column
prop="actualStartDate"
label="实际发货时间"
width="160"
@@ -374,7 +381,7 @@
<el-option
v-for="item in contracts"
:key="item.id"
:label="item.contractName"
:label="formatContractOptionLabel(item)"
:value="item.id"
/>
</el-select>
@@ -610,6 +617,7 @@ import { getList as getProjectList } from '@/api/business/project-apply';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { getDictionary } from '@/api/system/dictbiz';
import * as api from '@/api/business/waybill-manage';
import { formatContractOptionLabel } from '@/utils/contract';
import { downloadXls } from '@/utils/util';
import SectionCard from '@/components/section-card/main.vue';
@@ -1079,19 +1087,40 @@ const transportTypeLabel = value => {
);
return matched?.dictValue || matched?.label || named || text || '-';
};
const buildBatchQueryParams = () => ({
batchNo: query.batchNo || undefined,
carrierId: query.carrierId || undefined,
createUser: query.createUser || undefined,
createTimeStart: query.createTimeRange?.[0] || undefined,
createTimeEnd: query.createTimeRange?.[1] || undefined,
current: page.current,
size: page.size,
});
const loadListOptions = async () => {
const res = await api.getImportOptions?.();
const data = res?.data?.data || {};
queryCarriers.value = data.carriers || [];
creators.value = data.creators || [];
};
const loadBatches = async () => {
const res = await api.getImportBatches({ ...query, current: page.current, size: page.size });
const res = await api.getImportBatches(buildBatchQueryParams());
batches.value = res.data?.data?.records || [];
page.total = res.data?.data?.total || 0;
};
onMounted(() => {
if (props.standalone) loadBatches();
if (props.standalone) {
loadListOptions();
loadBatches();
}
if (props.createPage) openCreatePage();
});
watch(
() => props.modelValue,
value => {
if (value) loadBatches();
if (value) {
loadListOptions();
loadBatches();
}
}
);
const resetQuery = () => {
@@ -1106,7 +1135,7 @@ const loadDetails = async () => {
current: detailPage.current,
size: detailPage.size,
});
details.value = res.data?.data?.records || [];
details.value = (res.data?.data?.records || []).map(normalizeDetailRow);
detailPage.total = res.data?.data?.total || 0;
};
const openCreate = () => prepareCreateForm(true);
@@ -1155,7 +1184,7 @@ const loadBatchRows = async batchId => {
if (!batchId) return;
const res = await api.getImportDetails({ batchId, current: 1, size: 9999 });
rows.value = extractRecords(res).map(({ id, ...item }, index) => ({
...item,
...normalizeDetailRow(item),
_key: `${id || 'row'}-${index}`,
batchNo: item.batchNo || form.batchNo,
}));
@@ -1297,6 +1326,43 @@ const carrierChange = value => {
form.carrierName = item.carrierName || '';
if (!isSelfOperated.value) form.carrierContractId = item.carrierContractId || '';
};
const firstNotEmpty = (...values) => values.find(value => String(value ?? '').trim()) ?? '';
const OPTIONAL_NUMBER_EMPTY_SENTINELS = [-1];
const OPTIONAL_NUMBER_PROPS = ['mileage', 'unitPrice', 'freight', 'otherFeeTotal', 'freightTotal'];
const normalizeOptionalImportNumber = (value, emptySentinels = OPTIONAL_NUMBER_EMPTY_SENTINELS) => {
if (!String(value ?? '').trim()) return null;
return emptySentinels.some(sentinel => Number(value) === sentinel) ? null : value;
};
const formatOptionalNumber = value => {
const normalized = normalizeOptionalImportNumber(value);
return normalized === null || normalized === undefined ? '' : normalized;
};
const normalizeDetailRow = row => {
const next = { ...(row || {}) };
OPTIONAL_NUMBER_PROPS.forEach(prop => {
next[prop] = formatOptionalNumber(next[prop]);
});
return next;
};
const normalizeImportRow = row => ({
...row,
mileage: normalizeOptionalImportNumber(
firstNotEmpty(row.mileage, row['里程'], row['里程(km)'], row['里程(公里)'])
),
otherFeeTotal: normalizeOptionalImportNumber(
firstNotEmpty(row.otherFeeTotal, row['其他费用合计'])
),
unitPrice: normalizeOptionalImportNumber(firstNotEmpty(row.unitPrice, row['单价'])),
freight: normalizeOptionalImportNumber(firstNotEmpty(row.freight, row['运费'])),
freightTotal: normalizeOptionalImportNumber(firstNotEmpty(row.freightTotal, row['运费合计'])),
transportType: firstNotEmpty(
row.transportType,
row['运输类型'],
row['*运输类型'],
row['运输方式'],
row['*运输方式']
),
});
const fileChange = async (file, list) => {
if (!projectInfoReady.value) {
ElMessage.warning('请先选择项目、客户合同和承运信息,再导入运单明细');
@@ -1370,6 +1436,9 @@ const fileChange = async (file, list) => {
item[key] ??
'';
});
['mileage', 'unitPrice', 'freight', 'otherFeeTotal', 'freightTotal'].forEach(prop => {
row[prop] = normalizeOptionalImportNumber(row[prop]);
});
row.actualStartDate = normalizeImportDate(row.actualStartDate);
row.actualEndDate = normalizeImportDate(row.actualEndDate);
row.planStartDate = normalizeImportDate(row.planStartDate);
@@ -1618,6 +1687,7 @@ const downloadTemplate = async () => {
ElMessage.error('模板下载失败');
}
};
const isValidMobile = value => /^\d{11}$/.test(String(value || '').trim());
const validateImportRows = () => {
if (!rows.value.length) {
ElMessage.warning('请上传至少一条运单明细');
@@ -1629,6 +1699,11 @@ const validateImportRows = () => {
ElMessage.warning(`第${index + 1}行${missingField.label}不能为空`);
return false;
}
const driverPhone = String(row.driverPhone ?? '').trim();
if (driverPhone && !isValidMobile(driverPhone)) {
ElMessage.warning(`第${index + 1}行司机/船长手机号格式不正确,应为11位数字`);
return false;
}
}
return true;
};
@@ -1656,31 +1731,6 @@ const removeSelectedRows = () => {
rowSelection.value = [];
refreshDuplicateFlags();
};
const firstNotEmpty = (...values) => values.find(value => String(value ?? '').trim()) ?? '';
const normalizeOptionalImportNumber = (value, emptySentinels = []) => {
if (!String(value ?? '').trim()) return null;
return emptySentinels.some(sentinel => Number(value) === sentinel) ? null : value;
};
const normalizeImportRow = row => ({
...row,
mileage: normalizeOptionalImportNumber(
firstNotEmpty(row.mileage, row['里程'], row['里程(km)'], row['里程(公里)']),
[-1]
),
otherFeeTotal: normalizeOptionalImportNumber(
firstNotEmpty(row.otherFeeTotal, row['其他费用合计'])
),
unitPrice: normalizeOptionalImportNumber(firstNotEmpty(row.unitPrice, row['单价'])),
freight: normalizeOptionalImportNumber(firstNotEmpty(row.freight, row['运费'])),
freightTotal: normalizeOptionalImportNumber(firstNotEmpty(row.freightTotal, row['运费合计'])),
transportType: firstNotEmpty(
row.transportType,
row['运输类型'],
row['*运输类型'],
row['运输方式'],
row['*运输方式']
),
});
const normalizeOptionalPlanId = value => {
if (value === undefined || value === null || value === '') return null;
return Number(value) === -1 || Number(value) <= 0 ? null : value;
File diff suppressed because it is too large Load Diff
+83 -16
View File
@@ -45,7 +45,7 @@
<el-form-item label="项目">
<el-input v-model="form.projectName" disabled />
</el-form-item>
<el-form-item label="所属组织">
<el-form-item label="合同负责部门">
<el-input v-model="form.organizationName" disabled />
</el-form-item>
<el-form-item label="甲方">
@@ -63,6 +63,15 @@
disabled
/>
</el-form-item>
<el-form-item label="申请日期">
<el-date-picker
v-model="form.applyDate"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
disabled
/>
</el-form-item>
<el-form-item label="合同期限">
<div class="contract-basic-section__date-range">
<el-date-picker
@@ -91,12 +100,13 @@
<template #suffix>天</template>
</el-input>
</el-form-item>
<el-form-item label="合同金额">
<el-form-item label="合同金额" prop="contractAmount">
<el-input
:model-value="contractAmountDisplay"
placeholder="请输入"
clearable
@input="handleContractAmountInput"
@blur="formatContractAmountField"
@clear="form.contractAmount = null"
/>
</el-form-item>
@@ -154,7 +164,7 @@
</el-input>
</el-form-item>
<el-form-item label="是否需要加盖法人章">
<el-select v-model="form.legalSealFlag" placeholder="请选择">
<el-select v-model="form.legalSealFlag" placeholder="请选择" clearable>
<el-option label="否" :value="0" />
<el-option label="是" :value="1" />
</el-select>
@@ -173,20 +183,30 @@
<el-input v-model="form.handlerUserName" disabled />
</el-form-item>
</div>
<el-form-item label="备注" class="contract-basic-section__remark">
<el-form-item label="合同主要内容" class="contract-basic-section__remark">
<el-input
v-model="form.mainContent"
type="textarea"
:rows="4"
maxlength="1000"
show-word-limit
placeholder="请输入合同主要内容"
/>
</el-form-item>
<el-form-item label="其他事项说明" class="contract-basic-section__remark">
<el-input
v-model="form.remark"
type="textarea"
:rows="2"
maxlength="200"
show-word-limit
placeholder="请输入备注"
placeholder="请输入其他事项说明"
/>
</el-form-item>
</section>
<contract-attachment-section
title="合同文件"
title="用印材料"
description
attachment-type
:attachment-location-options="changeAttachmentLocationOptions"
@@ -251,13 +271,20 @@
<el-button type="primary" plain @click="addPaymentRatioRow">添加</el-button>
</section>
<billing-plan-editor v-model="planDialogVisible" :value="planEditor" :index="planEditorIndex" @save="savePlan" />
<billing-plan-editor
v-model="planDialogVisible"
:value="planEditor"
:index="planEditorIndex"
:settlement-currency="form.settlementCurrency"
@save="savePlan"
/>
<contract-attachment-section
title="其它附件"
title="附件(佐证材料等)"
description
attachment-type
attachment-location="其它附件"
attachment-location="附件(佐证材料等)"
attachment-type-dict-code="contract_supporting_attachment_type"
:attachment-location-options="changeAttachmentLocationOptions"
:rows="attachments"
:preview="previewAttachment"
@@ -308,6 +335,8 @@ import * as api from '@/api/business/contract-manage';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import BillingPlanEditor from './components/billing-plan-editor.vue';
import ContractAttachmentSection, {
isContractSealMaterialLocation,
isContractSupportingAttachmentLocation,
normalizeContractFileRows,
} from './components/contract-attachment-section.vue';
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
@@ -358,6 +387,19 @@ const normalizeOptionalAmount = value => {
const number = Number(value);
return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : null;
};
const formatContractAmountInput = (value, emptyValue = null) => {
if (
value === undefined ||
value === null ||
value === '' ||
Number(value) === -1 ||
String(value) === '-1'
) {
return emptyValue;
}
const number = Number(value);
return Number.isFinite(number) && number >= 0 ? number.toFixed(2) : emptyValue;
};
const normalizeCustomPeriods = (periods = []) =>
@@ -424,6 +466,21 @@ export default {
],
rules: {
contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
contractAmount: [
{ required: true, message: '请输入合同金额', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
return callback(new Error('请输入合同金额'));
}
if (Number(value) < 0) {
return callback(new Error('合同金额不能小于0'));
}
callback();
},
trigger: 'blur',
},
],
changeReason: [{ max: 500, message: '变更原因不能超过500个字符', trigger: 'blur' }],
},
};
@@ -433,7 +490,7 @@ export default {
return this.$route.query.name || '合同变更';
},
changeAttachmentLocationOptions() {
return ['合同文件', '其它附件', '变更材料'];
return ['用印材料', '附件(佐证材料等)', '变更材料'];
},
contractAmountDisplay() {
const value = this.form.contractAmount;
@@ -498,6 +555,9 @@ export default {
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
this.form.contractAmount = normalized === '' ? null : normalized;
},
formatContractAmountField() {
this.form.contractAmount = formatContractAmountInput(this.form.contractAmount);
},
loadDictionaries() {
Promise.all([
getSystemDictionary({ code: 'currency_type' }),
@@ -540,7 +600,7 @@ export default {
copyCount: normalizeOptionalPositiveInteger(data.copyCount),
invoiceCycle: normalizeOptionalPositiveInteger(data.invoiceCycle),
paymentDays: normalizeOptionalPositiveInteger(data.paymentDays),
contractAmount: normalizeOptionalAmount(data.contractAmount),
contractAmount: formatContractAmountInput(data.contractAmount),
templateFlag: data.templateFlag ?? 0,
electronicSealFlag: data.electronicSealFlag ?? 0,
archiveStatus: data.archiveStatus || '未归档',
@@ -601,7 +661,7 @@ export default {
return [];
}
},
// 审核通过后上一次的变更材料归集到「其它附件」,按文件地址/文件名去重,避免重复展示
// 审核通过后上一次的变更材料归集到「附件(佐证材料等)」,按文件地址/文件名去重,避免重复展示
mergeChangeAttachments(attachments = [], changeAttachments = []) {
const key = item => this.attachmentUrl(item) || this.attachmentName(item);
const exists = new Set(attachments.map(key));
@@ -655,11 +715,11 @@ export default {
previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; },
handleAttachmentUploadToLocation({ location, rows = [] }) {
if (!rows.length) return;
if (location === '合同文件') {
if (isContractSealMaterialLocation(location)) {
this.contractFileRows = [...(this.contractFileRows || []), ...rows];
return;
}
if (location === '其它附件') {
if (isContractSupportingAttachmentLocation(location)) {
this.attachments = [...(this.attachments || []), ...rows];
return;
}
@@ -692,11 +752,18 @@ export default {
.contract-basic-section > .dialog-section-title { margin-bottom: 20px; color: #303133; }
.contract-basic-section__grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: 48px; }
.contract-basic-section__change-type { grid-column: 1 / -1; }
.contract-basic-section__remark { width: 100%; margin-bottom: 0 !important; }
.contract-basic-section__remark {
width: 100%;
margin-bottom: 24px !important;
&:last-child {
margin-bottom: 0 !important;
}
}
.contract-basic-section__date-range { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 8px; width: 360px; max-width: 100%; }
.contract-basic-section__date-range span { color: #606266; }
.contract-basic-section__date-range :deep(.el-date-editor) { width: auto; min-width: 0; }
.contract-basic-section :deep(.el-form-item) { align-items: center; margin-bottom: 16px; }
.contract-basic-section :deep(.el-form-item) { align-items: center; margin-bottom: 24px; }
.contract-basic-section :deep(.el-form-item__label) { flex: 0 0 96px !important; width: 96px !important; height: auto; padding-right: 12px; white-space: normal; word-break: break-all; line-height: 18px; }
.contract-basic-section :deep(.el-form-item__content) { min-width: 0; }
.contract-basic-section :deep(.el-input),
+194 -47
View File
@@ -128,7 +128,7 @@
<el-descriptions-item label="甲方">{{ detailValue('partyA') }}</el-descriptions-item>
<el-descriptions-item label="乙方">{{ detailValue('partyB') }}</el-descriptions-item>
<el-descriptions-item label="合同期限">{{ detailContractPeriod }}</el-descriptions-item>
<el-descriptions-item label="所属组织">{{
<el-descriptions-item label="合同负责部门">{{
detailValue('organizationName')
}}</el-descriptions-item>
<el-descriptions-item label="经办人">{{
@@ -140,6 +140,9 @@
<el-descriptions-item label="签订日期">{{
detailValue('signDate')
}}</el-descriptions-item>
<el-descriptions-item label="申请日期">{{
detailValue('applyDate')
}}</el-descriptions-item>
<el-descriptions-item label="合同格式">{{
detailValue('contractFormat')
}}</el-descriptions-item>
@@ -171,14 +174,17 @@
<el-descriptions-item label="是否电子章">
{{ detailElectronicSealText }}
</el-descriptions-item>
<el-descriptions-item label="备注" :span="3">{{
<el-descriptions-item label="合同主要内容" :span="3">{{
detailValue('mainContent')
}}</el-descriptions-item>
<el-descriptions-item label="其他事项说明" :span="3">{{
detailValue('remark')
}}</el-descriptions-item>
</el-descriptions>
</section>
<contract-attachment-section
title="合同文件"
title="用印材料"
description
attachment-type
readonly
@@ -294,7 +300,7 @@
</section>
<contract-attachment-section
title="其它附件"
title="附件(佐证材料等)"
description
readonly
:rows="detailAttachmentRows"
@@ -377,8 +383,14 @@
<el-form-item label="合同编号">
<el-input
v-model="form.contractNo"
:disabled="formMode === 'edit'"
placeholder="不填写,则系统自动生成"
disabled
:placeholder="
formMode === 'add'
? form.organizationId
? '系统按部门规则自动生成'
: '请先选择合同负责部门'
: ''
"
/>
</el-form-item>
<el-form-item label="合同名称" prop="contractName">
@@ -417,7 +429,7 @@
/>
</el-select>
</el-form-item>
<el-form-item label="所属组织" prop="organizationName">
<el-form-item label="合同负责部门" prop="organizationName">
<el-cascader
v-model="organizationCascaderValue"
:options="organizationOptions"
@@ -471,6 +483,16 @@
placeholder="请选择签订日期"
/>
</el-form-item>
<el-form-item label="申请日期">
<el-date-picker
v-model="form.applyDate"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="提交正式合同时自动填充"
disabled
/>
</el-form-item>
<el-form-item label="合同期限" prop="startDate">
<div class="contract-manage-form__date-range">
<el-date-picker
@@ -497,12 +519,13 @@
@input="value => integerInput('paymentDays', value)"
><template #suffix>天</template></el-input>
</el-form-item>
<el-form-item label="合同金额">
<el-form-item label="合同金额" prop="contractAmount">
<el-input
:model-value="contractAmountDisplay"
placeholder="请输入"
clearable
@input="handleContractAmountInput"
@blur="formatContractAmountField"
@clear="form.contractAmount = null"
/>
</el-form-item>
@@ -557,7 +580,7 @@
>
</el-form-item>
<el-form-item label="是否需要加盖法人章" prop="legalSealFlag">
<el-select v-model="form.legalSealFlag" placeholder="请选择">
<el-select v-model="form.legalSealFlag" placeholder="请选择" clearable>
<el-option label="否" :value="0" />
<el-option label="是" :value="1" />
</el-select>
@@ -576,20 +599,30 @@
<el-input v-model="form.handlerUserName" disabled />
</el-form-item>
</div>
<el-form-item label="备注" prop="remark" class="contract-manage-form__remark">
<el-form-item label="合同主要内容" prop="mainContent" class="contract-manage-form__remark">
<el-input
v-model="form.mainContent"
type="textarea"
:rows="4"
maxlength="1000"
show-word-limit
placeholder="请输入合同主要内容"
/>
</el-form-item>
<el-form-item label="其他事项说明" prop="remark" class="contract-manage-form__remark">
<el-input
v-model="form.remark"
type="textarea"
:rows="2"
maxlength="200"
show-word-limit
placeholder="请输入备注"
placeholder="请输入其他事项说明"
/>
</el-form-item>
</section>
<contract-attachment-section
title="合同文件"
title="用印材料"
description
attachment-type
:attachment-location-options="contractAttachmentLocationOptions"
@@ -809,10 +842,11 @@
</section>
<contract-attachment-section
title="其它附件"
title="附件(佐证材料等)"
description
attachment-type
attachment-location="其它附件"
attachment-location="附件(佐证材料等)"
attachment-type-dict-code="contract_supporting_attachment_type"
:attachment-location-options="contractAttachmentLocationOptions"
:readonly="isAttachmentUploadMode"
:rows="attachmentRows"
@@ -920,18 +954,19 @@
v-model="billingPlanBox"
:value="billingPlanForm"
:index="billingPlanIndex"
:settlement-currency="form.settlementCurrency"
@save="saveBillingPlan"
/>
<contract-attachment-section
ref="listAttachmentUploader"
dialog-only
title="合同文件"
title="用印材料"
description
attachment-type
use-upload-dialog
mark-approved-on-upload
:attachment-location-options="['合同文件', '其它附件']"
:attachment-location-options="['用印材料', '附件(佐证材料等)']"
:rows="listUploadContractFileRows"
@update:rows="listUploadContractFileRows = $event"
@upload-to-location="handleListAttachmentUploadToLocation"
@@ -988,6 +1023,7 @@
v-model="detailBillingPlanBox"
:value="detailBillingPlanForm"
:index="detailBillingPlanIndex"
:settlement-currency="detailRow.settlementCurrency || form.settlementCurrency"
readonly
/>
@@ -1019,12 +1055,15 @@ import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
import { config, option } from '@/option/business/contract-manage';
import { getToken, isAdministrator } from '@/utils/auth';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { getMKApprovalUrl } from '@/api/system/business-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import BillingPlanEditor from './components/billing-plan-editor.vue';
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
import ContractAttachmentSection, {
attachmentName as sharedAttachmentName,
attachmentUrl as sharedAttachmentUrl,
isContractSealMaterialLocation,
isContractSupportingAttachmentLocation,
normalizeContractFileRows,
} from './components/contract-attachment-section.vue';
import PdfPreview from '@/components/pdf-preview/main.vue';
@@ -1060,6 +1099,19 @@ const normalizeOptionalAmount = (value, emptyValue = null) => {
const number = Number(value);
return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : emptyValue;
};
const formatContractAmountInput = (value, emptyValue = null) => {
if (
value === undefined ||
value === null ||
value === '' ||
Number(value) === -1 ||
String(value) === '-1'
) {
return emptyValue;
}
const number = Number(value);
return Number.isFinite(number) && number >= 0 ? number.toFixed(2) : emptyValue;
};
const normalizeProjectValue = value => {
if (value === undefined || value === null || value === '' || String(value) === '-1') return '';
return value;
@@ -1092,8 +1144,9 @@ const defaultForm = () => ({
partyB: '',
handlerUserName: '',
signDate: '',
applyDate: '',
contractFormat: '',
legalSealFlag: 0,
legalSealFlag: null,
copyCount: '',
settlementCurrency: '',
invoiceCycle: '',
@@ -1109,6 +1162,7 @@ const defaultForm = () => ({
effectiveType: '',
feeGenerationMode: 'system',
archiveStatus: '未归档',
mainContent: '',
remark: '',
});
// 复制新增:仅保留可编辑字段与明细,剔除主键、编号、审批与流程信息
@@ -1120,6 +1174,7 @@ const createCopiedDetail = (detail = {}) => {
return {
...form,
contractNo: '',
applyDate: '',
archiveStatus: '未归档',
feeGenerationMode: detail.feeGenerationMode || '',
billingEnabled: detail.billingEnabled,
@@ -1214,13 +1269,12 @@ export default {
signType: [{ required: true, message: '请选择签约类型', trigger: 'change' }],
settlementCurrency: [{ required: true, message: '请选择结算币种', trigger: 'change' }],
projectName: [{ required: true, message: '请选择所属项目', trigger: 'change' }],
organizationName: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
organizationName: [{ required: true, message: '请选择合同负责部门', trigger: 'change' }],
partyA: [{ required: true, message: '请选择甲方', trigger: 'change' }],
partyB: [{ required: true, message: '请选择乙方', trigger: 'change' }],
signDate: [{ required: true, message: '请选择签订日期', trigger: 'change' }],
settlementMode: [{ required: true, message: '请选择结算方式', trigger: 'change' }],
contractFormat: [{ required: true, message: '请选择合同格式', trigger: 'change' }],
legalSealFlag: [{ required: true, message: '请选择是否需要加盖法人章', trigger: 'change' }],
invoiceCycle: [
{
validator: (rule, value, callback) => {
@@ -1235,6 +1289,22 @@ export default {
],
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }],
contractAmount: [
{ required: true, message: '请输入合同金额', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
return callback(new Error('请输入合同金额'));
}
if (Number(value) < 0) {
return callback(new Error('合同金额不能小于0'));
}
callback();
},
trigger: 'blur',
},
],
mainContent: [{ max: 1000, message: '合同主要内容不能超过1000个字符', trigger: 'blur' }],
},
submitAction: '',
projectOptions: [],
@@ -1301,6 +1371,7 @@ export default {
flowBox: false,
flowUrl: '',
processInstanceId: '',
mkEnabled: false,
// 页面形态在实例创建时锁定,避免 keep-alive 缓存后随全局 $route 翻转
formPageLocked: false,
detailPageLocked: false,
@@ -1343,8 +1414,8 @@ export default {
return this.$route.query.attachmentUpload === '1';
},
contractAttachmentLocationOptions() {
if (this.isAttachmentUploadMode) return ['合同文件'];
return ['合同文件', '其它附件'];
if (this.isAttachmentUploadMode) return ['用印材料'];
return ['用印材料', '附件(佐证材料等)'];
},
contractAmountDisplay() {
const value = this.form.contractAmount;
@@ -1411,7 +1482,7 @@ export default {
expandTrigger: 'click',
};
},
// 所属组织级联:selectedOrganizationId 存部门 id,级联控件用根到节点的 id 路径,
// 合同负责部门级联:selectedOrganizationId 存部门 id,级联控件用根到节点的 id 路径,
// 两者在此双向转换;依赖 organizationOptions,树加载完成后回显自动刷新
organizationCascaderValue: {
get() {
@@ -1520,6 +1591,13 @@ export default {
this.loadProjectOptions();
this.loadSettlementDictionaries();
this.loadOrganizationOptions();
loadMkEnabled()
.then(enabled => {
this.mkEnabled = !!enabled;
})
.catch(() => {
this.mkEnabled = false;
});
}
if (this.isFormPage) this.initFormPage();
if (this.isDetailPage) this.initDetailPage();
@@ -1730,7 +1808,7 @@ export default {
copyCount: normalizeOptionalInteger(detail.copyCount, ''),
invoiceCycle: normalizeOptionalInteger(detail.invoiceCycle, ''),
paymentDays: normalizeOptionalInteger(detail.paymentDays, ''),
contractAmount: normalizeOptionalAmount(detail.contractAmount, null),
contractAmount: formatContractAmountInput(detail.contractAmount, null),
archiveStatus: detail.archiveStatus || '未归档',
feeGenerationMode:
detail.feeGenerationMode || (Number(detail.billingEnabled) === 0 ? 'manual' : 'system'),
@@ -1766,6 +1844,7 @@ export default {
this.applyDetail(createCopiedDetail(detail));
this.form.handlerUserName =
this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || '';
this.refreshPreviewContractNo();
this.$nextTick(() => this.$refs.contractForm?.clearValidate());
},
closeForm() {
@@ -1780,9 +1859,10 @@ export default {
['contractName', '合同名称'],
['contractCategory', '合同类型'],
['projectName', '所属项目'],
['organizationName', '所属组织'],
['organizationName', '合同负责部门'],
['partyA', '甲方'],
['partyB', '乙方'],
['contractAmount', '合同金额'],
];
const empty = required.find(([prop]) => {
const value = this.form[prop];
@@ -1790,10 +1870,16 @@ export default {
});
if (empty) {
this.$message.warning(
empty[0] === 'contractName' ? `请输入${empty[1]}` : `请选择${empty[1]}`
['contractName', 'contractAmount'].includes(empty[0])
? `请输入${empty[1]}`
: `请选择${empty[1]}`
);
return false;
}
if (Number(this.form.contractAmount) < 0) {
this.$message.warning('合同金额不能小于0');
return false;
}
return true;
}
if (!this.form.contractName?.trim()) {
@@ -1816,7 +1902,7 @@ export default {
if (action !== 'formal' && action !== 'edit') return true;
const required = [
['projectName', '所属项目'],
['organizationName', '所属组织'],
['organizationName', '合同负责部门'],
['partyA', '甲方'],
['partyB', '乙方'],
['startDate', '合同开始日期'],
@@ -1824,10 +1910,20 @@ export default {
['signDate', '签订日期'],
['settlementMode', '结算方式'],
['contractFormat', '合同格式'],
['contractAmount', '合同金额'],
];
const empty = required.find(([prop]) => !this.form[prop]);
const empty = required.find(([prop]) => {
const value = this.form[prop];
return value === undefined || value === null || String(value).trim() === '';
});
if (empty) {
this.$message.warning(`请选择或输入${empty[1]}`);
this.$message.warning(
empty[0] === 'contractAmount' ? `请输入${empty[1]}` : `请选择或输入${empty[1]}`
);
return false;
}
if (Number(this.form.contractAmount) < 0) {
this.$message.warning('合同金额不能小于0');
return false;
}
return (
@@ -2017,11 +2113,11 @@ export default {
},
handleListAttachmentUploadToLocation({ location, rows = [] }) {
if (!rows.length) return;
if (location === '合同文件') {
if (isContractSealMaterialLocation(location)) {
this.listUploadContractFileRows = [...(this.listUploadContractFileRows || []), ...rows];
return;
}
if (location === '其它附件') {
if (isContractSupportingAttachmentLocation(location)) {
this.listUploadAttachmentRows = [...(this.listUploadAttachmentRows || []), ...rows];
}
},
@@ -2141,6 +2237,9 @@ export default {
column.renderSearch = scope => this.renderOrganizationSearch(scope);
}
this.syncCurrentOrganizationOption();
if (this.isFormPage && this.formMode === 'add' && this.form.organizationId) {
this.refreshPreviewContractNo();
}
})
.finally(() => {
this.organizationLoading = false;
@@ -2203,6 +2302,29 @@ export default {
}
: { organizationId: '', organizationName: '' }
);
if (this.formMode === 'add') {
this.refreshPreviewContractNo();
}
},
refreshPreviewContractNo() {
if (this.formMode !== 'add') return;
const organizationId = this.form.organizationId;
if (!organizationId) {
this.form.contractNo = '';
return;
}
const requestId = (this.contractNoPreviewRequestId =
(this.contractNoPreviewRequestId || 0) + 1);
api
.nextContractNo(organizationId)
.then(res => {
if (requestId !== this.contractNoPreviewRequestId) return;
this.form.contractNo = res.data?.data || '';
})
.catch(() => {
if (requestId !== this.contractNoPreviewRequestId) return;
this.form.contractNo = '';
});
},
syncCurrentOrganizationOption() {
if (!this.form.organizationId || !this.form.organizationName) return;
@@ -2216,7 +2338,7 @@ export default {
this.organizationFlatOptions.unshift(item);
},
renderOrganizationSearch(scope) {
// 搜索区所属组织:单选级联(与新增/编辑表单一致)
// 搜索区合同负责部门:单选级联(与新增/编辑表单一致)
// 级联返回 id 路径数组,提交时由 normalizeSearch 取末级 id 转组织名称
return h(ElCascader, {
modelValue: Array.isArray(scope.row?.organizationName) ? scope.row.organizationName : [],
@@ -2235,7 +2357,7 @@ export default {
filterable: true,
clearable: true,
style: 'width: 100%',
placeholder: '请选择 所属组织',
placeholder: '请选择 合同负责部门',
});
},
integerInput(prop, value) {
@@ -2248,6 +2370,9 @@ export default {
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
this.form.contractAmount = normalized === '' ? null : normalized;
},
formatContractAmountField() {
this.form.contractAmount = formatContractAmountInput(this.form.contractAmount, null);
},
positiveIntegerInput(prop, value) {
const normalized = String(value || '').replace(/\D/g, '').replace(/^0+/, '');
this.form[prop] = normalized;
@@ -2280,11 +2405,11 @@ export default {
},
handleAttachmentUploadToLocation({ location, rows = [] }) {
if (!rows.length) return;
if (location === '合同文件') {
if (isContractSealMaterialLocation(location)) {
this.contractFileRows = [...(this.contractFileRows || []), ...rows];
return;
}
if (location === '其它附件') {
if (isContractSupportingAttachmentLocation(location)) {
this.attachmentRows = [...(this.attachmentRows || []), ...rows];
}
},
@@ -2645,15 +2770,16 @@ export default {
templateFlag: '是否范本',
originalContractNo: '原件合同编号',
electronicSealFlag: '是否电子章',
remark: '备注',
mainContent: '合同主要内容',
remark: '其他事项说明',
feeGenerationMode: '费用生成模式',
billingPlanJson: '计费方案',
settlementRuleJson: '结算单规则',
preSettlementConfigJson: '预结算配置',
formalSettlementConfigJson: '正式结算配置',
paymentRatioJson: '付款比例设置',
contractFileJson: '合同文件',
attachmentsJson: '其它附件',
contractFileJson: '用印材料',
attachmentsJson: '附件(佐证材料等)',
changeAttachmentsJson: '变更材料',
};
return labels[field] || field;
@@ -2820,13 +2946,8 @@ export default {
detailValue(prop) {
const value = this.detailRow[prop];
if (prop === 'contractAmount') {
return value === undefined ||
value === null ||
value === '' ||
Number(value) === -1 ||
String(value) === '-1'
? '-'
: value;
const amountText = formatContractAmountInput(value, '');
return amountText === '' ? '-' : amountText;
}
if (
prop === 'contractStage' ||
@@ -2858,6 +2979,10 @@ export default {
return;
}
if (operation.action === 'flow') {
if (this.mkEnabled) {
this.handleMkAudit(row);
return;
}
this.openFlow(row);
return;
}
@@ -2911,6 +3036,24 @@ export default {
query: { mode: 'add', copyId: row.id, name: '新增合同管理' },
});
},
handleMkAudit(row) {
if (!row?.id) {
this.$message.warning('合同ID为空,无法打开审核页');
return;
}
getMKApprovalUrl({ bizId: String(row.id) })
.then(res => {
const url = res?.data?.data;
if (!url) {
this.$message.error('获取审核跳转地址失败');
return;
}
window.open(url, '_blank');
})
.catch(() => {
this.$message.error('获取审核跳转地址失败');
});
},
openFlow(row, loaded = false) {
const processInstanceId = row.processInstanceId || row.processInstanceID || row.procInstId;
if (!processInstanceId && !loaded && row.id) {
@@ -3117,7 +3260,11 @@ export default {
&__remark {
width: 100%;
margin-bottom: 0 !important;
margin-bottom: 24px !important;
&:last-child {
margin-bottom: 0 !important;
}
}
&__date-range {
@@ -3191,7 +3338,7 @@ export default {
:deep(.el-form-item) {
align-items: center;
margin-bottom: 14px;
margin-bottom: 24px;
}
:deep(.el-form-item__label) {
+165 -88
View File
@@ -1,6 +1,6 @@
<template>
<basic-container class="loading-manage-page">
<div v-if="!isStandalonePage" class="loading-manage-page__search">
<div v-if="!isStandalonePage && searchVisible" class="loading-manage-page__search">
<el-form :model="searchForm" label-position="right" label-width="88px">
<div class="loading-manage-page__search-grid">
<el-form-item label="配载单号">
@@ -228,17 +228,13 @@
<el-button type="primary" plain @click="handleExport">批量导出</el-button>
<el-button type="primary" plain @click="handleBatchComplete">批量完成</el-button>
</div>
<div class="loading-manage-page__toolbar-right">
<el-tooltip content="刷新" placement="top">
<el-button :icon="Refresh" text @click="loadTable" />
</el-tooltip>
<el-tooltip content="表格密度" placement="top">
<el-button :icon="Rank" text @click="toggleTableSize" />
</el-tooltip>
<el-tooltip content="列设置" placement="top">
<el-button :icon="Setting" text @click="handleColumnSetting" />
</el-tooltip>
</div>
<table-header-tools
:columns="columnSettingOptions"
v-model:visible-column-keys="visibleColumnKeys"
v-model:table-size="tableSize"
v-model:search-visible="searchVisible"
@refresh="loadTable"
/>
</div>
<el-table
@@ -254,7 +250,12 @@
>
<el-table-column type="selection" width="52" fixed="left" align="center" />
<el-table-column type="index" label="序号" width="58" fixed="left" align="center" />
<el-table-column label="配载单号" prop="loadingNo" min-width="170">
<el-table-column
v-if="isColumnVisible('loadingNo')"
label="配载单号"
prop="loadingNo"
min-width="170"
>
<template #default="{ row }">
<el-link
type="primary"
@@ -264,7 +265,12 @@
>
</template>
</el-table-column>
<el-table-column label="运单号" prop="loadingSubNos" min-width="220">
<el-table-column
v-if="isColumnVisible('loadingSubNos')"
label="运单号"
prop="loadingSubNos"
min-width="220"
>
<template #default="{ row }">
<span class="loading-manage-page__sub-nos">
<template v-if="splitText(row.loadingSubNos).length">
@@ -283,11 +289,40 @@
</span>
</template>
</el-table-column>
<el-table-column label="车牌号" prop="vehicleNo" min-width="170" show-overflow-tooltip />
<el-table-column label="司机" prop="driverName" min-width="120" show-overflow-tooltip />
<el-table-column label="联系电话" prop="driverPhone" min-width="140" show-overflow-tooltip />
<el-table-column label="承运类型" prop="carrierType" min-width="120" show-overflow-tooltip />
<el-table-column label="发货地" prop="departureAddress" min-width="180">
<el-table-column
v-if="isColumnVisible('vehicleNo')"
label="车牌号"
prop="vehicleNo"
min-width="170"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('driverName')"
label="司机"
prop="driverName"
min-width="120"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('driverPhone')"
label="联系电话"
prop="driverPhone"
min-width="140"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('carrierType')"
label="承运类型"
prop="carrierType"
min-width="120"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('departureAddress')"
label="发货地"
prop="departureAddress"
min-width="180"
>
<template #default="{ row }">
<el-tooltip
v-if="formatFullRouteAddress(row, 'departure')"
@@ -304,7 +339,12 @@
</div>
</template>
</el-table-column>
<el-table-column label="途经地" prop="transitAddress" min-width="190">
<el-table-column
v-if="isColumnVisible('transitAddress')"
label="途经地"
prop="transitAddress"
min-width="190"
>
<template #default="{ row }">
<el-tooltip
v-if="formatFullRouteAddress(row, 'transit')"
@@ -321,7 +361,12 @@
</div>
</template>
</el-table-column>
<el-table-column label="到货地" prop="arrivalAddress" min-width="180">
<el-table-column
v-if="isColumnVisible('arrivalAddress')"
label="到货地"
prop="arrivalAddress"
min-width="180"
>
<template #default="{ row }">
<el-tooltip
v-if="formatFullRouteAddress(row, 'arrival')"
@@ -338,20 +383,40 @@
</div>
</template>
</el-table-column>
<el-table-column label="承运商" prop="carrierName" min-width="160" show-overflow-tooltip />
<el-table-column label="运输方式" prop="transportType" min-width="150" show-overflow-tooltip>
<el-table-column
v-if="isColumnVisible('carrierName')"
label="承运商"
prop="carrierName"
min-width="160"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('transportType')"
label="运输方式"
prop="transportType"
min-width="150"
show-overflow-tooltip
>
<template #default="{ row }">
{{ transportTypeLabel(row.transportType) }}
</template>
</el-table-column>
<el-table-column label="数据来源" prop="dataSource" min-width="130" show-overflow-tooltip />
<el-table-column
v-if="isColumnVisible('dataSource')"
label="数据来源"
prop="dataSource"
min-width="130"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('createUserName')"
label="创建人"
prop="createUserName"
min-width="120"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('createTime')"
label="创建时间"
prop="createTime"
min-width="170"
@@ -359,6 +424,7 @@
sortable
/>
<el-table-column
v-if="isColumnVisible('updateTime')"
label="更新时间"
prop="updateTime"
min-width="170"
@@ -366,12 +432,20 @@
sortable
/>
<el-table-column
v-if="isColumnVisible('currentProcessNode')"
label="过程节点"
prop="currentProcessNode"
min-width="130"
show-overflow-tooltip
/>
<el-table-column label="状态" prop="businessStatus" width="110" fixed="right" align="center">
<el-table-column
v-if="isColumnVisible('businessStatus')"
label="状态"
prop="businessStatus"
width="110"
fixed="right"
align="center"
>
<template #default="{ row }">
<span :class="['loading-manage-page__status', `is-${row.businessStatus || 'unknown'}`]">
{{ statusText(row) }}
@@ -484,12 +558,16 @@
<div class="loading-detail__field loading-detail__attachment-field">
<span class="loading-detail__label">附件</span>
<span class="loading-detail__value"
><el-link
><span
v-for="file in detailAttachments"
:key="file.url || file.name"
type="primary"
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(file)"
>{{ file.name }}</el-link
@keydown.enter="previewAttachment(file)"
@keydown.space.prevent="previewAttachment(file)"
>{{ file.name }}</span
><span v-if="!detailAttachments.length">-</span></span
>
</div>
@@ -555,15 +633,12 @@
prop="packageType"
label="包装"
min-width="100" /><el-table-column
prop="weight"
label="重量(吨)"
min-width="110" /><el-table-column
prop="volume"
label="体积(方)"
min-width="110" /><el-table-column
prop="quantity"
label="数量"
min-width="90" /><el-table-column
prop="quantityUnit"
label="数量单位"
min-width="100" /><el-table-column
prop="materialCode"
label="物料编码"
min-width="120" /><el-table-column
@@ -665,13 +740,6 @@
record.statusName || (record.punched ? '已打卡' : '未打卡')
}}</el-tag
>
<el-tag
v-if="record.type === 'enroute'"
size="small"
type="info"
effect="plain"
>在途</el-tag
>
<el-tag
v-if="record.exceptionFlag"
size="small"
@@ -701,22 +769,12 @@
>
备注:{{ record.remark }}
</div>
<div
<punch-record-photos
v-if="record.photos && record.photos.length"
class="loading-detail__punch-record-photos"
>
<div
v-for="(photo, pi) in record.photos"
:key="`${record.nodeCode || index}-${pi}`"
class="loading-detail__driver-upload-item"
@click="previewDriverUploadPhoto(record.photos, pi)"
>
<el-image :src="photo.url" fit="cover" lazy />
<div class="loading-detail__driver-upload-label">
{{ photo.label || `${record.nodeName}-凭证` }}
</div>
</div>
</div>
:photos="record.photos"
lazy
@preview="previewDriverUploadPhoto(record.photos, $event)"
/>
</template>
<div v-else class="loading-detail__punch-record-line">暂未打卡</div>
</div>
@@ -1066,6 +1124,7 @@
label="运单号"
min-width="150"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<el-link v-if="row.waybillNo" type="primary" @click="openWaybillDetail(row)">
@@ -1357,7 +1416,7 @@
<el-table-column prop="cargoType" label="货物类型" min-width="140" />
<el-table-column prop="packageType" label="包装" min-width="100" />
<el-table-column prop="quantity" label="数量" min-width="100" />
<el-table-column prop="quantityUnit" label="单位" min-width="100" />
<el-table-column prop="quantityUnit" label="数量单位" min-width="100" />
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
<el-table-column prop="equipmentCode" label="设备编码" min-width="130" />
<el-table-column prop="brand" label="品牌" min-width="120" />
@@ -1400,7 +1459,11 @@
<el-option
v-for="item in taskCarrierOptions"
:key="item.id || item.carrierName"
:label="item.carrierName || item.partyB"
:label="
dialogForm.carrierType === '承运商'
? formatTaskCarrierContractLabel(item)
: item.carrierName || item.partyB
"
:value="
dialogForm.carrierType === '承运商'
? item.id
@@ -1716,12 +1779,14 @@ import { getParentOptions as getCargoTypeOptions } from '@/api/base/cargo-type';
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
import { formatContractOptionLabel } from '@/utils/contract';
import {
getList as getProcessConfigList,
getDetail as getProcessConfigDetail,
getVoucherImages as getProcessConfigVoucherImages,
} from '@/api/business/process-config';
import VoucherExecutionDetail from './components/voucher-execution-detail.vue';
import PunchRecordPhotos from './components/punch-record-photos.vue';
import {
carrierTypeOptions,
loadingManageConfig,
@@ -1730,7 +1795,8 @@ import {
} from '@/option/business/loading-manage';
import { option as waybillOption } from '@/option/business/waybill-manage';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Location, Rank, Refresh, Search, Setting } from '@element-plus/icons-vue';
import { Location, Rank, Search } from '@element-plus/icons-vue';
import TableHeaderTools from '@/components/table-header-tools/main.vue';
import dayjs from 'dayjs';
import { isMobile } from '@/utils/validate';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
@@ -1927,21 +1993,44 @@ const uniqueText = (rows, prop) =>
),
].join('、');
const LOADING_TABLE_COLUMNS = [
{ prop: 'loadingNo', label: '配载单号' },
{ prop: 'loadingSubNos', label: '运单号' },
{ prop: 'vehicleNo', label: '车牌号' },
{ prop: 'driverName', label: '司机' },
{ prop: 'driverPhone', label: '联系电话' },
{ prop: 'carrierType', label: '承运类型' },
{ prop: 'departureAddress', label: '发货地' },
{ prop: 'transitAddress', label: '途经地' },
{ prop: 'arrivalAddress', label: '到货地' },
{ prop: 'carrierName', label: '承运商' },
{ prop: 'transportType', label: '运输方式' },
{ prop: 'dataSource', label: '数据来源' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
{ prop: 'updateTime', label: '更新时间' },
{ prop: 'currentProcessNode', label: '过程节点' },
{ prop: 'businessStatus', label: '状态' },
];
export default {
components: {
Rank,
Search,
TableHeaderTools,
VoucherExecutionDetail,
PunchRecordPhotos,
},
data() {
return {
Rank,
Location,
Refresh,
Search,
Setting,
loading: false,
tableSize: 'default',
searchVisible: true,
columnSettingOptions: LOADING_TABLE_COLUMNS,
visibleColumnKeys: LOADING_TABLE_COLUMNS.map(column => column.prop),
tableData: [],
page: {
currentPage: 1,
@@ -2189,6 +2278,14 @@ export default {
},
},
methods: {
formatTaskCarrierContractLabel(item = {}) {
const carrierName = item.carrierName || item.partyB || '';
const contractLabel = formatContractOptionLabel(item);
if (carrierName && contractLabel) {
return `${carrierName}(${contractLabel})`;
}
return carrierName || contractLabel;
},
syncStandaloneTagTitle() {
if (!this.isStandalonePage) return;
const title = this.isStandaloneDetailPage ? this.detailPageTitle : this.formPageDefaultTitle;
@@ -2248,13 +2345,8 @@ export default {
selectionChange(rows) {
this.selectionRows = rows || [];
},
toggleTableSize() {
const order = ['default', 'small', 'large'];
const index = order.indexOf(this.tableSize);
this.tableSize = order[(index + 1) % order.length];
},
handleColumnSetting() {
ElMessage.info('列设置按原型保留入口,当前默认展示全部列表字段');
isColumnVisible(prop) {
return this.visibleColumnKeys.includes(prop);
},
statusText(row) {
return row.businessStatusName || loadingStatusMap[row.businessStatus] || '未知';
@@ -2757,7 +2849,6 @@ export default {
if (status === 'draft') {
return [
{ type: 'edit', label: '编辑' },
{ type: 'copy', label: '复制' },
{ type: 'delete', label: '删除' },
];
}
@@ -2769,36 +2860,24 @@ export default {
if (row.driverRejected) {
actions.push({ type: 'reassign', label: '重新派单' });
}
actions.push(
{ type: 'edit', label: '编辑' },
{ type: 'copy', label: '复制' },
{ type: 'cancel', label: '取消' }
);
actions.push({ type: 'edit', label: '编辑' }, { type: 'cancel', label: '取消' });
return actions;
}
if (status === 'running') {
return [
{ type: 'view', label: '详情' },
{ type: 'complete', label: '完成' },
{ type: 'copy', label: '复制' },
];
}
if (status === 'completed' || status === 'cancelled') {
return [
{ type: 'view', label: '详情' },
{ type: 'copy', label: '复制' },
];
return [{ type: 'view', label: '详情' }];
}
return [
{ type: 'view', label: '详情' },
{ type: 'copy', label: '复制' },
];
return [{ type: 'view', label: '详情' }];
},
handleRowAction(type, row) {
const actionMap = {
view: () => this.gotoLoadingDetail(row),
edit: () => this.openLoadingDialog('edit', row),
copy: () => this.gotoLoadingCopy(row),
reassign: () => this.gotoLoadingReassign(row),
delete: () => this.deleteLoading(row),
start: () => this.startLoading(row),
@@ -4473,8 +4552,7 @@ export default {
margin: 12px 0;
}
.loading-manage-page__toolbar-left,
.loading-manage-page__toolbar-right {
.loading-manage-page__toolbar-left {
display: flex;
align-items: center;
gap: 12px;
@@ -4733,7 +4811,7 @@ export default {
}
:deep(.el-form-item) {
margin-bottom: 14px;
margin-bottom: 24px;
}
:deep(.el-form-item__label) {
@@ -4928,7 +5006,6 @@ export default {
margin-left: 12px;
}
}
.loading-detail__punch-record-photos,
.loading-detail__driver-upload-grid,
.loading-detail__voucher-image-grid {
display: grid;
+24 -4
View File
@@ -1,7 +1,7 @@
<template>
<basic-container class="master-order-page">
<template v-if="mode === 'list'">
<div class="master-order-page__search">
<div v-show="searchVisible" class="master-order-page__search">
<el-form :model="query" label-position="right" label-width="88px" @submit.prevent>
<div class="master-order-page__search-grid">
<el-form-item v-for="field in primaryFields" :key="field.prop" :label="field.label">
@@ -55,8 +55,16 @@
</div>
<section class="master-list-panel">
<div class="toolbar">
<el-button type="primary" @click="goCreate()">新建多联总单</el-button
><el-button type="primary" plain @click="download">批量导出</el-button>
<div class="toolbar-left">
<el-button type="primary" @click="goCreate()">新建多联总单</el-button>
<el-button type="primary" plain @click="download">批量导出</el-button>
</div>
<table-header-tools
:show-column="false"
:show-grid="false"
v-model:search-visible="searchVisible"
@refresh="load"
/>
</div>
<div v-loading="loading" class="master-cards">
<article v-for="row in records" :key="row.id" class="master-card">
@@ -147,12 +155,13 @@
<script>
import * as api from '@/api/business/master-order';
import TableHeaderTools from '@/components/table-header-tools/main.vue';
import MasterOrderEditor from './components/master-order-editor.vue';
import MasterOrderDispatch from './components/master-order-dispatch.vue';
import MasterOrderDetail from './components/master-order-detail.vue';
export default {
components: { MasterOrderEditor, MasterOrderDispatch, MasterOrderDetail },
components: { TableHeaderTools, MasterOrderEditor, MasterOrderDispatch, MasterOrderDetail },
data() {
return {
// 实例创建时锁定所属路由。keep-alive 缓存后 $route 会变成其它页面,
@@ -160,6 +169,7 @@ export default {
//(mode=add 且无 id)会落入详情分支并请求缺少 id 的详情接口。
routePathLocked: this.$route.path,
searchExpanded: false,
searchVisible: true,
loading: false,
records: [],
query: { businessStatus: '' },
@@ -633,8 +643,18 @@ export default {
}
.master-list-panel {
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 8px 0;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 12px;
}
}
.master-card {
display: flex;
+57 -21
View File
@@ -429,7 +429,7 @@ const NODE_TEMPLATES = [
type: 'confirm',
supportVoucher: false,
},
{ key: 'arrive_scene', name: '到场', type: 'punch', enabled: false },
{ key: 'arrive_scene', name: '到场', type: 'punch', enabled: false, supportCargo: false },
{
key: 'load',
name: '装货',
@@ -485,6 +485,7 @@ const NODE_TEMPLATES = [
key: 'return',
name: '回单',
type: 'confirm',
supportCargo: false,
supportVoucher: true,
voucherOptions: ['磅单', '签收单'],
},
@@ -565,7 +566,13 @@ export default {
});
},
defaultDeptId() {
return (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
const raw = (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
return (
String(raw)
.split(',')
.map(item => item.trim())
.filter(Boolean)[0] || ''
);
},
applyDefaultDeptSearch() {
const deptId = this.defaultDeptId();
@@ -580,15 +587,32 @@ export default {
});
},
flattenDept(tree, level = 0) {
return tree.flatMap(item => [
{
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
value: item.id,
},
...(item.children && item.children.length
? this.flattenDept(item.children, level + 1)
: []),
]);
return tree.flatMap(item => {
const id =
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
if (!id) return [];
return [
{
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
value: id,
},
...(item.children && item.children.length
? this.flattenDept(item.children, level + 1)
: []),
];
});
},
normalizeListQuery(params = {}) {
const query = { ...params, ...this.query };
const rawDeptId = query.deptId ?? query.searchDeptId;
delete query.searchDeptId;
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
query.deptId = String(rawDeptId).split(',')[0].trim();
} else {
delete query.deptId;
}
query.allDept = this.isAdmin ? 1 : 0;
return query;
},
displayStatus(status) {
const statusMap = {
@@ -631,7 +655,9 @@ export default {
enabled: false,
punch: true,
location: true,
uploadCargo: true,
// 不支持货量的节点(到场/回单/接单等)不得默认勾上,否则会写出
// 「上传货量为是、货量类型为空」的配置,而这类节点在界面上没有货量开关可关
uploadCargo: !!template?.supportCargo,
cargoTypes: [],
uploadVoucher: undefined,
voucherTypes: [],
@@ -646,6 +672,10 @@ export default {
confirmInternal: false,
...template,
};
if (!node.supportCargo) {
node.uploadCargo = false;
node.cargoTypes = [];
}
if ((node.key === 'accept' || node.name === '接单') && node.confirmMode === 'yes') {
node.confirmDriver = true;
} else if ((node.key === 'accept' || node.name === '接单') && node.confirmMode !== 'yes') {
@@ -918,6 +948,15 @@ export default {
return false;
}
}
// 勾了上传货量却没选类型,在小程序侧等同于不采集货量;此处直接拦住,
// 避免保存出「显示采集、实际不采集」的配置
const cargoWithoutTypes = this.nodeRows.find(
item => item.enabled && item.supportCargo && item.uploadCargo && !(item.cargoTypes || []).length
);
if (cargoWithoutTypes) {
this.$message.warning(`「${cargoWithoutTypes.name}」已选择上传货量,请至少勾选一个货量类型`);
return false;
}
return true;
},
needsDuplicateVoucherConfirm() {
@@ -1074,9 +1113,9 @@ export default {
this.onLoad(this.page, this.query);
},
searchChange(params, done) {
this.query = params;
this.query = this.normalizeListQuery(params);
this.page.currentPage = 1;
this.onLoad(this.page, params);
this.onLoad(this.page, this.query);
done();
},
selectionChange(list) {
@@ -1099,12 +1138,10 @@ export default {
},
onLoad(page, params = {}) {
this.loading = true;
const query = this.normalizeListQuery(params);
this.query = query;
api
.getList(page.currentPage, page.pageSize, {
...params,
...this.query,
allDept: this.isAdmin ? 1 : 0,
})
.getList(page.currentPage, page.pageSize, query)
.then(res => {
const result = res.data.data || {};
this.page.total = result.total || 0;
@@ -1117,8 +1154,7 @@ export default {
},
buildExportParams() {
return {
...this.query,
allDept: this.isAdmin ? 1 : 0,
...this.normalizeListQuery(this.query),
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
+45 -13
View File
@@ -573,7 +573,7 @@
type="textarea"
:rows="2"
placeholder="请输入"
maxlength="200"
maxlength="1000"
show-word-limit
/>
</el-form-item>
@@ -583,7 +583,7 @@
type="textarea"
:rows="2"
placeholder="请输入"
maxlength="200"
maxlength="1000"
show-word-limit
/>
</el-form-item>
@@ -593,7 +593,7 @@
type="textarea"
:rows="2"
placeholder="请输入"
maxlength="200"
maxlength="1000"
show-word-limit
/>
</el-form-item>
@@ -603,7 +603,7 @@
type="textarea"
:rows="2"
placeholder="请输入"
maxlength="200"
maxlength="1000"
show-word-limit
/>
</el-form-item>
@@ -645,11 +645,22 @@
</el-select>
</template>
</el-table-column>
<el-table-column label="文件名" min-width="220" align="center" show-overflow-tooltip>
<el-table-column
label="文件名"
min-width="220"
align="center"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }}
</el-link>
<span
class="attachment-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="240" align="center">
@@ -846,15 +857,15 @@
v-model="userQuery.realName"
placeholder="用户姓名"
clearable
@keyup.enter="loadUsers"
@keyup.enter="searchUsers"
/>
<el-input
v-model="userQuery.phone"
placeholder="手机号"
clearable
@keyup.enter="loadUsers"
@keyup.enter="searchUsers"
/>
<el-button type="primary" @click="loadUsers">查询</el-button>
<el-button type="primary" @click="searchUsers">查询</el-button>
<el-button @click="resetUserQuery">重置</el-button>
</div>
<el-table :data="userData" border v-loading="userLoading" @row-dblclick="selectUser">
@@ -1188,6 +1199,18 @@ export default {
changeReason: [
{ max: 2000, message: '变更原因不能超过2000个字符', trigger: 'blur' },
],
projectIntro: [
{ max: 1000, message: '项目简介不能超过1000个字符', trigger: 'blur' },
],
profitRemark: [
{ max: 1000, message: '盈利模式及利润测算情况说明不能超过1000个字符', trigger: 'blur' },
],
riskPoint: [
{ max: 1000, message: '项目风险点不能超过1000个字符', trigger: 'blur' },
],
emergencyPlan: [
{ max: 1000, message: '风险防范措施及应急预案不能超过1000个字符', trigger: 'blur' },
],
},
deptOptions: [],
businessDeptTreeOptions: [],
@@ -2756,11 +2779,20 @@ export default {
if (this.dialogReadonly) return;
this.userPickType = type;
this.userBox = true;
this.userPage.currentPage = 1;
this.loadUsers();
},
searchUsers() {
this.userPage.currentPage = 1;
this.loadUsers();
},
loadUsers() {
this.userLoading = true;
getUserList(this.userPage.currentPage, this.userPage.pageSize, this.userQuery)
const params = {
realName: String(this.userQuery.realName || '').trim(),
phone: String(this.userQuery.phone || '').trim(),
};
getUserList(this.userPage.currentPage, this.userPage.pageSize, params)
.then(res => {
const result = res.data.data || {};
this.userData = result.records || [];
@@ -2810,7 +2842,7 @@ export default {
}
:deep(.el-form-item) {
margin-bottom: 14px;
//margin-bottom: 24px;
}
:deep(.el-form-item__label) {
+50 -10
View File
@@ -99,11 +99,21 @@
<el-table :data="attachmentRows" border @selection-change="selectedAttachments = $event">
<el-table-column 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>
<el-table-column
label="文件名称"
min-width="240"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }}
</el-link>
<span
class="attachment-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">
@@ -184,6 +194,7 @@
title="查看临时额度申请"
append-to-body
destroy-on-close
align-center
:width="formDialogWidth"
class="temporary-credit-limit-dialog temporary-credit-limit-detail-dialog"
>
@@ -216,11 +227,21 @@
</div>
<el-table :data="attachmentRows" empty-text="暂无附件">
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="文件名称" min-width="240" show-overflow-tooltip>
<el-table-column
label="文件名称"
min-width="240"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }}
</el-link>
<span
class="attachment-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" show-overflow-tooltip>
@@ -615,6 +636,12 @@ export default {
}
const value = row[prop];
if (value === undefined || value === null || value === '') return '-';
if (
(prop === 'usedFundLimit' || prop === 'remainingFundLimit') &&
Number(value) === -1
) {
return '-';
}
return value;
},
applyDetail(detail) {
@@ -1188,10 +1215,10 @@ export default {
}
// 新增/编辑/查看:宽度与高度规则统一(Avue setPx 对 '1100px' 会拼成 '1100pxpx',故 option 用数字 1100)
// margin:auto 配合下方 unscoped 的 overlay flex,实现垂直水平居中
.temporary-credit-limit-dialog.el-dialog {
width: 1100px !important;
margin-top: 20px !important;
margin-bottom: 20px !important;
margin: auto !important;
height: auto !important;
max-height: calc(100vh - 40px) !important;
display: flex;
@@ -1329,3 +1356,16 @@ export default {
}
</style>
<!-- append-to-body 弹窗挂到 body,overlay 无 scoped 属性,需全局样式才能居中 -->
<style lang="scss">
.el-overlay-dialog:has(> .temporary-credit-limit-dialog) {
display: flex;
align-items: center;
justify-content: center;
}
.temporary-credit-limit-dialog.el-dialog {
margin: auto !important;
}
</style>
+51 -22
View File
@@ -585,6 +585,10 @@ export default {
this.$message.warning(`第${index + 1}条调度明细缺少车牌号`);
return;
}
if (!this.resolveDispatchQuantityUnit(row)) {
this.$message.warning(`第${index + 1}条调度明细缺少数量单位`);
return;
}
}
this.$confirm(
@@ -600,32 +604,57 @@ export default {
this.submitDispatch();
});
},
resolveDispatchQuantityUnit(row = {}) {
const topUnit = String(row.quantityUnit || '').trim();
if (topUnit) return topUnit;
const goodsList = this.parseGoodsList(row.goodsJson);
for (const goods of goodsList) {
const unit = String(
goods?.quantityUnit || goods?.goodsQuantityUnit || goods?.unit || ''
).trim();
if (unit) return unit;
}
return TRANSPORT_PLAN_QUANTITY_UNIT;
},
enrichDispatchGoodsJson(row = {}, quantityUnit) {
const goodsList = this.parseGoodsList(row.goodsJson);
if (goodsList.length) {
const nextGoods = goodsList.map(goods => ({
...goods,
quantityUnit: String(goods.quantityUnit || quantityUnit || '').trim() || quantityUnit,
}));
return JSON.stringify(nextGoods);
}
return JSON.stringify([
{
cargoName: row.cargoName,
cargoType: row.cargoType,
quantity: row.quantity,
quantityUnit,
specification: row.specification,
model: row.model,
packageType: row.packageType,
brand: row.brand,
},
]);
},
submitDispatch() {
this.saving = true;
const payload = {
id: this.planId,
mode: 'submit',
waybills: this.dispatchList.map(row => ({
...row,
taskEntryMode: row.taskEntryMode || 'simple',
taskRemark: row.remark || '',
estimatedStartTime: row.estimatedStartTime || this.planData.planStartDate || '',
estimatedEndTime: row.estimatedEndTime || this.planData.planEndDate || '',
goodsJson:
row.goodsJson ||
JSON.stringify([
{
cargoName: row.cargoName,
cargoType: row.cargoType,
quantity: row.quantity,
quantityUnit: row.quantityUnit,
specification: row.specification,
model: row.model,
packageType: row.packageType,
brand: row.brand,
},
]),
})),
waybills: this.dispatchList.map(row => {
const quantityUnit = this.resolveDispatchQuantityUnit(row);
return {
...row,
quantityUnit,
taskEntryMode: row.taskEntryMode || 'simple',
taskRemark: row.remark || '',
estimatedStartTime: row.estimatedStartTime || this.planData.planStartDate || '',
estimatedEndTime: row.estimatedEndTime || this.planData.planEndDate || '',
goodsJson: this.enrichDispatchGoodsJson(row, quantityUnit),
};
}),
};
dispatch(payload)
@@ -639,7 +668,7 @@ export default {
})
.catch(err => {
console.error('调度失败:', err);
this.$message.error('调度失败');
this.$message.error(err?.response?.data?.msg || err?.message || '调度失败');
})
.finally(() => {
this.saving = false;
+4 -2
View File
@@ -51,7 +51,7 @@
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="item.contractName"
:label="formatContractOptionLabel(item)"
:value="item.id"
/>
</el-select>
@@ -165,6 +165,7 @@ import { getList as getContractList } from '@/api/business/contract-manage';
import { getList as getProjectList } from '@/api/business/project-apply';
import { importTransportPlan, validateTransportPlan } from '@/api/business/transport-plan';
import { config } from '@/option/business/transport-plan';
import { formatContractOptionLabel } from '@/utils/contract';
import { downloadXls } from '@/utils/util';
import dayjs from 'dayjs';
@@ -271,6 +272,7 @@ export default {
this.loadProjectOptions();
},
methods: {
formatContractOptionLabel,
loadProjectOptions() {
if (this.projectLoading) return;
this.projectLoading = true;
@@ -651,7 +653,7 @@ export default {
}
:deep(.el-form-item) {
margin-bottom: 16px;
margin-bottom: 24px;
}
}
+18 -4
View File
@@ -96,11 +96,25 @@ const uploadReplaceFile = async file => {
if (!task?.id || !task.objectKey) throw new Error('创建分片上传任务失败');
replaceProgress.value = 0;
for (let partNumber = 1; partNumber <= task.chunkTotal; partNumber += 1) {
const urlRes = await api.getPartUploadUrl(task.id, partNumber);
const part = file.slice((partNumber - 1) * chunkSize, Math.min(partNumber * chunkSize, file.size));
const response = await fetch(urlRes.data?.data, { method: 'PUT', body: part });
if (!response.ok) throw new Error(`分片${partNumber}上传失败`);
const etag = String(response.headers.get('etag') || '').replaceAll('"', '');
const urlRes = await api.getPartUploadUrl(task.id, partNumber);
const url = urlRes.data?.data;
let etag = '';
const needProxy =
typeof window !== 'undefined' &&
window.location.protocol === 'https:' &&
String(url || '')
.trim()
.toLowerCase()
.startsWith('http:');
if (!url || needProxy) {
const proxyRes = await api.uploadPartByProxy(task.id, partNumber, part);
etag = String(proxyRes.data?.data || '').replaceAll('"', '');
} else {
const response = await fetch(url, { method: 'PUT', body: part });
if (!response.ok) throw new Error(`分片${partNumber}上传失败`);
etag = String(response.headers.get('etag') || '').replaceAll('"', '');
}
if (!etag) throw new Error('未获取到分片 ETag');
await api.updateFileTask({ id: task.id, partNumber, etag });
replaceProgress.value = Math.round((partNumber / task.chunkTotal) * 100);
+139 -22
View File
@@ -1,6 +1,6 @@
<template>
<basic-container class="voucher-manage-page">
<section class="voucher-manage-page__search">
<section v-show="searchVisible" class="voucher-manage-page__search">
<el-form :model="query" label-position="right" label-width="88px">
<div class="voucher-manage-page__search-grid">
<el-form-item label="凭证批次号"
@@ -55,10 +55,25 @@
</section>
<section>
<div class="voucher-manage-page__toolbar">
<el-button type="primary" @click="openUpload">批量导入凭证</el-button
><el-button plain @click="openProgress">上传进度</el-button>
<div class="voucher-manage-page__toolbar-left">
<el-button type="primary" @click="openUpload">批量导入凭证</el-button>
<el-button plain @click="openProgress">上传进度</el-button>
</div>
<table-header-tools
:columns="columnSettingOptions"
v-model:visible-column-keys="visibleColumnKeys"
v-model:table-size="tableSize"
v-model:search-visible="searchVisible"
@refresh="load"
/>
</div>
<el-table v-loading="loading" :data="rows" border class="voucher-manage-page__table">
<el-table
v-loading="loading"
:data="rows"
:size="tableSize"
border
class="voucher-manage-page__table"
>
<el-table-column type="selection" width="52" fixed="left" align="center" /><el-table-column
type="index"
label="序号"
@@ -66,27 +81,47 @@
fixed="left"
align="center"
/>
<el-table-column prop="voucherBatchNo" label="凭证批次号" min-width="150" fixed="left"
<el-table-column
v-if="isColumnVisible('voucherBatchNo')"
prop="voucherBatchNo"
label="凭证批次号"
min-width="150"
fixed="left"
><template #default="{ row }"
><el-link class="voucher-manage-page__link" type="primary" @click="view(row)">{{
row.voucherBatchNo
}}</el-link></template
></el-table-column
>
<el-table-column prop="waybillBatchNo" label="运单批次号" min-width="170" /><el-table-column
<el-table-column
v-if="isColumnVisible('waybillBatchNo')"
prop="waybillBatchNo"
label="运单批次号"
min-width="170"
/><el-table-column
v-if="isColumnVisible('fileName')"
prop="fileName"
label="文件名"
min-width="200"
show-overflow-tooltip
/>
<el-table-column v-if="!isCarrier" prop="uploadSource" label="上传来源" width="100" /><el-table-column
v-if="!isCarrier"
<el-table-column
v-if="!isCarrier && isColumnVisible('uploadSource')"
prop="uploadSource"
label="上传来源"
width="100"
/><el-table-column
v-if="!isCarrier && isColumnVisible('carrierName')"
prop="carrierName"
label="承运商名称"
min-width="160"
><template #default="{ row }">{{ row.carrierName || '-' }}</template></el-table-column
>
<el-table-column prop="processStatus" label="处理状态" width="120"
<el-table-column
v-if="isColumnVisible('processStatus')"
prop="processStatus"
label="处理状态"
width="120"
><template #default="{ row }"
><span
:class="{ 'voucher-manage-page__process-failed': row.processStatus === '处理失败' }"
@@ -94,17 +129,35 @@
></template
></el-table-column
><el-table-column
v-if="isColumnVisible('voucherCount')"
prop="voucherCount"
label="凭证数量"
width="100"
/><el-table-column
v-if="isColumnVisible('relatedWaybillCount')"
prop="relatedWaybillCount"
label="凭证已关联数量"
width="125"
/> <el-table-column prop="unRelatedWaybillCount" label="凭证未关联数量" width="125" />
<el-table-column prop="createUserName" label="创建人" min-width="120" />
<el-table-column prop="createTime" label="创建时间" min-width="170" />
/><el-table-column
v-if="isColumnVisible('unRelatedWaybillCount')"
prop="unRelatedWaybillCount"
label="凭证未关联数量"
width="125"
/>
<el-table-column
v-if="isColumnVisible('createUserName')"
prop="createUserName"
label="创建人"
min-width="120"
/>
<el-table-column
v-if="isColumnVisible('createTime')"
prop="createTime"
label="创建时间"
min-width="170"
/>
<el-table-column
v-if="isColumnVisible('auditStatus')"
prop="auditStatus"
label="审核状态"
width="120"
@@ -512,6 +565,7 @@ import { WarnTriangleFilled } from '@element-plus/icons-vue';
import { getList as getProjectList } from '@/api/business/project-apply';
import * as api from '@/api/business/voucher-manage';
import SectionCard from '@/components/section-card/main.vue';
import TableHeaderTools from '@/components/table-header-tools/main.vue';
const router = useRouter();
const store = useStore();
@@ -539,6 +593,47 @@ const checkIfCarrier = () => {
isCarrier.value = topDeptName === '外部组织';
};
const VOUCHER_BASE_COLUMNS = [
{ prop: 'voucherBatchNo', label: '凭证批次号' },
{ prop: 'waybillBatchNo', label: '运单批次号' },
{ prop: 'fileName', label: '文件名' },
{ prop: 'uploadSource', label: '上传来源', carrierHidden: true },
{ prop: 'carrierName', label: '承运商名称', carrierHidden: true },
{ prop: 'processStatus', label: '处理状态' },
{ prop: 'voucherCount', label: '凭证数量' },
{ prop: 'relatedWaybillCount', label: '凭证已关联数量' },
{ prop: 'unRelatedWaybillCount', label: '凭证未关联数量' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
{ prop: 'auditStatus', label: '审核状态' },
];
const searchVisible = ref(true);
const tableSize = ref('default');
const visibleColumnKeys = ref([]);
const columnSettingOptions = computed(() =>
VOUCHER_BASE_COLUMNS.filter(column => !isCarrier.value || !column.carrierHidden).map(column => ({
prop: column.prop,
label: column.label,
}))
);
const isColumnVisible = prop => visibleColumnKeys.value.includes(prop);
watch(
columnSettingOptions,
options => {
const props = options.map(column => column.prop);
if (!visibleColumnKeys.value.length) {
visibleColumnKeys.value = props;
return;
}
const visible = new Set(visibleColumnKeys.value);
visibleColumnKeys.value = props.filter(prop => visible.has(prop));
if (!visibleColumnKeys.value.length) visibleColumnKeys.value = props;
},
{ immediate: true }
);
onMounted(() => {
checkIfCarrier();
});
@@ -742,6 +837,30 @@ const calculateMd5 = async file => {
const buffer = await file.arrayBuffer();
return md5.base64(buffer);
};
const needsUploadProxy = url =>
typeof window !== 'undefined' &&
window.location.protocol === 'https:' &&
String(url || '')
.trim()
.toLowerCase()
.startsWith('http:');
const uploadPartWithFallback = async (taskId, partNumber, part, signal) => {
const urlRes = await api.getPartUploadUrl(taskId, partNumber);
if (signal?.aborted) throw new DOMException('上传已暂停', 'AbortError');
const url = urlRes.data?.data;
// HTTPS 页面直连 http://IP MinIO 会被 Mixed Content 拦截,回退服务端代传
if (!url || needsUploadProxy(url)) {
const proxyRes = await api.uploadPartByProxy(taskId, partNumber, part);
return String(proxyRes.data?.data || '').replaceAll('"', '');
}
const response = await fetch(url, {
method: 'PUT',
body: part,
signal,
});
if (!response.ok) throw new Error(`分片${partNumber}上传失败`);
return String(response.headers.get('etag') || '').replaceAll('"', '');
};
const uploadParts = async (file, task, sessionId) => {
uploading.value = true;
activeUploadTaskId.value = task.id;
@@ -756,21 +875,13 @@ const uploadParts = async (file, task, sessionId) => {
) {
ensureUploadSession(sessionId, task.id);
if (abortController.signal.aborted) throw new DOMException('上传已暂停', 'AbortError');
const urlRes = await api.getPartUploadUrl(task.id, partNumber);
ensureUploadSession(sessionId, task.id);
if (abortController.signal.aborted) throw new DOMException('上传已暂停', 'AbortError');
const url = urlRes.data?.data;
const part = file.slice(
(partNumber - 1) * chunkSize,
Math.min(partNumber * chunkSize, file.size)
);
const response = await fetch(url, {
method: 'PUT',
body: part,
signal: abortController.signal,
});
if (!response.ok) throw new Error(`分片${partNumber}上传失败`);
const etag = String(response.headers.get('etag') || '').replaceAll('"', '');
const etag = await uploadPartWithFallback(task.id, partNumber, part, abortController.signal);
if (!etag) throw new Error('MinIO 未返回分片 ETag,请确认跨域配置已暴露 ETag 响应头');
await api.updateFileTask({ id: task.id, partNumber, etag });
ensureUploadSession(sessionId, task.id);
@@ -1329,9 +1440,15 @@ load();
&__toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin: 12px 0;
}
&__toolbar-left {
display: flex;
align-items: center;
gap: 12px;
}
&__table {
width: 100%;
}
@@ -1370,7 +1487,7 @@ load();
display: none;
}
&__upload-form :deep(.el-form-item) {
margin-bottom: 14px;
margin-bottom: 24px;
}
&__upload-tip {
margin-left: 16px;
+1 -1
View File
@@ -529,7 +529,7 @@ export default {
<style scoped lang="scss">
.bill-ledger-form-page__form :deep(.el-form-item) {
margin-bottom: 14px;
margin-bottom: 24px;
}
.bill-ledger-form-page__basic-grid {
display: grid;
+1 -1
View File
@@ -367,7 +367,7 @@ export default {
<style scoped lang="scss">
.bill-payment-form-page__form :deep(.el-form-item) {
margin-bottom: 14px;
margin-bottom: 24px;
}
.bill-payment-form-page__form :deep(.el-input),
.bill-payment-form-page__form :deep(.el-input-number),
@@ -61,7 +61,7 @@
<el-link
v-if="
mkEnabled &&
row.approvalStatus === 'reviewing' &&
['reviewing', 'approved'].includes(row.approvalStatus) &&
hasPermission('bill_payment_approve')
"
type="primary"
@@ -499,7 +499,6 @@ const emptyLine = () => ({
localId: ++localId,
goodsCategory: '',
goodsName: '',
taxClassificationCode: '',
unit: '吨',
quantity: 0,
unitPriceNoTax: 0,
@@ -743,11 +742,9 @@ export default {
const item = this.findInvoiceItem(line);
if (item) {
line.taxRate = Number(item.defaultTaxRate || 0);
line.taxClassificationCode = item.taxClassificationCode || '';
} else if (clearInvalid) {
line.goodsName = '';
line.taxRate = 0;
line.taxClassificationCode = '';
}
this.recalculateLine(line);
},
@@ -755,7 +752,6 @@ export default {
const names = this.invoiceItemNames(line.goodsCategory);
if (!names.some(item => item.shortName === line.goodsName)) line.goodsName = '';
line.taxRate = 0;
line.taxClassificationCode = this.findInvoiceItem(line)?.taxClassificationCode || '';
if (names.length === 1) {
line.goodsName = names[0].shortName;
this.handleGoodsNameChange(line);
@@ -765,11 +761,9 @@ export default {
const item = this.findInvoiceItem(line);
if (!item) {
line.taxRate = 0;
line.taxClassificationCode = '';
return;
}
line.taxRate = Number(item.defaultTaxRate || 0);
line.taxClassificationCode = item.taxClassificationCode || '';
this.recalculateLine(line);
},
validateSettlements(rule, value, callback) {
@@ -1165,7 +1159,6 @@ export default {
lines: sheet.lines.map(line => ({
goodsCategory: line.goodsCategory,
goodsName: line.goodsName,
taxClassificationCode: line.taxClassificationCode,
unit: line.unit,
quantity: line.quantity,
unitPriceNoTax: line.unitPriceNoTax,
+5 -54
View File
@@ -61,7 +61,6 @@
v-if="hasPermission('invoice_application_sync')"
type="primary"
plain
:loading="batchSyncing"
@click="handleBatchSync"
>批量同步</el-button
>
@@ -115,12 +114,6 @@
{{ displayValue(row[column.prop]) }}
</el-tag>
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
<template v-else-if="column.prop === 'kingdeeLockStatus'">
<el-tooltip v-if="lockTag(row).show" :content="lockTag(row).tip" placement="top">
<el-tag :type="lockTag(row).type" class="status-text">{{ lockTag(row).label }}</el-tag>
</el-tooltip>
<span v-else>-</span>
</template>
<span v-else>{{ displayValue(row[column.prop]) }}</span>
</template>
</el-table-column>
@@ -161,7 +154,7 @@
v-if="
mkEnabled &&
hasPermission('invoice_application_approve') &&
row.approvalStatus === 'reviewing'
['reviewing', 'approved'].includes(row.approvalStatus)
"
type="primary"
@click="handleMkAudit(row)"
@@ -189,9 +182,7 @@
>
<el-link
v-if="
hasPermission('invoice_application_void') &&
row.approvalStatus === 'approved' &&
row.kingdeeLockStatus !== 'locked'
hasPermission('invoice_application_void') && row.approvalStatus === 'approved'
"
type="danger"
@click="handleVoid(row)"
@@ -253,7 +244,6 @@ export default {
rows: [],
selection: [],
loading: false,
batchSyncing: false,
page: { current: 1, size: 10, total: 0 },
mkEnabled: false,
};
@@ -402,31 +392,14 @@ export default {
this.loadTable();
},
async handleBatchSync() {
if (this.batchSyncing) return;
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
if (!rows.length) {
this.$message.warning('请选择至少一条审批通过的开票申请');
return;
}
this.batchSyncing = true;
try {
await Promise.all(rows.map(row => api.syncKingdee(row.id)));
this.$message.success(`已同步${rows.length}条开票申请`);
await this.loadTable();
// 附件联带同步提示:同步成功但附件未全部上传时追加 warning,重推即自动补传
const idSet = new Set(rows.map(row => String(row.id)));
const pendingCount = (this.rows || []).filter(row => idSet.has(String(row.id))
&& ['pending', 'partial'].includes(row.attachmentSyncStatus)).length;
if (pendingCount) {
this.$message({
type: 'warning',
message: `其中${pendingCount}单附件未全部上传,可重推补传`,
duration: 8000,
});
}
} finally {
this.batchSyncing = false;
}
await Promise.all(rows.map(row => api.syncKingdee(row.id)));
this.$message.success(`已同步${rows.length}条开票申请`);
this.loadTable();
},
async handleExport() {
const data = this.unwrapData(await api.getList(1, 100000, this.query));
@@ -456,28 +429,6 @@ export default {
''
);
},
lockTag(row) {
if (row.kingdeeLockStatus === 'locked') {
return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' };
}
if (row.kingdeeLockStatus === 'failed') {
if ((row.kingdeeTryCount || 0) >= 5) {
return {
show: true,
type: 'danger',
label: '锁定失败·已达上限',
tip: row.kingdeeLockFailReason || '自动补偿次数已用完,请重推「同步金蝶」立即补锁',
};
}
return {
show: true,
type: 'warning',
label: '锁定失败·补偿中',
tip: row.kingdeeLockFailReason || '系统将自动补偿重锁',
};
}
return { show: false };
},
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
+241 -33
View File
@@ -47,7 +47,7 @@
<el-col :span="6">
<el-form-item label="开票金额">
<el-input
:model-value="form.invoiceNo ? formatMoney(form.invoiceAmount) : ''"
:model-value="form.kingdeeInvoicePoolId ? formatMoney(form.invoiceAmount) : ''"
disabled
placeholder="从金蝶票据池带出"
/>
@@ -56,7 +56,7 @@
<el-col :span="6">
<el-form-item label="税额">
<el-input
:model-value="form.invoiceNo ? formatMoney(form.taxAmount) : ''"
:model-value="form.kingdeeInvoicePoolId ? formatMoney(form.taxAmount) : ''"
disabled
placeholder="从金蝶票据池带出"
/>
@@ -118,13 +118,7 @@
</section-card>
<section-card title="附件信息">
<!-- 金蝶票池认领的发票附件为 piaozone path 型,需带发票号走 A801-2 代理查看/下载 -->
<vehicle-attachment-table
v-model="form.attachments"
:readonly="readonly"
show-view-actions
:kingdee-invoice-no="form.invoiceNo"
/>
<vehicle-attachment-table v-model="form.attachments" :readonly="readonly" />
</section-card>
<section-card title="备注">
@@ -155,13 +149,62 @@
</div>
</el-form>
<kingdee-invoice-pool-dialog
v-model="invoiceDialog.visible"
title="查询金蝶票据池"
:initial-invoice-no="form.invoiceNo"
:query-api="apiQueryInvoicePool"
@confirm="handleInvoicePicked"
/>
<el-dialog v-model="invoiceDialog.visible" title="查询金蝶票据池" width="86%" append-to-body>
<div class="receipt-form-page__dialog-search">
<el-input
v-model="invoiceDialog.keyword"
clearable
placeholder="发票号码、开票单位或受票单位"
@keyup.enter="loadInvoicePool"
/>
<el-button type="primary" @click="loadInvoicePool">查询</el-button>
</div>
<el-table
v-loading="invoiceDialog.loading"
:data="invoiceDialog.rows"
border
empty-text="暂无票据数据"
highlight-current-row
@current-change="invoiceDialog.current = $event"
@row-dblclick="confirmInvoice"
>
<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="130" />
<el-table-column prop="issuerName" label="开票单位" min-width="170" />
<el-table-column prop="receiverName" label="受票单位" min-width="170" />
<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="100" 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 prop="kingdeeBillNo" label="金蝶单据号" min-width="150" />
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click.stop="confirmInvoice(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<div class="receipt-form-page__dialog-pagination">
<el-pagination
v-model:current-page="invoiceDialog.page.current"
v-model:page-size="invoiceDialog.page.size"
:total="invoiceDialog.page.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="refreshInvoicePoolPage"
@size-change="handleInvoicePoolSizeChange"
/>
</div>
<template #footer>
<el-button @click="invoiceDialog.visible = false">取消</el-button>
<el-button type="primary" @click="confirmInvoice()">确定</el-button>
</template>
</el-dialog>
<el-dialog
v-model="settlementDialog.visible"
@@ -216,13 +259,82 @@
import { Search } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/invoiceReceipt';
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { getMkPublicDetail } from '@/api/mk-process';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
const mockInvoiceTypes = ['增值税专用发票', '增值税普通发票'];
const mockTaxRates = [3, 6, 9, 13];
const mockBankNames = ['中国工商银行', '中国农业银行', '中国银行', '中国建设银行', '招商银行'];
const randomDigits = length =>
Array.from({ length }, () => Math.floor(Math.random() * 10)).join('');
const formatMockDate = date => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const customerName = customer =>
customer.fullName || customer.shortName || customer.customerCode || `客商${customer.id}`;
const customerEmail = (customer, index) => {
const prefix = String(customer.customerCode || customer.id || `customer${index + 1}`)
.replace(/[^a-zA-Z0-9_-]/g, '')
.toLowerCase();
return `finance@${prefix || `customer${index + 1}`}.example.com`;
};
const createInvoicePoolMockRows = (customers, counterpartyName, departmentEmail) => {
const rows = [];
const baseId = Date.now();
customers.forEach((customer, customerIndex) => {
const name = customerName(customer);
['issuer', 'receiver'].forEach((role, roleIndex) => {
const count = 3 + Math.floor(Math.random() * 5);
for (let index = 0; index < count; index += 1) {
const rowIndex = rows.length;
const invoiceDateValue = new Date();
invoiceDateValue.setDate(invoiceDateValue.getDate() - Math.floor(Math.random() * 90));
const invoiceDate = formatMockDate(invoiceDateValue);
const taxRate = mockTaxRates[(customerIndex + roleIndex + index) % mockTaxRates.length];
const invoiceAmount = Math.floor(10000 + Math.random() * 190000);
const taxAmount = Number(
((invoiceAmount * Number(taxRate)) / (100 + Number(taxRate))).toFixed(2)
);
const bankName = mockBankNames[(customerIndex + index) % mockBankNames.length];
rows.push({
id: baseId + rowIndex,
invoiceNo: `25${randomDigits(18)}`,
invoiceDate,
invoiceType: mockInvoiceTypes[(customerIndex + index) % mockInvoiceTypes.length],
taxRate,
invoiceAmount,
taxAmount,
receiverName: role === 'receiver' ? name : counterpartyName,
issuerName: role === 'issuer' ? name : counterpartyName,
bankName,
bankAccount: randomDigits(19),
issuingBank: `${bankName}营业部`,
phone: customer.contactPhone || '',
customerEmails: customerEmail(customer, customerIndex),
departmentEmails: departmentEmail || '',
attachmentsJson: '[]',
kingdeeBillNo: `KD-FP-${invoiceDate.replaceAll('-', '')}${randomDigits(5)}`,
kingdeeStatus: 'unsynced',
});
}
});
});
return rows;
};
const emptyForm = () => ({
id: null,
kingdeeInvoicePoolId: null,
invoiceNo: '',
invoiceDate: '',
invoiceType: '',
@@ -234,6 +346,8 @@ const emptyForm = () => ({
bankName: '',
bankAccount: '',
issuingBank: '',
kingdeeBillNo: '',
kingdeeStatus: 'unsynced',
phone: '',
customerEmails: [],
departmentEmails: [],
@@ -249,17 +363,24 @@ const emptyForm = () => ({
export default {
name: 'InvoiceReceiptForm',
components: {
KingdeeInvoicePoolDialog,
VehicleAttachmentTable,
},
data() {
return {
Search,
form: emptyForm(),
invoicePoolMockRows: [],
invoicePoolMockLoaded: false,
customerEmailOptions: [],
departmentEmailOptions: [],
invoiceDialog: {
visible: false,
loading: false,
keyword: '',
sourceRows: [],
rows: [],
current: null,
page: { current: 1, size: 10, total: 0 },
},
settlementDialog: {
visible: false,
@@ -305,12 +426,6 @@ export default {
this.initialize();
},
methods: {
apiQueryInvoicePool(params) {
return api.queryInvoicePool(params);
},
handleInvoicePicked(rows) {
this.confirmInvoice(rows[0]);
},
hasPermission(code) {
return this.permission?.[code] !== false;
},
@@ -368,7 +483,7 @@ export default {
};
},
validateInvoice(rule, value, callback) {
if (!value) {
if (!value || !this.form.kingdeeInvoicePoolId) {
callback(new Error('请选择金蝶票据池发票'));
return;
}
@@ -394,17 +509,99 @@ export default {
}
callback();
},
openInvoiceDialog() {
async openInvoiceDialog() {
this.invoiceDialog.visible = true;
this.invoiceDialog.keyword = this.form.invoiceNo || '';
await this.loadInvoicePool();
},
async loadInvoicePool() {
this.invoiceDialog.loading = true;
this.invoiceDialog.page.current = 1;
this.invoiceDialog.current = null;
try {
if (!this.recordId) {
if (!this.invoicePoolMockLoaded) await this.loadInvoicePoolMockRows();
this.invoiceDialog.sourceRows = this.filterInvoicePoolMockRows(
this.invoiceDialog.keyword
);
this.refreshInvoicePoolPage();
return;
}
const data = this.unwrapData(await api.getInvoicePool(this.invoiceDialog.keyword));
this.invoiceDialog.sourceRows = Array.isArray(data) ? data : data?.records || [];
this.refreshInvoicePoolPage();
} finally {
this.invoiceDialog.loading = false;
}
},
async loadInvoicePoolMockRows() {
const pageSize = 500;
const firstData = this.unwrapData(await getCustomerList(1, pageSize, {}));
const firstRows = Array.isArray(firstData) ? firstData : firstData?.records || [];
const total = Number(firstData?.total || firstRows.length);
const pageCount = Math.ceil(total / pageSize);
const remainingPages = await Promise.all(
Array.from({ length: Math.max(pageCount - 1, 0) }, (_, index) =>
getCustomerList(index + 2, pageSize, {})
)
);
const customers = [
...firstRows,
...remainingPages.flatMap(response => {
const data = this.unwrapData(response);
return Array.isArray(data) ? data : data?.records || [];
}),
].filter(
(customer, index, rows) =>
rows.findIndex(item => String(item.id) === String(customer.id)) === index
);
const userInfo = this.$store.getters.userInfo || {};
const counterpartyName = userInfo.deptName || userInfo.dept_name || '本企业';
this.invoicePoolMockRows = createInvoicePoolMockRows(
customers,
counterpartyName,
userInfo.email || ''
);
this.invoicePoolMockLoaded = true;
},
refreshInvoicePoolPage() {
const page = this.invoiceDialog.page;
const total = this.invoiceDialog.sourceRows.length;
const maxPage = Math.max(Math.ceil(total / page.size), 1);
if (page.current > maxPage) page.current = maxPage;
const start = (page.current - 1) * page.size;
page.total = total;
this.invoiceDialog.current = null;
this.invoiceDialog.rows = this.invoiceDialog.sourceRows.slice(start, start + page.size);
},
handleInvoicePoolSizeChange() {
this.invoiceDialog.page.current = 1;
this.refreshInvoicePoolPage();
},
filterInvoicePoolMockRows(keyword) {
const normalizedKeyword = String(keyword || '')
.trim()
.toLowerCase();
if (!normalizedKeyword) return this.invoicePoolMockRows.map(item => ({ ...item }));
return this.invoicePoolMockRows
.filter(item =>
[item.invoiceNo, item.issuerName, item.receiverName].some(value =>
String(value || '')
.toLowerCase()
.includes(normalizedKeyword)
)
)
.map(item => ({ ...item }));
},
confirmInvoice(row, options = {}) {
const selected = row;
const selected = row?.id ? row : this.invoiceDialog.current;
if (!selected) {
this.$message.warning('请选择一张金蝶票据池发票');
return;
}
const preserveUserInput = options.preserveUserInput === true;
Object.assign(this.form, {
kingdeeInvoicePoolId: selected.id,
invoiceNo: selected.invoiceNo,
invoiceDate: selected.invoiceDate,
invoiceType: selected.invoiceType,
@@ -416,6 +613,8 @@ export default {
bankName: selected.bankName,
bankAccount: selected.bankAccount,
issuingBank: selected.issuingBank,
kingdeeBillNo: selected.kingdeeBillNo || '',
kingdeeStatus: selected.kingdeeStatus || 'unsynced',
phone: preserveUserInput ? this.form.phone : selected.phone || '',
customerEmails: preserveUserInput
? this.form.customerEmails
@@ -526,6 +725,7 @@ export default {
payload() {
return {
id: this.form.id,
kingdeeInvoicePoolId: this.form.kingdeeInvoicePoolId,
invoiceNo: this.form.invoiceNo,
invoiceDate: this.form.invoiceDate,
invoiceType: this.form.invoiceType,
@@ -537,6 +737,8 @@ export default {
bankName: this.form.bankName,
bankAccount: this.form.bankAccount,
issuingBank: this.form.issuingBank,
kingdeeBillNo: this.form.kingdeeBillNo,
kingdeeStatus: this.form.kingdeeStatus,
phone: this.form.phone,
customerEmails: this.form.customerEmails.join(';'),
departmentEmails: this.form.departmentEmails.join(';'),
@@ -580,12 +782,13 @@ export default {
this.$message.warning('请先选择金蝶票据池发票');
return;
}
const data = this.unwrapData(
await api.queryInvoicePool({ current: 1, size: 1, invoiceNo: this.form.invoiceNo })
);
const invoice = (data.records || []).find(item => item.invoiceNo === this.form.invoiceNo);
const data = this.recordId
? this.unwrapData(await api.getInvoicePool(this.form.invoiceNo))
: this.filterInvoicePoolMockRows(this.form.invoiceNo);
const rows = Array.isArray(data) ? data : data?.records || [];
const invoice = rows.find(item => item.invoiceNo === this.form.invoiceNo);
if (!invoice) {
this.$message.warning('金蝶票池未查询到该发票');
this.$message.warning('金蝶票据池中未查询到该发票');
return;
}
if (!this.confirmInvoice(invoice, { preserveUserInput: true })) return;
@@ -610,7 +813,7 @@ export default {
padding-bottom: 72px;
}
.receipt-form-page__form :deep(.el-form-item) {
margin-bottom: 14px;
margin-bottom: 24px;
}
.receipt-form-page__form :deep(.el-select),
.receipt-form-page__form :deep(.el-input-number) {
@@ -648,6 +851,11 @@ export default {
.receipt-form-page__dialog-search .el-input {
width: 360px;
}
.receipt-form-page__dialog-pagination {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
.receipt-form-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
+13 -30
View File
@@ -40,10 +40,10 @@
/>
</el-select>
</el-form-item>
<el-form-item v-if="searchExpanded" label="金蝶状态">
<el-select v-model="query.kingdeePushStatus" clearable placeholder="请选择">
<el-form-item v-if="searchExpanded" label="金蝶单据状态">
<el-select v-model="query.kingdeeStatus" clearable placeholder="请选择">
<el-option
v-for="item in kingdeePushStatusOptions"
v-for="item in kingdeeStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
@@ -70,8 +70,7 @@
v-if="hasPermission('invoice_receipt_sync')"
type="primary"
plain
:loading="pushLoading"
@click="handlePushKingdee"
@click="handleBatchSync"
>批量同步</el-button
>
<el-button v-if="hasPermission('invoice_receipt_add')" type="primary" @click="openCreate"
@@ -150,7 +149,7 @@
v-if="
mkEnabled &&
hasPermission('invoice_receipt_approve') &&
row.approvalStatus === 'reviewing'
['reviewing', 'approved'].includes(row.approvalStatus)
"
type="primary"
@click="handleMkAudit(row)"
@@ -219,7 +218,7 @@ const emptyQuery = () => ({
deptName: '',
deptPath: [],
approvalStatus: '',
kingdeePushStatus: '',
kingdeeStatus: '',
});
export default {
@@ -233,12 +232,11 @@ export default {
query: emptyQuery(),
searchExpanded: false,
approvalStatusOptions: api.approvalStatusOptions,
kingdeePushStatusOptions: api.kingdeePushStatusOptions,
kingdeeStatusOptions: api.kingdeeStatusOptions,
columns: invoiceReceiptTableColumns,
rows: [],
selection: [],
loading: false,
pushLoading: false,
page: { current: 1, size: 10, total: 0 },
mkEnabled: false,
};
@@ -358,29 +356,15 @@ export default {
this.$message.success('作废成功');
this.loadTable();
},
async handlePushKingdee() {
if (this.pushLoading) return;
async handleBatchSync() {
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
if (!rows.length) {
this.$message.warning('请选择至少一条审批通过的收票登记');
return;
}
await this.$confirm(`确认将选中的 ${rows.length} 条收票登记同步至金蝶?`, '提示', {
type: 'warning',
});
this.pushLoading = true;
try {
const summary = this.unwrapData(await api.pushKingdee(rows.map(row => row.id)));
await this.loadTable();
// 附件联带同步提示:推送成功但附件未全部上传时在结果弹窗追加说明,重推即自动补传
const idSet = new Set(rows.map(row => String(row.id)));
const pendingCount = (this.rows || []).filter(row => idSet.has(String(row.id))
&& ['pending', 'partial'].includes(row.attachmentSyncStatus)).length;
const attachmentTip = pendingCount ? `\n其中${pendingCount}单附件未全部上传,可重推补传` : '';
this.$alert((summary || '推送完成') + attachmentTip, '推送结果', { confirmButtonText: '知道了' });
} finally {
this.pushLoading = false;
}
await Promise.all(rows.map(row => api.syncKingdee(row.id)));
this.$message.success(`已同步${rows.length}条收票登记`);
this.loadTable();
},
async handleExport() {
const data = this.unwrapData(await api.getList(1, 100000, this.query));
@@ -397,9 +381,8 @@ export default {
当前处理人: item.currentProcessor,
创建人: item.createUserName,
创建时间: item.createTime,
金蝶单据号: item.kingdeeReceivedInvoiceNo,
金蝶状态: item.kingdeePushStatusName,
失败原因: item.kingdeePushMsg,
金蝶单据号: item.kingdeeBillNo,
金蝶单据状态: item.kingdeeStatusName,
}));
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(exportRows), '收票登记');
+24 -91
View File
@@ -100,7 +100,7 @@
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="item.contractName || item.contractNo"
:label="formatContractOptionLabel(item)"
:value="item.id"
/>
</el-select>
@@ -281,18 +281,8 @@
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row, $index }">
<div class="payment-form-page__invoice-actions">
<el-link
type="primary"
:disabled="kingdeeAttachmentBusy"
@click="viewInvoiceAttachment(row)"
>查看</el-link
>
<el-link
type="primary"
:disabled="kingdeeAttachmentBusy"
@click="downloadInvoiceAttachment(row)"
>下载</el-link
>
<el-link type="primary" @click="viewInvoiceAttachment(row)">查看</el-link>
<el-link type="primary" @click="downloadInvoiceAttachment(row)">下载</el-link>
<el-link
v-if="!readonly"
type="danger"
@@ -355,19 +345,22 @@
value="special_approval" /><el-option
label="其他"
value="other" /></el-select></template></el-table-column
><el-table-column label="文件名" min-width="220"
><el-table-column
label="文件名"
min-width="220"
show-overflow-tooltip
class-name="is-ellipsis-link"
><template #default="{ row }"
><div
class="payment-form-page__attachment-file-name-cell"
:title="attachmentFileName(row) || '-'"
>
<span
class="payment-form-page__attachment-file-name"
:class="{ 'is-disabled': !attachmentFileUrl(row) }"
@click="attachmentFileUrl(row) && previewAttachment(row)"
>{{ attachmentFileName(row) || '-' }}</span
>
</div></template
><span
class="attachment-file-name"
:class="{ 'is-disabled': !attachmentFileUrl(row) }"
role="button"
tabindex="0"
@click="attachmentFileUrl(row) && previewAttachment(row)"
@keydown.enter="attachmentFileUrl(row) && previewAttachment(row)"
@keydown.space.prevent="attachmentFileUrl(row) && previewAttachment(row)"
>{{ attachmentFileName(row) || '-' }}</span
></template
></el-table-column
><el-table-column label="附件描述" min-width="220"
><template #default="{ row }"
@@ -671,13 +664,9 @@ import {
getList as getCustomerArchiveList,
} from '@/api/vehicle/customer-archive';
import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer';
import { formatContractOptionLabel } from '@/utils/contract';
import { downloadFileByUrl } from '@/utils/util';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import {
downloadKingdeeInvoiceFile,
isKingdeePathAttachment,
viewKingdeeInvoiceFile,
} from '@/utils/kingdee-attachment';
import PdfPreview from '@/components/pdf-preview/main.vue';
const attachmentViewerPlugins = [
@@ -758,8 +747,6 @@ export default {
form: emptyForm(),
amountSyncing: false,
transferPayload: null,
// 金蝶侧发票附件查看/下载进行中(行组级 loading 守卫)
kingdeeAttachmentBusy: false,
referenceRows: [],
paymentRecords: [],
referenceVisible: false,
@@ -1038,6 +1025,7 @@ export default {
this.initialize();
},
methods: {
formatContractOptionLabel,
hasPermission(code) {
return this.permission?.[code] !== false;
},
@@ -1601,19 +1589,8 @@ export default {
this.attachmentFileName(this.invoiceAttachment(row)) || `${row.invoiceNo || '发票'}.pdf`
);
},
async viewInvoiceAttachment(row) {
viewInvoiceAttachment(row) {
const attachment = this.invoiceAttachment(row);
// 金蝶侧附件(path 型)经后端代理 A801-2 预览,OSS 直链维持本地预览组件
if (isKingdeePathAttachment(attachment)) {
if (this.kingdeeAttachmentBusy) return;
this.kingdeeAttachmentBusy = true;
try {
await viewKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row));
} finally {
this.kingdeeAttachmentBusy = false;
}
return;
}
if (!this.attachmentFileUrl(attachment)) {
this.$message.warning('当前发票暂无可查看附件');
return;
@@ -1623,20 +1600,8 @@ export default {
[attachment]
);
},
async downloadInvoiceAttachment(row) {
const attachment = this.invoiceAttachment(row);
// 金蝶侧附件(path 型)经后端代理 A801-2 下载,OSS 直链维持前端直连下载
if (isKingdeePathAttachment(attachment)) {
if (this.kingdeeAttachmentBusy) return;
this.kingdeeAttachmentBusy = true;
try {
await downloadKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row));
} finally {
this.kingdeeAttachmentBusy = false;
}
return;
}
const url = this.attachmentFileUrl(attachment);
downloadInvoiceAttachment(row) {
const url = this.invoiceAttachmentUrl(row);
if (!url) {
this.$message.warning('当前发票暂无可下载附件');
return;
@@ -2787,38 +2752,6 @@ export default {
justify-content: center;
gap: 8px;
}
.payment-form-page__attachment-file-name-cell {
display: block;
width: 100%;
min-width: 0;
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.payment-form-page__attachment-file-name {
display: block;
width: 100%;
overflow: hidden;
color: var(--el-text-color-regular);
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
transition: color 0.2s ease;
&:hover {
color: #409eff;
}
&.is-disabled {
color: var(--el-text-color-regular);
cursor: default;
&:hover {
color: var(--el-text-color-regular);
}
}
}
.payment-form-page__attachment-upload .vehicle-attachment-upload {
width: auto;
}
@@ -2826,7 +2759,7 @@ export default {
display: inline-flex;
}
.payment-form-page :deep(.el-form-item) {
margin-bottom: 14px;
margin-bottom: 24px;
}
.payment-form-page :deep(.el-input-number .el-input__inner) {
text-align: left;
+6 -88
View File
@@ -81,18 +81,9 @@
v-if="hasPermission('payment_application_sync')"
type="primary"
plain
:loading="syncLoading"
@click="handleSync"
>批量同步</el-button
>
<el-button
v-if="hasPermission('payment_application_sync')"
type="primary"
plain
:loading="resultSyncing"
@click="handleSyncResult"
>同步付款结果</el-button
>
<el-button
v-if="hasPermission('payment_application_add')"
type="primary"
@@ -138,12 +129,6 @@
>{{ row[column.prop] || '-' }}</el-tag
>
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
<template v-else-if="column.prop === 'kingdeeLockStatus'">
<el-tooltip v-if="lockTag(row).show" :content="lockTag(row).tip" placement="top">
<el-tag :type="lockTag(row).type" class="status-text">{{ lockTag(row).label }}</el-tag>
</el-tooltip>
<span v-else>-</span>
</template>
<span v-else>{{ displayValue(row[column.prop]) }}</span>
</template>
</el-table-column>
@@ -178,7 +163,7 @@
v-if="
mkEnabled &&
hasPermission('payment_application_approve') &&
row.approvalStatus === 'reviewing'
['reviewing', 'approved'].includes(row.approvalStatus)
"
type="primary"
@click="handleMkAudit(row)"
@@ -206,9 +191,7 @@
>
<el-link
v-if="
hasPermission('payment_application_void') &&
row.approvalStatus === 'approved' &&
row.kingdeeLockStatus !== 'locked'
hasPermission('payment_application_void') && row.approvalStatus === 'approved'
"
type="danger"
@click="handleVoid(row)"
@@ -277,8 +260,6 @@ export default {
rows: [],
selection: [],
loading: false,
syncLoading: false,
resultSyncing: false,
page: { current: 1, size: 10, total: 0 },
mkEnabled: false,
};
@@ -432,58 +413,17 @@ export default {
async handleSyncRow(row) {
const data = this.unwrapData(await api.syncKingdee(row.id));
this.$message.success(`已生成金蝶付款单:${data}`);
await this.loadTable();
this.warnAttachmentNotUploaded([row.id]);
this.loadTable();
},
async handleSync() {
if (this.syncLoading) return;
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
if (!rows.length) {
this.$message.warning('请选择至少一条审批通过的付款申请');
return;
}
this.syncLoading = true;
try {
const data = this.unwrapData(await api.syncKingdeeBatch(rows.map(row => row.id))) || [];
const failedList = data.filter(item => String(item).includes('同步失败'));
if (failedList.length) {
this.$message({
type: 'warning',
message: `同步完成:成功${data.length - failedList.length}条,失败${failedList.length}条。${failedList.join(';')}`,
duration: 8000,
});
} else {
this.$message.success(`已同步${data.length}条付款申请`);
}
await this.loadTable();
this.warnAttachmentNotUploaded(rows.map(row => row.id));
} finally {
this.syncLoading = false;
}
},
// 附件联带同步提示:同步成功但附件未全部上传时追加 warning,重推即自动补传
warnAttachmentNotUploaded(ids) {
const idSet = new Set(ids.map(String));
const pendingCount = (this.rows || []).filter(row => idSet.has(String(row.id))
&& ['pending', 'partial'].includes(row.attachmentSyncStatus)).length;
if (pendingCount) {
this.$message({
type: 'warning',
message: `其中${pendingCount}单附件未全部上传,可重推补传`,
duration: 8000,
});
}
},
async handleSyncResult() {
if (this.resultSyncing) return;
this.resultSyncing = true;
try {
const data = this.unwrapData(await api.syncKingdeeResult());
this.$message.success(data || '金蝶付款结果回写完成');
this.loadTable();
} finally {
this.resultSyncing = false;
}
await api.syncKingdeeBatch(rows.map(row => row.id));
this.$message.success(`已同步${rows.length}条付款申请`);
this.loadTable();
},
handleExport() {
const headers = this.columns.map(item => item.label);
@@ -504,28 +444,6 @@ export default {
''
);
},
lockTag(row) {
if (row.kingdeeLockStatus === 'locked') {
return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' };
}
if (row.kingdeeLockStatus === 'failed') {
if ((row.kingdeeTryCount || 0) >= 5) {
return {
show: true,
type: 'danger',
label: '锁定失败·已达上限',
tip: row.kingdeeLockFailReason || '自动补偿次数已用完,请重推「同步金蝶」立即补锁',
};
}
return {
show: true,
type: 'warning',
label: '锁定失败·补偿中',
tip: row.kingdeeLockFailReason || '系统将自动补偿重锁',
};
}
return { show: false };
},
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
+6 -46
View File
@@ -56,16 +56,6 @@
<el-form-item v-show="searchExpanded" label="所属组织">
<el-input v-model="query.claimerDeptName" clearable placeholder="请输入" />
</el-form-item>
<el-form-item v-show="searchExpanded" label="推送状态">
<el-select v-model="query.pushStatus" clearable placeholder="请选择">
<el-option
v-for="item in pushStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item v-show="searchExpanded" label="金蝶单据状态">
<el-select v-model="query.kingdeeBillStatus" clearable placeholder="请选择">
<el-option
@@ -90,11 +80,6 @@
</section-card>
<section class="claim-record-page__table-panel">
<div class="claim-record-page__toolbar">
<el-button :loading="refreshing" @click="handleRefreshKingdeeResult">
查询金蝶认领结果
</el-button>
</div>
<el-table v-loading="loading" :data="rows" border>
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
<el-table-column
@@ -122,11 +107,7 @@
<el-table-column label="操作" width="120" fixed="right" align="center">
<template #default="{ row }">
<el-link
v-if="
hasPermission('receipt_claim_record_void') &&
row.claimStatus === 'claimed' &&
row.pushStatus !== 'pushed'
"
v-if="hasPermission('receipt_claim_record_void') && row.claimStatus === 'claimed'"
type="danger"
@click="handleVoid(row)"
>作废</el-link
@@ -171,7 +152,6 @@ const emptyQuery = () => ({
claimDateRange: [],
claimerDeptName: '',
kingdeeBillStatus: '',
pushStatus: '',
});
export default {
@@ -185,11 +165,9 @@ export default {
searchExpanded: false,
claimStatusOptions: api.claimStatusOptions,
kingdeeBillStatusOptions: api.kingdeeBillStatusOptions,
pushStatusOptions: api.pushStatusOptions,
columns: receiptClaimRecordTableColumns,
rows: [],
loading: false,
refreshing: false,
page: { current: 1, size: 10, total: 0 },
};
},
@@ -204,9 +182,8 @@ export default {
},
methods: {
unwrapData(response) {
// 数字 0 是合法返回值(如回写/同步 0 条),须用 ?? 判空避免 0 被信封对象顶掉导致 Number() 得 NaN
const body = response?.data ?? response ?? {};
return body?.data ?? body;
const body = response?.data || response || {};
return body?.data || body;
},
queryParams() {
const transactionRange = this.query.transactionDateRange || [];
@@ -250,24 +227,14 @@ export default {
},
async handleVoid(row) {
await this.$confirm(
'作废后将回退本次认领金额,是否继续?',
'作废后将回退本次认领金额,并生成审核通过状态的金蝶认领冲单,是否继续?',
'作废认领记录',
{ type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }
);
await api.voidClaim(row.id);
this.$message.success('作废成功');
const kingdeeBillNo = this.unwrapData(await api.voidClaim(row.id));
this.$message.success(`作废成功,金蝶冲单号:${kingdeeBillNo}`);
await this.loadTable();
},
async handleRefreshKingdeeResult() {
this.refreshing = true;
try {
const updated = Number(this.unwrapData(await api.queryKingdeeResult()) || 0);
this.$message.success(`金蝶认领结果回写完成,更新${updated}条`);
await this.loadTable();
} finally {
this.refreshing = false;
}
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission?.[code], false);
},
@@ -312,13 +279,6 @@ export default {
.claim-record-page__table-panel {
margin-top: 8px;
}
.claim-record-page__toolbar {
display: flex;
justify-content: flex-start;
align-items: center;
gap: 8px;
padding: 12px 0;
}
.claim-record-page__pagination {
display: flex;
justify-content: flex-end;
@@ -43,9 +43,6 @@
<el-table-column label="已认领金额" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.claimedAmount) }}</template>
</el-table-column>
<el-table-column label="金蝶未认领金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.unclaimedAmount) }}</template>
</el-table-column>
<el-table-column prop="summary" label="摘要" min-width="180" show-overflow-tooltip />
</el-table>
<el-row :gutter="32" class="receipt-claim-batch-form-page__claim-info">
@@ -339,11 +336,11 @@ export default {
for (const flow of this.flows) {
const total = this.flowAllocatedTotal(flow.id);
const remaining = Math.max(
Number(flow.unclaimedAmount || 0) - Number(flow.claimedAmount || 0),
Number(flow.receiptAmount || 0) - Number(flow.claimedAmount || 0),
0
);
if (total > remaining + 0.001) {
this.$message.warning(`流水${flow.receiptNoticeNo}的累计认领金额不能超过公告未认领金额`);
this.$message.warning(`流水${flow.receiptNoticeNo}的分摊金额不能超过剩余可认领金额`);
return;
}
}
+2 -17
View File
@@ -47,14 +47,6 @@
><el-form-item label="已认领金额"
><el-input :model-value="formatMoney(form.claimedAmount)" disabled /></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="金蝶未认领金额"
><el-input :model-value="formatMoney(form.unclaimedAmount)" disabled /></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="剩余可认领金额"
><el-input :model-value="formatMoney(claimableAmount)" disabled /></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="摘要"><el-input v-model="form.summary" disabled /></el-form-item
></el-col>
@@ -205,7 +197,6 @@ const emptyForm = () => ({
transactionTime: '',
detailSerialNo: '',
claimedAmount: 0,
unclaimedAmount: 0,
claimerName: '',
claimerDeptName: '',
claimDate: '',
@@ -251,12 +242,6 @@ export default {
0
);
},
claimableAmount() {
return Math.max(
Number(this.form.unclaimedAmount || 0) - Number(this.form.claimedAmount || 0),
0
);
},
},
created() {
if (!this.batchMode) this.initialize();
@@ -346,8 +331,8 @@ export default {
this.$message.warning('分摊收款金额必须大于0');
return;
}
if (this.allocatedTotal > this.claimableAmount + 0.001) {
this.$message.warning('累计认领金额不能超过公告未认领金额(剩余可认领金额)');
if (this.allocatedTotal > amount - Number(this.form.claimedAmount || 0) + 0.001) {
this.$message.warning('本次分摊金额不能超过流水剩余可认领金额');
return;
}
for (const row of this.form.settlements)
+82 -156
View File
@@ -9,20 +9,16 @@
<el-form-item label="对方户名">
<el-input v-model="query.counterpartyName" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="对方开户行">
<el-input v-model="query.counterpartyBank" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="对方账号">
<el-input v-model="query.counterpartyAccount" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="推送状态">
<el-select v-model="query.pushStatus" clearable placeholder="请选择">
<el-option
v-for="item in pushStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-form-item v-show="searchExpanded" label="摘要">
<el-input v-model="query.summary" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="认领状态">
<el-form-item v-show="searchExpanded" label="认领状态">
<el-select v-model="query.claimStatus" clearable placeholder="请选择">
<el-option
v-for="item in claimStatusOptions"
@@ -32,7 +28,7 @@
/>
</el-select>
</el-form-item>
<el-form-item label="交易时间">
<el-form-item v-show="searchExpanded" label="交易时间">
<el-date-picker
v-model="query.transactionTimeRange"
type="datetimerange"
@@ -43,54 +39,13 @@
clearable
/>
</el-form-item>
<el-form-item v-show="searchExpanded" label="对方开户行">
<el-input v-model="query.counterpartyBank" clearable placeholder="请输入" />
</el-form-item>
<el-form-item v-show="searchExpanded" label="摘要">
<el-input v-model="query.summary" clearable placeholder="请输入" />
</el-form-item>
<el-form-item v-show="searchExpanded" label="明细流水号">
<el-input v-model="query.detailSerialNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item v-show="searchExpanded" label="金蝶状态">
<el-select v-model="query.kingdeeBillStatus" clearable placeholder="请选择">
<el-option
v-for="item in kingdeeBillStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item v-show="searchExpanded" label="收款金额">
<div class="amount-range">
<el-input-number
v-model="query.receiptAmountStart"
:controls="false"
:min="0"
placeholder="最低金额"
/>
<span>~</span>
<el-input-number
v-model="query.receiptAmountEnd"
:controls="false"
:min="0"
placeholder="最高金额"
/>
</div>
</el-form-item>
<el-form-item v-show="searchExpanded" label="认领人">
<el-input v-model="query.claimerName" clearable placeholder="请输入" />
</el-form-item>
</div>
<div class="receipt-flow-page__search-actions">
<el-button @click="searchExpanded = !searchExpanded">
{{ searchExpanded ? '折叠' : '展开' }}
</el-button>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
<el-tooltip :content="searchExpanded ? '折叠' : '展开'" placement="top">
<el-button text @click="searchExpanded = !searchExpanded">
<el-icon><component :is="searchExpanded ? ArrowUp : ArrowDown" /></el-icon>
</el-button>
</el-tooltip>
</div>
</el-form>
</section>
@@ -135,7 +90,7 @@
</el-link>
<el-tag
v-else-if="column.status"
:type="statusType(row, column)"
:type="statusType(row.claimStatus)"
class="status-text"
>
{{ displayValue(row[column.prop]) }}
@@ -144,23 +99,10 @@
<span v-else>{{ displayValue(row[column.prop]) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="170" fixed="right" align="center">
<el-table-column label="操作" width="120" fixed="right" align="center">
<template #default="{ row }">
<el-link
v-if="canClaim(row)"
type="primary"
:disabled="pushingId === row.id"
@click="openClaim(row)"
>认领</el-link
>
<el-link
v-if="canPush(row)"
type="warning"
:disabled="pushingId === row.id"
@click="handlePush(row)"
>推送金蝶</el-link
>
<span v-if="!canClaim(row) && !canPush(row)">-</span>
<el-link v-if="canClaim(row)" type="primary" @click="openClaim(row)">认领</el-link>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
@@ -211,7 +153,7 @@
{{ displayValue(detailDialog.data.detailSerialNo) }}
</el-descriptions-item>
<el-descriptions-item label="认领状态">
<el-tag :type="statusType(detailDialog.data, { prop: 'claimStatusName' })">
<el-tag :type="statusType(detailDialog.data.claimStatus)">
{{ displayValue(detailDialog.data.claimStatusName) }}
</el-tag>
</el-descriptions-item>
@@ -221,23 +163,6 @@
<el-descriptions-item label="剩余可认领金额">
{{ formatMoney(detailDialog.data.remainingAmount) }}
</el-descriptions-item>
<el-descriptions-item label="金蝶未认领金额">
{{ formatMoney(detailDialog.data.unclaimedAmount) }}
</el-descriptions-item>
<el-descriptions-item label="推送状态">
<el-tag :type="statusType(detailDialog.data, { prop: 'pushStatusName' })">
{{ displayValue(detailDialog.data.pushStatusName) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="金蝶收款单号">
{{ displayValue(detailDialog.data.kingdeeBillNo) }}
</el-descriptions-item>
<el-descriptions-item label="金蝶状态">
{{ displayValue(detailDialog.data.kingdeeBillStatusName) }}
</el-descriptions-item>
<el-descriptions-item label="推送失败原因">
{{ displayValue(detailDialog.data.pushFailReason) }}
</el-descriptions-item>
<el-descriptions-item label="摘要">
{{ displayValue(detailDialog.data.summary) }}
</el-descriptions-item>
@@ -252,9 +177,7 @@
</el-table-column>
<el-table-column prop="operatorName" label="认领人" min-width="120" align="center" />
<el-table-column label="状态变化" min-width="170" align="center">
<template #default="{ row }">
{{ row.actionType === 'push' ? row.actionName : claimStatusChange(row) }}
</template>
<template #default="{ row }">{{ claimStatusChange(row) }}</template>
</el-table-column>
<el-table-column prop="content" label="认领说明" min-width="220" />
</el-table>
@@ -267,25 +190,76 @@
</template>
<script>
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/receiptFlow';
import { receiptFlowTableColumns } from '@/option/payment/receiptFlow';
import { isAdministrator } from '@/utils/auth';
const receiptFlowMockTemplates = [
{
receiptNoticeNo: 'SKTZ20260827001',
payerName: '华东供应链管理有限公司',
receiptAmount: 48000,
counterpartyName: '上海安捷运输有限公司',
counterpartyAccount: '310000000000001001',
counterpartyBank: '中国工商银行上海分行',
summary: '运输服务款',
transactionTime: '2026-08-27 09:30:00',
detailSerialNo: 'MOCK-RF-20260827-001',
sourceUpdatedTime: '2026-08-27 09:30:00',
},
{
receiptNoticeNo: 'SKTZ20260827002',
payerName: '华东供应链管理有限公司',
receiptAmount: 26500,
counterpartyName: '江苏联运物流有限公司',
counterpartyAccount: '320100000000002002',
counterpartyBank: '中国建设银行南京分行',
summary: '物流服务款',
transactionTime: '2026-08-27 13:45:00',
detailSerialNo: 'MOCK-RF-20260827-002',
sourceUpdatedTime: '2026-08-27 13:45:00',
},
];
const randomDigits = length =>
Array.from({ length }, () => Math.floor(Math.random() * 10)).join('');
const formatMockDateTime = date => {
const pad = value => String(value).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(
date.getHours()
)}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
};
const createReceiptFlowMockRows = () => {
const count = 20 + Math.floor(Math.random() * 31);
return Array.from({ length: count }, (_, index) => {
const template = receiptFlowMockTemplates[index % receiptFlowMockTemplates.length];
const transactionDate = new Date();
transactionDate.setDate(transactionDate.getDate() - Math.floor(Math.random() * 7));
transactionDate.setHours(8 + Math.floor(Math.random() * 10), Math.floor(Math.random() * 60), 0);
const transactionTime = formatMockDateTime(transactionDate);
const dateCode = transactionTime.slice(0, 10).replaceAll('-', '');
return {
...template,
receiptNoticeNo: `SKTZ${dateCode}${randomDigits(5)}`,
receiptAmount: Math.floor(10000 + Math.random() * 90000),
counterpartyAccount: `${template.counterpartyAccount.slice(0, -6)}${randomDigits(6)}`,
transactionTime,
detailSerialNo: `MOCK-RF-${dateCode}-${index + 1}${randomDigits(4)}`,
sourceUpdatedTime: transactionTime,
};
});
};
const emptyQuery = () => ({
receiptNoticeNo: '',
counterpartyName: '',
counterpartyBank: '',
counterpartyAccount: '',
summary: '',
detailSerialNo: '',
claimStatus: '',
pushStatus: '',
kingdeeBillStatus: '',
receiptAmountStart: undefined,
receiptAmountEnd: undefined,
claimerName: '',
transactionTimeRange: [],
});
@@ -293,18 +267,13 @@ export default {
name: 'ReceiptFlow',
data() {
return {
ArrowDown,
ArrowUp,
query: emptyQuery(),
claimStatusOptions: api.claimStatusOptions,
pushStatusOptions: api.pushStatusOptions,
kingdeeBillStatusOptions: api.kingdeeBillStatusOptions,
columns: receiptFlowTableColumns,
rows: [],
selectedRows: [],
loading: false,
syncing: false,
pushingId: null,
searchExpanded: false,
page: { current: 1, size: 10, total: 0 },
detailDialog: { visible: false, loading: false, data: {}, claimRecords: [] },
@@ -321,9 +290,8 @@ export default {
},
methods: {
unwrapData(response) {
// 数字 0 是合法返回值(如回写/同步 0 条),须用 ?? 判空避免 0 被信封对象顶掉导致 Number() 得 NaN
const body = response?.data ?? response ?? {};
return body?.data ?? body;
const body = response?.data || response || {};
return body?.data || body;
},
async loadTable() {
this.loading = true;
@@ -359,8 +327,9 @@ export default {
async handleSync() {
this.syncing = true;
try {
const syncedCount = Number(this.unwrapData(await api.sync()) || 0);
this.$message.success(`流水同步完成,共同步${syncedCount}条认领公告`);
const mockRows = createReceiptFlowMockRows();
const syncedCount = Number(this.unwrapData(await api.sync({ flows: mockRows })) || 0);
this.$message.success(`流水同步完成,共同步${syncedCount}条`);
await this.loadTable();
} finally {
this.syncing = false;
@@ -419,41 +388,8 @@ export default {
canClaim(row) {
return this.hasPermission('receipt_flow_claim') && Number(row.remainingAmount || 0) > 0;
},
canPush(row) {
return (
this.hasPermission('receipt_flow_push') &&
!!row.billId &&
row.claimStatus === 'claimed' &&
row.pushStatus !== 'pushed'
);
},
async handlePush(row) {
try {
await this.$confirm(
`确认将流水【${row.receiptNoticeNo || row.detailSerialNo || ''}】推送金蝶生成收款单?`,
'推送金蝶',
{ type: 'warning' }
);
} catch {
return;
}
this.pushingId = row.id;
try {
const data = this.unwrapData(await api.pushKingdee(row.id));
const billNo = data?.kingdeeBillNo;
this.$message.success(billNo ? `推送成功,金蝶收款单号:${billNo}` : '推送成功');
await this.loadTable();
} finally {
this.pushingId = null;
}
},
statusType(row, column) {
if (column?.prop === 'pushStatusName') {
return { pushed: 'success', failed: 'danger' }[row.pushStatus] || 'info';
}
return (
{ claimed: 'success', partial: 'warning', unclaimed: 'info' }[row.claimStatus] || ''
);
statusType(status) {
return { claimed: 'success', partial: 'warning', unclaimed: 'info' }[status] || '';
},
claimStatusChange(row) {
const label = status =>
@@ -486,14 +422,7 @@ export default {
}
.receipt-flow-page__search :deep(.el-input),
.receipt-flow-page__search :deep(.el-select),
.receipt-flow-page__search :deep(.el-date-editor),
.receipt-flow-page__search :deep(.el-input-number) {
width: 100%;
}
.receipt-flow-page__search .amount-range {
display: flex;
align-items: center;
gap: 4px;
.receipt-flow-page__search :deep(.el-date-editor) {
width: 100%;
}
.receipt-flow-page__search-actions {
@@ -523,9 +452,6 @@ export default {
.receipt-flow-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.receipt-flow-page :deep(.el-table .cell .el-link + .el-link) {
margin-left: 12px;
}
.receipt-flow-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
@@ -22,7 +22,6 @@
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择"
@change="fetchBfiExchangeRate"
/>
<el-select
v-else-if="field.type === 'project' && editable"
@@ -53,7 +52,7 @@
<el-option
v-for="item in contracts"
:key="item.id"
:label="`${item.contractNo} / ${item.contractName}`"
:label="formatContractOptionLabel(item)"
:value="item.id"
/>
</el-select>
@@ -299,15 +298,23 @@
<span v-else>{{ attachmentTypeName(row.type) }}</span>
</template>
</el-table-column>
<el-table-column label="文件名" min-width="220" align="left" show-overflow-tooltip>
<el-table-column
label="文件名"
min-width="220"
align="left"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<el-link
:disabled="!getAttachmentUrl(row)"
type="primary"
@click="previewAttachment(row)"
>
{{ row.originalName || row.name || '-' }}
</el-link>
<span
class="attachment-file-name"
:class="{ 'is-disabled': !getAttachmentUrl(row) }"
role="button"
tabindex="0"
@click="getAttachmentUrl(row) && previewAttachment(row)"
@keydown.enter="getAttachmentUrl(row) && previewAttachment(row)"
@keydown.space.prevent="getAttachmentUrl(row) && previewAttachment(row)"
>{{ row.originalName || row.name || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="uploadUserName" label="上传人" min-width="120" align="center">
@@ -417,61 +424,78 @@
<span v-else>{{ formatMoney(row.matchedAmount) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center">
<el-table-column label="操作" width="220" align="center">
<template #default="{ row, $index }">
<div class="formal-editor__links">
<el-link
type="primary"
:disabled="kingdeeAttachmentBusy"
@click="viewInvoiceAttachment(row)"
>查看</el-link
>
<el-link
type="primary"
:disabled="kingdeeAttachmentBusy"
@click="downloadInvoiceAttachment(row)"
>下载</el-link
>
<el-link type="primary" @click="viewInvoiceAttachment(row)">查看</el-link>
<el-link type="primary" @click="downloadInvoiceAttachment(row)">下载</el-link>
<el-link v-if="editable" type="danger" @click="removeInvoice($index)">删除</el-link>
</div>
</template>
</el-table-column>
</el-table>
<div v-if="issuedInvoices.length" class="formal-editor__issued-invoices">
<div class="formal-editor__issued-title">
开票发票(本结算单开票申请经金蝶开票回写,文件由金蝶发票云直链提供)
</div>
<el-table :data="issuedInvoices" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="invoiceNo" label="发票号" min-width="150" align="center" />
<el-table-column prop="invoiceDate" label="开票日期" min-width="130" align="center">
<template #default="{ row }">{{ displayValue(row.invoiceDate) }}</template>
</el-table-column>
<el-table-column prop="invoiceAmount" label="价税合计" min-width="130" align="center">
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right" align="center">
<template #default="{ row }">
<div class="formal-editor__links">
<el-link type="primary" @click="viewIssuedInvoice(row)">查看</el-link>
<el-link type="primary" @click="downloadIssuedInvoice(row)">下载</el-link>
</div>
</template>
</el-table-column>
</el-table>
</div>
</section-card>
<kingdee-invoice-pool-dialog
<el-dialog
v-model="invoiceClaimDialog.visible"
title="查询可认领发票"
confirm-text="确认认领"
multiple
:initial-invoice-no="String(invoiceClaimNo || '').trim()"
:query-api="invoicePoolQueryApi"
@confirm="handleInvoiceClaimConfirm"
/>
width="92%"
append-to-body
destroy-on-close
>
<el-table
ref="invoiceClaimTable"
v-loading="invoiceClaimDialog.loading"
:data="invoiceClaimPagedRows"
row-key="invoiceNo"
border
height="520"
@selection-change="invoiceClaimDialog.selected = $event"
>
<el-table-column
type="selection"
width="52"
fixed="left"
align="center"
reserve-selection
/>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="invoiceNo" label="发票号" min-width="180" align="center" />
<el-table-column prop="invoiceDate" label="开票日期" min-width="130" align="center" />
<el-table-column prop="invoiceType" label="发票类型" min-width="150" align="center" />
<el-table-column prop="issuerName" label="开票单位" min-width="180" align="center" />
<el-table-column prop="receiverName" label="受票单位" min-width="180" align="center" />
<el-table-column
prop="invoiceAmount"
label="发票金额(含税)"
min-width="160"
align="right"
>
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
</el-table-column>
<el-table-column prop="matchedAmount" label="认领金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.matchedAmount) }}</template>
</el-table-column>
</el-table>
<div class="formal-editor__pagination">
<el-pagination
v-model:current-page="invoiceClaimDialog.page.current"
v-model:page-size="invoiceClaimDialog.page.size"
:total="invoiceClaimDialog.page.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
/>
</div>
<template #footer>
<el-button @click="invoiceClaimDialog.visible = false">取消</el-button>
<el-button
type="primary"
:loading="invoiceClaimDialog.confirming"
@click="confirmInvoiceClaims"
>确认认领</el-button
>
</template>
</el-dialog>
<section-card
v-if="readonly"
@@ -942,7 +966,6 @@ import {
getNextNo,
getReceiptClaims,
claimInvoices as claimInvoicesApi,
queryInvoicePool,
save,
} from '@/api/settlement/formalSettlement';
import {
@@ -950,7 +973,6 @@ import {
getDetailFees as getPreSettlementDetailFees,
} from '@/api/settlement/preSettlement';
import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail';
import { getBfiExchangeRate } from '@/api/base/currency';
import {
createFormalSettlementForm,
formalSettlementFormFields,
@@ -966,18 +988,13 @@ import { getDictionary } from '@/api/system/dictbiz';
import { h } from 'vue';
import { InfoFilled } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import { formatContractOptionLabel } from '@/utils/contract';
import { downloadFileByUrl } from '@/utils/util';
import {
downloadKingdeeInvoiceFile,
isKingdeePathAttachment,
viewKingdeeInvoiceFile,
} from '@/utils/kingdee-attachment';
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
import * as XLSX from 'xlsx';
export default {
name: 'FormalSettlementEditor',
components: { InfoFilled, KingdeeInvoicePoolDialog },
components: { InfoFilled },
props: {
modelValue: Boolean,
recordId: [String, Number],
@@ -1009,16 +1026,15 @@ export default {
attachments: [],
selectedAttachmentRows: [],
invoices: [],
// 开票申请经金蝶回写的发票结果(只读展示,含板式文件/数电PDF下载地址)
issuedInvoices: [],
invoiceClaimNo: '',
invoiceClaimLoading: false,
// 金蝶侧发票附件查看/下载进行中(行组级 loading 守卫)
kingdeeAttachmentBusy: false,
invoiceClaimDialog: {
visible: false,
selected: [],
loading: false,
confirming: false,
rows: [],
selected: [],
page: { current: 1, size: 10, total: 0 },
},
attachmentUploadFiles: [],
attachmentTypeOptions: [],
@@ -1172,6 +1188,11 @@ export default {
});
return Array.from(names);
},
invoiceClaimPagedRows() {
const { current, size } = this.invoiceClaimDialog.page;
const start = (current - 1) * size;
return this.invoiceClaimDialog.rows.slice(start, start + size);
},
},
watch: {
modelValue: {
@@ -1203,6 +1224,7 @@ export default {
},
},
methods: {
formatContractOptionLabel,
async loadFormalDetail(id) {
const response = this.publicMode
? await getMkPublicDetail('formal-settlement', id)
@@ -1232,7 +1254,6 @@ export default {
this.attachments = this.parseAttachments(data.attachmentsJson);
this.selectedAttachmentRows = [];
this.invoices = data.invoices || [];
this.issuedInvoices = data.issuedInvoices || [];
this.sortAttachments();
this.contracts = this.filterContracts(this.allContracts, this.form.projectId);
},
@@ -1262,9 +1283,9 @@ export default {
this.attachments = [];
this.selectedAttachmentRows = [];
this.invoices = [];
this.issuedInvoices = [];
this.invoiceClaimNo = '';
this.invoiceClaimDialog.visible = false;
this.invoiceClaimDialog.rows = [];
this.invoiceClaimDialog.selected = [];
this.attachmentUploadFiles = [];
this.billingRuleDialog = {
@@ -1288,7 +1309,6 @@ export default {
if (this.initialData) await this.applyInitialData();
Object.assign(this.form, newRecordAudit);
await this.refreshFormalSettlementNo();
await this.fetchBfiExchangeRate();
return;
}
this.loading = true;
@@ -1523,7 +1543,6 @@ export default {
(settlementType === 'receivable' ? contract.partyA : contract.partyB),
settlementType,
settlementTypeName: settlementType === 'receivable' ? '应收' : '应付',
currency: contract.settlementCurrency || 'RMB',
});
this.sources = [];
this.details = [];
@@ -1531,23 +1550,6 @@ export default {
this.form.sourcePreSettlementIds = [];
this.form.sourceDetailIds = [];
if (refreshSettlementNo) this.refreshFormalSettlementNo();
if (this.form.currency !== 'RMB') this.fetchBfiExchangeRate();
},
async fetchBfiExchangeRate() {
const currency = this.form.currency;
if (!currency || currency === 'RMB') return;
if (!this.form.exchangeRateDate) return;
try {
const { data } = await getBfiExchangeRate(currency, this.form.exchangeRateDate);
const rateData = data?.data;
if (rateData?.excval != null) {
this.form.exchangeRate = Number(rateData.excval);
} else {
this.$message.warning(`未查询到 ${currency} 的BFI汇率数据`);
}
} catch (e) {
this.$message.warning('查询BFI汇率失败,请手动输入结算汇率');
}
},
openCandidateDialog() {
this.candidate.visible = true;
@@ -2272,19 +2274,79 @@ export default {
this.adjust.saving = false;
}
},
claimInvoices() {
this.invoiceClaimLoading = true;
try {
const keyword = String(this.invoiceClaimNo || '').trim();
const defaultNo = `MOCK${this.$dayjs().format('YYYYMMDD')}`;
const baseNo = (keyword || defaultNo).slice(0, 29);
const numbers = [baseNo, `${baseNo}-02`, `${baseNo}-03`];
const invoiceAmounts = [1000, 2000, 3000];
const availableAmounts = [1000, 1000, 3000];
const preferredMatchedAmounts = [1000, 1000, 2000];
let remainingAmount = Math.max(
Number(this.form.settlementAmount || this.summaryTotal || 0) -
this.invoices.reduce((total, item) => total + Number(item.matchedAmount || 0), 0),
0
);
const existingNumbers = new Set(this.invoices.map(item => String(item.invoiceNo)));
const mockRows = numbers
.map((invoiceNo, index) => {
const matchedAmount = Math.min(
preferredMatchedAmounts[index],
availableAmounts[index],
remainingAmount
);
remainingAmount = Number((remainingAmount - matchedAmount).toFixed(2));
return {
invoiceNo,
invoiceDate: this.$dayjs().subtract(index, 'day').format('YYYY-MM-DD'),
invoiceType: index === 1 ? '增值税普通发票' : '增值税专用发票',
taxRate: [3, 6, 9][index],
invoiceAmount: invoiceAmounts[index],
availableInvoiceAmount: availableAmounts[index],
matchedAmount,
attachmentJson: '',
mockData: true,
};
})
.filter(item => !existingNumbers.has(item.invoiceNo));
if (!mockRows.length) {
this.$message.warning('当前发票号码的Mock数据已认领');
return;
}
this.invoices = [...this.invoices, ...mockRows];
this.$message.success(`已生成${mockRows.length}条Mock发票数据`);
} finally {
this.invoiceClaimLoading = false;
}
},
openInvoiceClaimDialog() {
const stamp = Date.now();
this.invoiceClaimDialog.loading = true;
this.invoiceClaimDialog.rows = Array.from({ length: 80 }, (_, index) => {
const amount = Math.floor(1000 + Math.random() * 49000);
return {
invoiceNo: `MOCK${stamp}${String(index + 1).padStart(3, '0')}`.slice(0, 32),
invoiceDate: this.$dayjs()
.subtract(Math.floor(Math.random() * 90), 'day')
.format('YYYY-MM-DD'),
invoiceType: index % 2 ? '增值税普通发票' : '增值税专用发票',
issuerName: this.form.payerName || 'Mock开票单位',
receiverName: this.form.payeeName || 'Mock受票单位',
invoiceAmount: amount,
availableInvoiceAmount: amount,
matchedAmount: amount,
taxRate: [3, 6, 9, 13][index % 4],
attachmentJson: '',
mockData: true,
};
});
this.invoiceClaimDialog.selected = [];
this.invoiceClaimDialog.page.current = 1;
this.invoiceClaimDialog.page.total = 80;
this.invoiceClaimDialog.visible = true;
},
invoicePoolQueryApi(params) {
return queryInvoicePool(params);
},
async handleInvoiceClaimConfirm(rows) {
// 统一票池行附件字段为复数 attachmentsJson,认领请求契约是单数 attachmentJson,中转处归一
this.invoiceClaimDialog.selected = rows.map(row => ({
...row,
attachmentJson: row.attachmentJson || row.attachmentsJson,
}));
await this.confirmInvoiceClaims();
this.invoiceClaimDialog.loading = false;
},
async confirmInvoiceClaims() {
if (!this.invoiceClaimDialog.selected.length) {
@@ -2310,11 +2372,7 @@ export default {
if (Number(this.form.settlementAmount || 0) > 0) {
remaining = Math.max(0, remaining - matchedAmount);
}
return {
...row,
invoiceType: row.invoiceTypeName || row.invoiceType || '',
matchedAmount,
};
return { ...row, matchedAmount };
})
.filter(row => row.matchedAmount > 0);
if (!invoices.length) {
@@ -2400,64 +2458,16 @@ export default {
const attachment = this.invoiceAttachment(row);
return attachment.originalName || attachment.name || `${row.invoiceNo || '发票'}.pdf`;
},
async viewInvoiceAttachment(row) {
const attachment = this.invoiceAttachment(row);
// 金蝶侧附件(path 型)经后端代理 A801-2 预览,OSS 直链维持 window.open
if (isKingdeePathAttachment(attachment)) {
if (this.kingdeeAttachmentBusy) return;
this.kingdeeAttachmentBusy = true;
try {
await viewKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row));
} finally {
this.kingdeeAttachmentBusy = false;
}
return;
}
viewInvoiceAttachment(row) {
const url = this.invoiceAttachmentUrl(row);
if (!url) return this.$message.warning('当前发票暂无可查看附件');
if (!url) return this.$message.warning('当前Mock发票暂无可查看附件');
window.open(url, '_blank');
},
async downloadInvoiceAttachment(row) {
const attachment = this.invoiceAttachment(row);
// 金蝶侧附件(path 型)经后端代理 A801-2 下载,OSS 直链维持前端直连下载
if (isKingdeePathAttachment(attachment)) {
if (this.kingdeeAttachmentBusy) return;
this.kingdeeAttachmentBusy = true;
try {
await downloadKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row));
} finally {
this.kingdeeAttachmentBusy = false;
}
return;
}
downloadInvoiceAttachment(row) {
const url = this.invoiceAttachmentUrl(row);
if (!url) return this.$message.warning('当前发票暂无可下载附件');
if (!url) return this.$message.warning('当前Mock发票暂无可下载附件');
downloadFileByUrl(url, this.invoiceAttachmentName(row));
},
// 开票发票预览地址:数电PDF优先(OFD浏览器无法预览),退预览图、板式文件
issuedInvoicePreviewUrl(row) {
return row.invoicePdfFileUrl || row.invoiceImageUrl || row.invoiceFileUrl || '';
},
// 开票发票下载地址:数电PDF优先,退板式文件、数电XML
issuedInvoiceDownloadUrl(row) {
return row.invoicePdfFileUrl || row.invoiceFileUrl || row.invoiceXmlFileUrl || '';
},
// 下载文件名按直链扩展名推断(数电票 .pdf / 板式文件 .ofd|.pdf),缺省 .pdf
issuedInvoiceFileName(row, url) {
const match = String(url || '').split('?')[0].match(/\.([a-zA-Z0-9]+)$/);
const extension = match ? `.${match[1].toLowerCase()}` : '.pdf';
return `${row.invoiceNo || '发票'}${extension}`;
},
viewIssuedInvoice(row) {
const url = this.issuedInvoicePreviewUrl(row);
if (!url) return this.$message.warning('当前发票暂无可查看文件,可稍后重试开票结果同步');
window.open(url, '_blank');
},
downloadIssuedInvoice(row) {
const url = this.issuedInvoiceDownloadUrl(row);
if (!url) return this.$message.warning('当前发票暂无可下载文件,可稍后重试开票结果同步');
downloadFileByUrl(url, this.issuedInvoiceFileName(row, url));
},
buildSavePayload() {
return {
id: this.form.id,
@@ -2862,7 +2872,7 @@ export default {
gap: 8px;
}
.formal-editor :deep(.el-form-item) {
margin-bottom: 14px;
margin-bottom: 24px;
}
.formal-editor :deep(.el-form-item__content) {
line-height: normal;
@@ -2939,14 +2949,6 @@ export default {
.formal-editor__invoice-claim :deep(.el-input) {
width: 360px;
}
.formal-editor__issued-invoices {
margin-top: 16px;
}
.formal-editor__issued-title {
margin-bottom: 8px;
color: var(--el-text-color-secondary);
font-size: 13px;
}
.formal-editor__negative-amount {
color: var(--el-color-danger);
}

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