Compare commits

..

44 Commits

Author SHA1 Message Date
刘泉佳 b077ba5be4 fix: 审批通过提示改用后端返回文案,金蝶自动同步失败弹警告 2026-10-11 14:35:57 +08:00
刘泉佳 4294c92e43 fix: 金蝶票池隐藏金蝶单据号 2026-10-08 14:22:28 +08:00
刘泉佳 b840530fe9 fix: 选票客商下拉改取客商发票信息抬头/税号(票池税号是开票税号,非统一信用代码) 2026-10-07 19:24:06 +08:00
刘泉佳 e6a1370de2 feat: 选票弹窗销/购方条件改客商选择器——按名称远程搜客商档案带出税号过滤,粘贴裸税号直查 2026-10-07 17:55:38 +08:00
刘泉佳 65296618a7 feat: 金蝶票池选票弹窗回切本地镜像标准分页(total跳页+名称模糊+认领登记三态过滤,下线游标翻页;附件查看下载与可用金额列保留) 2026-10-05 17:52:44 +08:00
刘泉佳 c4ca347f0e fix: 金蝶OFD附件预览短路提示下载;正式结算发票信息操作列固定最右侧 2026-10-05 15:52:31 +08:00
刘泉佳 b4ecc90470 feat: 收票登记发票附件查看下载与查看态操作列(金蝶piaozone path附件代理)
- vehicle-attachment-table 新增 kingdee-invoice-no/show-view-actions props:piaozone path 型附件查看经 A801-2 代理取流进弹窗预览(objectURL 弹窗关闭回收),下载/批量下载走金蝶代理串行触发
- 查看态展示查看/下载操作列,编辑态为查看/下载/删除;未传 props 的页面行为不变
- 收票登记表单页接入(编辑/查看态均生效),kingdee-attachment.js 导出 requestKingdeeFile 供组件复用
2026-10-01 18:32:22 +08:00
刘泉佳 0484c2359d Merge remote-tracking branch 'refs/remotes/origin/master' into feature/bfi-api-rate
# Conflicts:
#	pnpm-lock.yaml
#	src/axios.js
#	src/views/settlement/formal-settlement.vue
#	src/views/vehicle/customer-archive.vue
2026-10-01 13:46:44 +08:00
刘泉佳 7fca74907f feat: 正式结算单详情新增开票发票查看下载(金蝶回写直链)
- 发票信息卡片内新增只读开票发票表格(issuedInvoices,金蝶A303-5回写)
- 查看:数电PDF优先,退预览图/板式文件直链新窗口打开
- 下载:按直链扩展名命名,不经A801-2代理
2026-09-30 14:16:19 +08:00
刘泉佳 d7ddeacbbf feat: 已推送金蝶的认领隐藏作废按钮 2026-09-29 11:58:26 +08:00
刘泉佳 3d59203dbe fix: 作废确认弹窗去掉冲单描述,成功提示改为作废成功 2026-09-29 11:16:08 +08:00
刘泉佳 3700f5d82b ui: 收款流水页搜索区展开器改为与认领记录页一致的箭头图标按钮(tooltip 展开/折叠) 2026-09-29 10:35:56 +08:00
刘泉佳 05b16bbea0 ui: 收款流水页筛选重排——认领状态/交易时间提升为默认展示,低频条件收进展开区 2026-09-29 10:33:25 +08:00
刘泉佳 dc28bc2afa feat: 收款流水页筛选条件补充收款金额区间与认领人 2026-09-29 10:28:37 +08:00
刘泉佳 730c4e2a6a fix: 认领结果回写/流水同步 0 条时提示 NaN——unwrapData 判空改 ?? 保留合法数字 0 2026-09-29 01:33:58 +08:00
刘泉佳 9b42551bcd feat: 认领记录页筛选条件补充推送状态 2026-09-29 01:32:15 +08:00
刘泉佳 66944bdaf9 feat: 收款流水页筛选条件补充推送状态/金蝶状态/明细流水号(推送状态默认展示) 2026-09-29 01:25:36 +08:00
刘泉佳 09da05c75e feat: 收款流水页新增推送金蝶按钮与推送状态/失败原因/金蝶单号列,认领口径切公告未认领金额
- 行按钮推送金蝶(receipt_flow_push 权限,认领完成且未推送可点),成功提示金蝶收款单号
- 列表/详情展示推送状态、推送失败原因、金蝶收款单号与金蝶状态,留痕表含推送记录
- 认领表单/批量认领上限校验切公告未认领金额口径,新增剩余可认领金额展示
2026-09-29 01:03:21 +08:00
刘泉佳 8bc312502f feat: 收款流水认领页面展示金蝶未认领金额(unclaimedAmount) 2026-09-28 20:04:58 +08:00
刘泉佳 9b40800aab Merge remote-tracking branch 'origin/feature/kingdee-attachment-sync' into feature/bfi-api-rate
# Conflicts:
#	src/api/payment/invoiceApplication.js
#	src/api/payment/invoiceReceipt.js
#	src/api/payment/paymentApplication.js
#	src/api/settlement/formalSettlement.js
#	src/views/payment/invoice-application.vue
#	src/views/payment/invoice-receipt.vue
#	src/views/payment/payment-application-form.vue
#	src/views/payment/payment-application.vue
#	src/views/settlement/formal-settlement.vue
2026-09-28 10:07:19 +08:00
刘泉佳 f49f36fb60 ✨ 金蝶附件下载(A801-2)前端:票池弹窗/正式结算/付款申请三入口附件查看下载与类型分流 2026-09-27 21:17:59 +08:00
刘泉佳 7fd8140761 feat: 收款认领页直连A301-2公告同步并支持A301-1认领结果查询回写 2026-09-24 22:21:38 +08:00
刘泉佳 aabe67e4d6 ✨ 金蝶附件同步(A902)前端:四页面附件同步列、重推补传提示与收票推送超时放宽 2026-09-24 13:27:41 +08:00
刘泉佳 e63c7fdd42 Merge remote-tracking branch 'origin/feature/bfi-api-rate' into feature/bfi-api-rate
# Conflicts:
#	src/api/payment/invoiceReceipt.js
2026-09-24 11:56:47 +08:00
刘泉佳 ec56ee07be feat: 正式结算、客商档案金蝶推送按钮增加防重复点击 2026-09-24 05:08:01 +08:00
刘泉佳 a5eeda6152 feat: 付款申请、开票申请、收票登记金蝶按钮增加防重复点击 2026-09-24 05:07:51 +08:00
刘泉佳 5c45e9b2c9 feat: 金蝶推送接口前端超时扩大至30秒 2026-09-24 05:07:39 +08:00
刘泉佳 331a39b37d Merge remote-tracking branch 'origin/master' into feature/bfi-api-rate
# Conflicts:
#	src/views/vehicle/customer-archive.vue
2026-09-24 03:15:23 +08:00
刘泉佳 aa0aec3704 feat: 批量转结算弹窗新增单据号和合同编码查询条件 2026-09-23 15:39:32 +08:00
刘泉佳 622705325b feat: 选票弹窗新增剩余可用金额列并支持游标翻页与上一页缓存回显
- 下一页回传上页nextCursor续扫,新查询/换页容量/重新打开自动重置游标与页缓存
- 上一页直接回显已浏览页缓存,不再发起请求
- 查询表单删除单据使用状态条件(后端固定查未用),新增剩余可用金额展示列
2026-09-23 15:06:33 +08:00
刘泉佳 4657fc29bd ♻️ 收票管理仅保留推送金蝶收票单并统一命名为批量同步 2026-09-23 11:05:19 +08:00
刘泉佳 884de893db ✨ 收票单推送金蝶与票池实时游标分页前端适配:选票弹窗hasMore分页,下线镜像补查与票池同步入口,保存时按票号重查回填 2026-09-23 10:22:51 +08:00
刘泉佳 cc73d544ff 🔧 引入vue-tsc与typecheck脚本支持类型检查,pnpm-lock.yaml改为忽略不跟踪 2026-09-23 10:22:46 +08:00
刘泉佳 10fecb5a54 ✨ 金蝶票池选票统一查镜像前端接线(新增统一选票弹窗组件,替换收票与结算认领弹窗并删除Mock) 2026-09-22 21:26:50 +08:00
刘泉佳 bab6bdba27 Merge remote-tracking branch 'origin/feature/bfi-api-rate' into feature/bfi-api-rate 2026-09-22 16:36:33 +08:00
刘泉佳 a554ccf38e 🐛 axios网络级失败(502/504/超时)补充统一错误提示,修复接口报错页面无感知 2026-09-21 21:20:57 +08:00
刘泉佳 a748e52551 ✨ 开票申请商品行选择开票内容时快照税收分类编码并随保存提交 2026-09-21 19:14:57 +08:00
刘泉佳 e89e78b527 ✨ 正式结算单认领查询对接发票云全票池(多条件搜索+服务端分页) 2026-09-20 19:41:37 +08:00
刘泉佳 2a03991297 ⚡ 客商档案金蝶回填优化:放宽锁定字段、信用代码可重新输入并修复查询误判 2026-09-20 16:06:03 +08:00
刘泉佳 884cc4355f fix: 同步金蝶接口放宽请求超时,修复长耗时推送成功但页面无提示 2026-09-18 20:42:52 +08:00
刘泉佳 58aaf79445 ✨ 客商档案新增金蝶客商查询回填与同步金蝶重推入口 2026-09-18 20:01:29 +08:00
刘泉佳 f067518c0e fix: 付款申请批量同步金蝶失败时展示失败原因
- 批量同步后解析返回结果,按"同步失败"标识区分成功与失败条目
- 部分失败时以 warning 提示成功/失败条数及具体失败原因(停留8秒),全部成功仍提示已同步条数
2026-09-18 11:58:00 +08:00
刘泉佳 11460f3378 ✨ 付款管理新增同步付款结果按钮并扩展金蝶单据状态选项 2026-09-18 11:57:59 +08:00
刘泉佳 7f543937fb ✨ 结算单新增时自动查询BFI汇率
- currency.js: 新增getBfiExchangeRate接口调用
- pre-settlement-editor.vue:
  - 选择合同后同步结算币种并自动查询当天BFI汇率
  - 汇率日期变更时自动查询BFI汇率并回填结算汇率
  - 新增时自动设置当天日期并查询汇率
- formal-settlement-editor.vue:
  - 选择合同后同步结算币种并自动查询当天BFI汇率
  - 汇率日期变更时自动查询BFI汇率并回填结算汇率
  - 新增时自动查询当天BFI汇率
2026-09-16 14:14:53 +08:00
128 changed files with 3563 additions and 9514 deletions
+1
View File
@@ -1,5 +1,6 @@
.DS_Store
node_modules
pnpm-lock.yaml
/dist
/tests/e2e/videos/
-18
View File
@@ -1,18 +0,0 @@
# 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,14 +12,6 @@
**坑二:宿主 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」重复项。
## 样式硬规则
+5 -2
View File
@@ -6,7 +6,8 @@
"prod": "vite --mode production",
"build": "vite build",
"build:prod": "vite build --mode production",
"serve": "vite preview --host"
"serve": "vite preview --host",
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
@@ -47,9 +48,11 @@
"prettier": "^2.8.7",
"sass": "^1.85.1",
"terser": "^5.31.1",
"typescript": "^7.0.2",
"unplugin-auto-import": "^0.11.2",
"vite": "^5.4.19",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-setup-extend": "^0.4.0"
"vite-plugin-vue-setup-extend": "^0.4.0",
"vue-tsc": "^3.3.11"
}
}
+72 -41
View File
@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
patchedDependencies:
'@smallwei/avue': 5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db
importers:
.:
@@ -28,16 +31,16 @@ importers:
version: 1.7.0
'@smallwei/avue':
specifier: ~3.9.2
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)
version: 3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)
animate.css:
specifier: ^4.1.1
version: 4.1.1
avue-plugin-ueditor:
specifier: ^1.0.4
version: 1.0.4(axios@1.16.0)(vue@3.5.34)
version: 1.0.4(axios@1.16.0(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34)
axios:
specifier: ^1.13.5
version: 1.16.0
version: 1.16.0(debug@4.4.3(supports-color@7.2.0))
codemirror:
specifier: ^5.65.21
version: 5.65.21
@@ -125,7 +128,7 @@ importers:
version: 5.4.21(sass@1.99.0)(terser@5.46.2)
vite-plugin-compression:
specifier: ^0.5.1
version: 0.5.1(vite@5.4.21(sass@1.99.0)(terser@5.46.2))
version: 0.5.1(supports-color@7.2.0)(vite@5.4.21(sass@1.99.0)(terser@5.46.2))
vite-plugin-vue-setup-extend:
specifier: ^0.4.0
version: 0.4.0(vite@5.4.21(sass@1.99.0)(terser@5.46.2))
@@ -404,30 +407,35 @@ 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==}
@@ -503,36 +511,42 @@ 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==}
@@ -599,66 +613,79 @@ 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==}
@@ -2627,10 +2654,10 @@ snapshots:
countup.js: 2.10.0
signature_pad: 5.1.3
'@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)':
'@smallwei/avue@3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)':
dependencies:
'@element-plus/icons-vue': 2.3.2(vue@3.5.34)
axios: 0.21.4
axios: 0.21.4(debug@4.4.3(supports-color@7.2.0))
countup.js: 1.9.3
dayjs: 1.11.20
element-plus: 2.14.2(vue@3.5.34)
@@ -2766,25 +2793,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@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/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@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/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 3.0.0
is-url: 1.2.4
lodash.throttle: 4.1.1
nanoid: 3.3.12
slate: 0.72.8
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@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/code-highlight@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@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/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 4.0.6
prismjs: 1.30.0
slate: 0.123.0
snabbdom: 3.6.3
'@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@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/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@types/event-emitter': 0.3.5
'@uppy/core': 2.3.4
@@ -2800,9 +2827,9 @@ snapshots:
lodash.foreach: 4.5.0
lodash.throttle: 4.1.1
lodash.toarray: 4.4.0
nanoid: 3.3.12
nanoid: 5.1.11
scroll-into-view-if-needed: 3.1.0
slate: 0.72.8
slate: 0.123.0
slate-history: 0.115.0(slate@0.123.0)
snabbdom: 3.6.3
@@ -2810,13 +2837,13 @@ snapshots:
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@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)
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/code-highlight': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/list-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/table-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/upload-image-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/video-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 4.0.6
is-hotkey: 0.2.0
lodash.camelcase: 4.3.0
@@ -2829,16 +2856,16 @@ snapshots:
slate: 0.123.0
snabbdom: 3.6.3
'@wangeditor-next/list-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@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/list-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@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/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@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/table-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@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/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
@@ -2846,22 +2873,22 @@ snapshots:
slate: 0.123.0
snabbdom: 3.6.3
'@wangeditor-next/upload-image-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@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/upload-image-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@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)
'@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@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)':
'@wangeditor-next/video-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@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/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
@@ -3038,22 +3065,22 @@ snapshots:
asynckit@0.4.0: {}
avue-plugin-ueditor@1.0.4(axios@1.16.0)(vue@3.5.34):
avue-plugin-ueditor@1.0.4(axios@1.16.0(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34):
dependencies:
'@wangeditor/editor': 5.1.23
'@wangeditor/editor-for-vue': 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34)
axios: 1.16.0
axios: 1.16.0(debug@4.4.3(supports-color@7.2.0))
vue: 3.5.34
axios@0.21.4:
axios@0.21.4(debug@4.4.3(supports-color@7.2.0)):
dependencies:
follow-redirects: 1.16.0
follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0))
transitivePeerDependencies:
- debug
axios@1.16.0:
axios@1.16.0(debug@4.4.3(supports-color@7.2.0)):
dependencies:
follow-redirects: 1.16.0
follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0))
form-data: 4.0.5
proxy-from-env: 2.1.0
transitivePeerDependencies:
@@ -3221,9 +3248,11 @@ snapshots:
dayjs@1.11.20: {}
debug@4.4.3:
debug@4.4.3(supports-color@7.2.0):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 7.2.0
delayed-stream@1.0.0: {}
@@ -3431,7 +3460,9 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
follow-redirects@1.16.0: {}
follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)):
optionalDependencies:
debug: 4.4.3(supports-color@7.2.0)
form-data@4.0.5:
dependencies:
@@ -4126,10 +4157,10 @@ snapshots:
uuid@8.3.2: {}
vite-plugin-compression@0.5.1(vite@5.4.21(sass@1.99.0)(terser@5.46.2)):
vite-plugin-compression@0.5.1(supports-color@7.2.0)(vite@5.4.21(sass@1.99.0)(terser@5.46.2)):
dependencies:
chalk: 4.1.2
debug: 4.4.3
debug: 4.4.3(supports-color@7.2.0)
fs-extra: 10.1.0
vite: 5.4.21(sass@1.99.0)(terser@5.46.2)
transitivePeerDependencies:
+3 -3
View File
@@ -2,9 +2,9 @@ packages:
- '.'
allowBuilds:
'@parcel/watcher': true
es5-ext: true
esbuild: true
'@parcel/watcher': set this to true or false
es5-ext: set this to true or false
esbuild: set this to true or false
patchedDependencies:
'@smallwei/avue': patches/@smallwei__avue.patch
+11
View File
@@ -58,3 +58,14 @@ export const changeStatus = (id, status) => {
},
});
};
export const getBfiExchangeRate = (currencyCode, effectdate) => {
return request({
url: '/blade-transport/bfi/exchange-rate',
method: 'get',
params: {
currencyCode,
effectdate,
},
});
};
-8
View File
@@ -90,11 +90,3 @@ 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,17 +52,6 @@ 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,14 +34,6 @@ 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', params: { id } });
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
export const invoiceTypeOptions = [
{ label: '电子专票', value: 'electronic_special' },
+18 -8
View File
@@ -5,8 +5,17 @@ const baseUrl = '/blade-transport/invoice-receipt';
export const getList = (current, size, params) =>
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
export const getInvoicePool = keyword =>
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params: { keyword } });
export const queryInvoicePool = params =>
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params });
export const downloadInvoicePoolAttachment = invoiceNo =>
// 金蝶票池发票文件下载(后端代理 A801-2 流式回源),耗时较普通接口长,放宽到 120s
request({
url: `${baseUrl}/invoice-pool/attachment/download`,
method: 'get',
params: { invoiceNo },
responseType: 'blob',
timeout: 120000,
});
export const getSettlementCandidates = (keyword, receiptId) =>
request({
url: `${baseUrl}/settlement-candidates`,
@@ -25,8 +34,8 @@ export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post'
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
export const syncKingdee = id =>
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
export const pushKingdee = ids =>
request({ url: `${baseUrl}/push-kingdee`, method: 'post', timeout: 30000, data: ids });
export const approvalStatusOptions = [
{ label: '草稿', value: 'draft' },
@@ -36,8 +45,9 @@ export const approvalStatusOptions = [
{ label: '已作废', value: 'voided' },
];
export const kingdeeStatusOptions = [
{ label: '未同步', value: 'unsynced' },
{ label: '已同步', value: 'synced' },
{ label: '同步失败', value: 'failed' },
export const kingdeePushStatusOptions = [
{ label: '未推送', value: 'unsynced' },
{ label: '已保存收票单', value: 'pushed' },
{ label: '已指定应付', value: 'assigned' },
{ label: '推送失败', value: 'failed' },
];
+10 -2
View File
@@ -16,10 +16,15 @@ 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', params: { id } });
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
// 批量同步逐单单据级事务串行推送,耗时随单量线性增长
export const syncKingdeeBatch = ids =>
request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', data: ids });
request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', timeout: 30000, data: ids });
// 回写任务按 paymentNo 分批远程查询 A103-2/A103-4,同样可能超过全局超时
export const syncKingdeeResult = () =>
request({ url: `${baseUrl}/sync-kingdee-result`, method: 'post', timeout: 30000 });
export const paymentTypeOptions = [
{ label: '项目预付', value: 'project_advance' },
@@ -36,5 +41,8 @@ export const approvalStatusOptions = [
export const kingdeeStatusOptions = [
{ label: '未生成', value: 'unsynced' },
{ label: '已生成', value: 'synced' },
{ label: '付款中', value: 'paying' },
{ label: '已付款', value: 'paid' },
{ label: '已关闭', value: 'closed' },
{ label: '生成失败', value: 'failed' },
];
+10
View File
@@ -8,14 +8,24 @@ export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get'
export const updateAttachments = data =>
request({ url: `${baseUrl}/attachments`, method: 'post', data });
export const voidClaim = id => request({ url: `${baseUrl}/void`, method: 'post', params: { id } });
export const queryKingdeeResult = () =>
request({ url: `${baseUrl}/query-kingdee-result`, method: 'post' });
export const claimStatusOptions = [
{ label: '已认领', value: 'claimed' },
{ label: '已作废', value: 'voided' },
];
export const pushStatusOptions = [
{ label: '未推送', value: 'none' },
{ label: '已推送', value: 'pushed' },
{ label: '推送失败', value: 'failed' },
];
export const kingdeeBillStatusOptions = [
{ label: '未生成', value: 'none' },
{ label: '已推送', value: 'pushed' },
{ label: '审核通过', value: 'approved' },
{ label: '已收款', value: 'received' },
{ label: '处理失败', value: 'failed' },
];
+16 -1
View File
@@ -12,10 +12,25 @@ export const getSettlementCandidates = (keyword, flowId) =>
params: { keyword, flowId },
});
export const claim = data => request({ url: `${baseUrl}/claim`, method: 'post', data });
export const sync = data => request({ url: `${baseUrl}/sync`, method: 'post', data });
export const sync = () => request({ url: `${baseUrl}/sync`, method: 'post' });
export const pushKingdee = flowId =>
request({ url: `${baseUrl}/push-kingdee`, method: 'post', params: { flowId } });
export const claimStatusOptions = [
{ label: '未认领', value: 'unclaimed' },
{ label: '部分认领', value: 'partial' },
{ label: '认领完成', value: 'claimed' },
];
export const pushStatusOptions = [
{ label: '未推送', value: 'none' },
{ label: '已推送', value: 'pushed' },
{ label: '推送失败', value: 'failed' },
];
export const kingdeeBillStatusOptions = [
{ label: '已推送', value: 'pushed' },
{ label: '审核通过', value: 'approved' },
{ label: '已收款', value: 'received' },
{ label: '处理失败', value: 'failed' },
];
+4 -1
View File
@@ -30,8 +30,9 @@ export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post'
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
// 同步金蝶涉及多次金蝶远程接口调用(A102-4 预检 + A102-2 保存),可达 20s+,覆盖全局 10s 超时
export const syncKingdee = id =>
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
export const getDetailFees = detailId =>
request({ url: `${baseUrl}/detail-fees`, method: 'get', params: { detailId } });
export const getReceiptClaims = formalSettlementId =>
@@ -48,3 +49,5 @@ 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 });
+2 -12
View File
@@ -57,36 +57,26 @@ export const clearNonTopDept = signal => {
});
};
export const syncOaCompany = (current = 1, size = 20, signal, incremental = false, modifiedSince) => {
export const syncOaCompany = (current = 1, size = 20, signal) => {
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,
incremental = false,
modifiedSince
) => {
export const syncOaDepartment = (current = 1, size = 20, signal) => {
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
@@ -1,62 +0,0 @@
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,
},
});
};
+1 -14
View File
@@ -31,15 +31,13 @@ export const add = row => {
});
};
export const syncIamAccounts = (current = 1, size = 50, signal, incremental = false, modifiedSince) => {
export const syncIamAccounts = (current = 1, size = 50, signal) => {
return request({
url: '/blade-system/user/sync-iam-accounts',
method: 'post',
params: {
current,
size,
incremental: !!incremental,
modifiedSince: modifiedSince || undefined,
},
timeout: 60000,
signal,
@@ -54,17 +52,6 @@ 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,8 +9,6 @@ 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 =>
@@ -1,49 +0,0 @@
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,6 +51,14 @@ export const changeStatus = (id, status) => {
});
};
export const auditCertification = (id, certificationStatus, rejectReason) => {
return request({
url: '/blade-transport/transport-vehicle/audit-certification',
method: 'post',
params: { id, certificationStatus, rejectReason },
});
};
export const getExpiryStat = params => {
return request({
url: '/blade-transport/transport-vehicle/expiry-stat',
+22
View File
@@ -152,6 +152,28 @@ export const getScoreTemplate = quantificationId => {
});
};
export const queryKingdeeCustomer = creditCode => {
return request({
url: '/blade-transport/bfi/customer/query-by-credit-code',
method: 'get',
timeout: 30000,
params: {
creditCode,
},
});
};
export const syncKingdee = id => {
return request({
url: '/blade-transport/customer-archive/sync-kingdee',
method: 'post',
timeout: 30000,
params: {
id,
},
});
};
export const recognizeBusinessLicenseOcr = imageUrl => {
return request({
url: '/blade-transport/baidu-ocr/recognize-url',
+10 -9
View File
@@ -189,14 +189,7 @@ axios.interceptors.response.use(
// 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。
if (isJsonContentType(res) && !isPlainObject(res.data)) {
try {
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 text = res.data instanceof Blob ? await res.data.text() : res.data;
const parsed = typeof text === 'string' && text ? JSON.parse(text) : null;
if (parsed && typeof parsed === 'object') {
res.data = parsed;
@@ -271,10 +264,18 @@ 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);
}
);
+5 -67
View File
@@ -16,11 +16,7 @@
<div class="address-map-picker__info">{{ status }}</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button
type="primary"
:disabled="!hasCoordinateValue(selected.longitude) || !hasCoordinateValue(selected.latitude) || resolving"
@click="confirm"
>
<el-button type="primary" :disabled="!selected.longitude || resolving" @click="confirm">
确定
</el-button>
</template>
@@ -275,12 +271,7 @@ export default {
this.runGeocode('address', lnglat)
.then(result => {
const detailAddress = result.regeocode?.formattedAddress || fallbackAddress;
const region = this.resolveRegionFromGeocode(result);
this.selected = {
...this.selected,
address: detailAddress,
...region,
};
this.selected = { ...this.selected, address: detailAddress };
this.keyword = detailAddress || this.keyword;
this.status = detailAddress || '已选点,可确认回填';
})
@@ -294,26 +285,6 @@ 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 });
@@ -326,31 +297,8 @@ 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() {
this.syncSelectedFromMarker();
if (!this.hasCoordinateValue(this.selected.longitude) || !this.hasCoordinateValue(this.selected.latitude)) {
if (!this.selected.longitude || !this.selected.latitude) {
this.$message.warning('请先搜索或点击地图完成选点');
return;
}
@@ -360,18 +308,8 @@ export default {
}
this.$emit('confirm', {
address: this.selected.address,
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 || '',
longitude: this.selected.longitude,
latitude: this.selected.latitude,
});
this.visible = false;
},
+1 -1
View File
@@ -576,7 +576,7 @@ export default {
}
.mode-content :deep(.el-form-item) {
margin-bottom: 24px;
margin-bottom: 14px;
}
.mode-content :deep(.el-form-item:last-child) {
@@ -0,0 +1,434 @@
<template>
<el-dialog v-model="visible" :title="title" width="92%" append-to-body destroy-on-close @open="handleOpen">
<el-form :model="query" inline label-width="130px" @submit.prevent>
<el-form-item label="发票号码">
<el-input v-model="query.invoiceNo" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="开票日期">
<el-date-picker
v-model="query.invoiceDateRange"
type="daterange"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item label="开票单位">
<el-input v-model="query.salerName" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="受票单位">
<el-input v-model="query.buyerName" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="开票单位(销方)">
<el-select
v-model="query.salerTaxNo"
clearable
filterable
remote
reserve-keyword
:remote-method="keyword => searchCustomers(keyword, 'saler')"
:loading="customerLoading.saler"
placeholder="输入名称搜索或直接粘贴税号"
style="width: 220px"
@keyup.enter="handleQuery"
>
<el-option
v-for="item in customerOptions.saler"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="受票单位(购方)">
<el-select
v-model="query.buyerTaxNo"
clearable
filterable
remote
reserve-keyword
:remote-method="keyword => searchCustomers(keyword, 'buyer')"
:loading="customerLoading.buyer"
placeholder="输入名称搜索或直接粘贴税号"
style="width: 220px"
@keyup.enter="handleQuery"
>
<el-option
v-for="item in customerOptions.buyer"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="发票类型">
<el-select
v-model="query.invoiceType"
clearable
filterable
placeholder="不限"
style="width: 230px"
>
<el-option
v-for="option in invoiceTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
<el-form-item label="认领状态">
<el-select v-model="query.claimed" clearable placeholder="不限" style="width: 120px">
<el-option label="未认领" :value="false" />
<el-option label="已认领" :value="true" />
</el-select>
</el-form-item>
<el-form-item label="登记状态">
<el-select v-model="query.registered" clearable placeholder="不限" style="width: 120px">
<el-option label="未登记" :value="false" />
<el-option label="已登记" :value="true" />
</el-select>
</el-form-item>
<el-form-item>
<el-button :disabled="loading" @click="resetQuery">重置</el-button>
<el-button type="primary" :disabled="loading" @click="handleQuery">查询</el-button>
</el-form-item>
</el-form>
<el-table
ref="poolTable"
v-loading="loading"
:data="rows"
:row-key="rowKey"
border
height="520"
:highlight-current-row="!multiple"
@selection-change="selection => (selected = selection)"
@row-dblclick="handleRowDblclick"
>
<el-table-column v-if="multiple" type="selection" width="52" fixed="left" align="center" reserve-selection />
<el-table-column v-else width="52" align="center" fixed="left">
<template #default="{ row }">
<el-radio
:model-value="current && current.invoiceNo"
:value="row.invoiceNo"
@change="current = row"
>
<span></span>
</el-radio>
</template>
</el-table-column>
<el-table-column prop="invoiceNo" label="发票号码" min-width="180" />
<el-table-column prop="invoiceDate" label="开票日期" min-width="120" />
<el-table-column prop="invoiceType" label="发票类型" min-width="140" />
<el-table-column prop="issuerName" label="开票单位" min-width="180" show-overflow-tooltip />
<el-table-column prop="receiverName" label="受票单位" min-width="180" show-overflow-tooltip />
<el-table-column label="金额(含税)" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
</el-table-column>
<el-table-column label="税率" min-width="90" align="center">
<template #default="{ row }">{{ formatRate(row.taxRate) }}</template>
</el-table-column>
<el-table-column label="税额" min-width="120" align="right">
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
</el-table-column>
<el-table-column label="剩余可用金额" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.availableTaxAmount) }}</template>
</el-table-column>
<!--<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="210" show-overflow-tooltip />-->
<el-table-column label="认领状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.claimed" type="danger" size="small">已认领</el-tag>
<el-tag v-else type="info" size="small">未认领</el-tag>
</template>
</el-table-column>
<el-table-column label="登记状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.registered" type="warning" size="small">已登记</el-tag>
<el-tag v-else type="info" size="small">未登记</el-tag>
</template>
</el-table-column>
<el-table-column label="附件" width="120" fixed="right" align="center">
<template #default="{ row }">
<el-link
type="primary"
:underline="false"
:disabled="attachmentBusy === row.invoiceNo"
@click.stop="viewAttachment(row)"
>查看</el-link
>
<el-link
type="primary"
:underline="false"
:disabled="attachmentBusy === row.invoiceNo"
@click.stop="downloadAttachment(row)"
>下载</el-link
>
</template>
</el-table-column>
<el-table-column v-if="!multiple" label="操作" width="90" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click.stop="confirmRows([row])">选择</el-link>
</template>
</el-table-column>
</el-table>
<div class="kingdee-pool-dialog__pager">
<span class="kingdee-pool-dialog__pager-tip">按名称检索范围为已同步票,未找到请改用发票号码或税号精确查找</span>
<el-pagination
v-model:current-page="page.current"
v-model:page-size="page.size"
:total="page.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:disabled="loading"
@current-change="load"
@size-change="handleSizeChange"
/>
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleConfirm">{{ confirmText }}</el-button>
</template>
</el-dialog>
</template>
<script>
import request from '@/axios';
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
import {
downloadKingdeeInvoiceFile,
isKingdeePathAttachment,
viewKingdeeInvoiceFile,
} from '@/utils/kingdee-attachment';
export default {
name: 'KingdeeInvoicePoolDialog',
props: {
modelValue: { type: Boolean, required: true },
title: { type: String, default: '查询金蝶票据池' },
confirmText: { type: String, default: '确定' },
multiple: { type: Boolean, default: false },
initialInvoiceNo: { type: String, default: '' },
queryApi: { type: Function, required: true },
},
emits: ['update:modelValue', 'confirm'],
data() {
return {
loading: false,
// 与后端 BfiConstant.INVOICE_TYPE_NAMES 对齐:value 为发票类型编码(镜像按 invoice_type_code 精确匹配)
// 选票场景只开放数电票(普票/专票)
invoiceTypeOptions: [
{ value: '26', label: '数电发票(普通发票)' },
{ value: '27', label: '数电发票(增值税专用发票)' },
],
rows: [],
selected: [],
current: null,
page: { current: 1, size: 10, total: 0 },
// 正在查看/下载附件的发票号(行级 loading 守卫)
attachmentBusy: null,
// 销/购方客商下拉:远程按名称搜客商档案,选中带出税号查询;手动输入的裸税号直接作为值过滤
customerOptions: { saler: [], buyer: [] },
customerLoading: { saler: false, buyer: false },
query: this.emptyQuery(),
};
},
computed: {
visible: {
get() {
return this.modelValue;
},
set(value) {
this.$emit('update:modelValue', value);
},
},
},
methods: {
emptyQuery() {
return {
invoiceNo: '',
// 默认查近一个月,随请求传后端并在页面回显
invoiceDateRange: [this.$dayjs().subtract(1, 'month').format('YYYY-MM-DD'), this.$dayjs().format('YYYY-MM-DD')],
salerName: '',
buyerName: '',
salerTaxNo: '',
buyerTaxNo: '',
invoiceType: '',
claimed: null,
registered: null,
};
},
// 镜像行主键为 id,回源入库前的响应仍可能无 id,用发票号码兜底
rowKey(row) {
return row.id || row.invoiceNo;
},
// 票池行附件字段为复数 attachmentsJson,行内首项即发票文件({source,path,name})
rowAttachment(row = {}) {
const value = row.attachmentsJson;
if (!value) return null;
let parsed = value;
if (typeof value === 'string') {
try {
parsed = JSON.parse(value);
} catch {
return null;
}
}
return Array.isArray(parsed) ? parsed[0] || null : parsed || null;
},
attachmentName(row) {
const attachment = this.rowAttachment(row);
return attachment?.name || `${row.invoiceNo || '发票'}.pdf`;
},
async viewAttachment(row) {
const attachment = this.rowAttachment(row);
if (!isKingdeePathAttachment(attachment)) {
this.$message.warning('当前发票暂无可查看附件');
return;
}
this.attachmentBusy = row.invoiceNo;
try {
await viewKingdeeInvoiceFile(row.invoiceNo, this.attachmentName(row));
} finally {
this.attachmentBusy = null;
}
},
async downloadAttachment(row) {
const attachment = this.rowAttachment(row);
if (!isKingdeePathAttachment(attachment)) {
this.$message.warning('当前发票暂无可下载附件');
return;
}
this.attachmentBusy = row.invoiceNo;
try {
await downloadKingdeeInvoiceFile(row.invoiceNo, this.attachmentName(row));
} finally {
this.attachmentBusy = null;
}
},
handleOpen() {
this.query = this.emptyQuery();
this.query.invoiceNo = this.initialInvoiceNo || '';
this.current = null;
this.selected = [];
this.$refs.poolTable?.clearSelection();
this.page.current = 1;
this.load();
},
handleQuery() {
this.page.current = 1;
this.load();
},
handleSizeChange() {
this.page.current = 1;
this.load();
},
resetQuery() {
this.query = this.emptyQuery();
this.handleQuery();
},
// 客商名称远程搜索:含 18 位社会信用代码特征(数字+大写字母)时按裸税号直查;
// 否则搜客商档案,取客商发票信息(blade_customer_invoice_info)的抬头/税号作选项——票池税号是开票税号,非统一信用代码
async searchCustomers(keyword, side) {
const text = String(keyword || '').trim();
if (!text) return;
if (/^[0-9A-Z]{15,20}$/.test(text)) {
this.customerOptions[side] = [{ label: `${text}(手输税号)`, value: text }];
return;
}
this.customerLoading[side] = true;
try {
const data = this.unwrap(await getCustomerList(1, 20, { fullName: text }));
const customers = (data.records || []).filter(item => item.id);
if (!customers.length) {
this.customerOptions[side] = [];
return;
}
// 列表接口不带发票信息,逐户取 detail(detail 组装 invoices,每户一张发票信息)
const options = await Promise.all(
customers.map(async customer => {
const detail = this.unwrap(await request({ url: '/blade-transport/customer-archive/detail', method: 'get', params: { id: customer.id } }));
return (detail?.invoices || [])
.filter(invoice => invoice.taxNo)
.map(invoice => ({ label: `${invoice.invoiceTitle || customer.fullName}(${invoice.taxNo})`, value: invoice.taxNo }));
})
);
this.customerOptions[side] = options.flat().filter(
(option, index, list) => list.findIndex(item => item.value === option.value) === index
);
} finally {
this.customerLoading[side] = false;
}
},
unwrap(response) {
const body = response?.data || response || {};
return body?.data || body;
},
async load() {
this.loading = true;
try {
const range = this.query.invoiceDateRange || [];
const data = this.unwrap(
await this.queryApi({
current: this.page.current,
size: this.page.size,
invoiceNo: this.query.invoiceNo || undefined,
invoiceDateStart: range[0] || undefined,
invoiceDateEnd: range[1] || undefined,
salerName: this.query.salerName || undefined,
buyerName: this.query.buyerName || undefined,
salerTaxNo: this.query.salerTaxNo || undefined,
buyerTaxNo: this.query.buyerTaxNo || undefined,
invoiceType: this.query.invoiceType || undefined,
claimed: this.query.claimed ?? undefined,
registered: this.query.registered ?? undefined,
})
);
this.rows = data.records || [];
this.page.total = Number(data.total || 0);
} finally {
this.loading = false;
}
},
handleRowDblclick(row) {
if (!this.multiple) {
this.confirmRows([row]);
}
},
handleConfirm() {
const picked = this.multiple ? this.selected : this.current ? [this.current] : [];
this.confirmRows(picked);
},
confirmRows(rows) {
if (!rows.length) {
this.$message.warning(this.multiple ? '请至少选择一条发票' : '请选择一张发票');
return;
}
this.$emit('confirm', rows);
this.visible = false;
},
formatMoney(value) {
const number = Number(value);
return Number.isFinite(number) ? number.toFixed(2) : '-';
},
formatRate(value) {
const number = Number(value);
return Number.isFinite(number) ? `${number}%` : '-';
},
},
};
</script>
<style lang="scss" scoped>
.kingdee-pool-dialog__pager {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12px;
gap: 16px;
}
.kingdee-pool-dialog__pager-tip {
color: #909399;
font-size: 12px;
}
</style>
-113
View File
@@ -1,113 +0,0 @@
<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>
+139 -14
View File
@@ -9,21 +9,18 @@
<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
class-name="is-ellipsis-link"
>
<el-table-column label="文件名" min-width="240" show-overflow-tooltip>
<template #default="{ row }">
<span
class="attachment-file-name"
class="vehicle-attachment-table__file-name"
role="button"
tabindex="0"
@click="previewAttachment(row)"
@keydown.enter="previewAttachment(row)"
@keydown.space.prevent="previewAttachment(row)"
>{{ attachmentName(row) }}</span>
>
{{ attachmentName(row) }}
</span>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220">
@@ -43,9 +40,23 @@
</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" label="操作" width="100" align="center" fixed="right">
<template #default="{ $index }">
<el-link type="danger" @click="removeAttachment($index)">删除</el-link>
<el-table-column
v-if="!readonly || showViewActions"
label="操作"
:width="readonly ? 120 : 170"
align="center"
fixed="right"
>
<template #default="{ row, $index }">
<div class="vehicle-attachment-table__actions">
<el-link type="primary" :disabled="kingdeeBusy" @click="previewAttachment(row)"
>查看</el-link
>
<el-link type="primary" :disabled="kingdeeBusy" @click="downloadAttachment(row)"
>下载</el-link
>
<el-link v-if="!readonly" type="danger" @click="removeAttachment($index)">删除</el-link>
</div>
</template>
</el-table-column>
</el-table>
@@ -71,6 +82,7 @@
width="90%"
top="4vh"
class="vehicle-attachment-table__viewer-dialog"
@closed="handlePreviewClosed"
>
<pdf-preview
v-if="documentPreviewVisible && previewFile.url && previewFile.isPdf"
@@ -111,6 +123,11 @@ 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',
@@ -156,6 +173,16 @@ export default {
type: Number,
default: 500,
},
// 金蝶侧附件(piaozone path 型)代理下载所依赖的发票号;行内自带 invoiceNo 时优先用行内值
kingdeeInvoiceNo: {
type: String,
default: '',
},
// 查看态也展示「查看/下载」操作列(编辑态始终展示,含删除)
showViewActions: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue'],
data() {
@@ -167,6 +194,10 @@ export default {
imagePreviewIndex: 0,
documentPreviewVisible: false,
previewFile: {},
// 金蝶侧附件代理请求进行中(行级按钮共享 loading 守卫,防重复触发)
kingdeeBusy: false,
// 金蝶附件预览用的 objectURL,弹窗关闭时回收
kingdeeObjectUrl: '',
viewerPlugins,
viewerToolbar: {
download: true,
@@ -201,6 +232,12 @@ 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('.');
@@ -248,6 +285,10 @@ 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('附件地址为空,无法下载');
@@ -255,20 +296,47 @@ 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 downloadableRows = rows.filter(row => this.attachmentUrl(row));
if (!downloadableRows.length) {
const kingdeeRows = rows.filter(row => this.isKingdeeRow(row));
const urlRows = rows.filter(row => !this.isKingdeeRow(row) && this.attachmentUrl(row));
if (!kingdeeRows.length && !urlRows.length) {
this.$message.warning(
this.selectedRows.length ? '所选附件暂无可下载地址' : '暂无可下载附件'
);
return;
}
downloadableRows.forEach((row, index) => {
urlRows.forEach((row, index) => {
window.setTimeout(() => this.downloadAttachment(row), index * 500);
});
// 金蝶侧附件走代理接口,串行触发避免并发重复回源
this.downloadKingdeeRows(kingdeeRows);
},
async downloadKingdeeRows(rows) {
if (this.kingdeeBusy) return;
this.kingdeeBusy = true;
try {
for (const row of rows) {
await downloadKingdeeInvoiceFile(this.kingdeeInvoiceNoOf(row), this.attachmentName(row));
}
} finally {
this.kingdeeBusy = false;
}
},
previewAttachment(row) {
// 金蝶侧附件经 A801-2 代理取流后进弹窗预览,OSS 直链维持原逻辑
if (this.isKingdeeRow(row)) {
return this.previewKingdeeAttachment(row);
}
const url = this.attachmentUrl(row);
if (!url) {
this.$message.warning('附件地址为空,无法预览');
@@ -291,6 +359,42 @@ 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('当前文件暂不支持在线预览');
},
@@ -311,6 +415,27 @@ 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,10 +305,6 @@ export const createOption = () => ({
searchOrder: 3,
searchPlaceholder: '请选择',
dicData: [],
props: {
label: 'label',
value: 'value',
},
filterable: true,
hide: true,
display: false,
-3
View File
@@ -2,9 +2,6 @@ const dimensionOptions = [
{ label: '重量', value: '重量' },
{ label: '体积', value: '体积' },
{ label: '数量', value: '数量' },
{ label: '整车', value: '整车' },
{ label: '里程', value: '里程' },
{ label: '吨·公里', value: '吨·公里' },
];
const statusOptions = [
-1
View File
@@ -117,7 +117,6 @@ export const contractCategoryOptions = [
export const signTypeOptions = [
{ label: '新签', value: '新签' },
{ label: '续签', value: '续签' },
{ label: '补充协议', value: '补充协议' },
];
export const contractStageOptions = [
+10 -39
View File
@@ -6,7 +6,6 @@ import {
contractStageOptions,
createCrudOption,
effectiveTypeOptions,
nonNegativeRule,
selectRule,
signTypeOptions,
textRule,
@@ -62,14 +61,13 @@ export const config = {
['contractNo', '合同编号'],
['contractName', '合同名称'],
['projectName', '所属项目'],
['organizationName', '合同负责部门'],
['organizationName', '所属组织'],
['contractCategory', '合同类别'],
['signType', '签约类型'],
['partyA', '甲方'],
['partyB', '乙方'],
['handlerUserName', '经办人'],
['signDate', '签订日期'],
['applyDate', '申请日期'],
['contractFormat', '合同格式'],
['legalSealFlag', '是否需要加盖法人章'],
['copyCount', '一式(份)'],
@@ -78,8 +76,7 @@ export const config = {
['templateFlag', '是否范本'],
['originalContractNo', '原件合同编号'],
['electronicSealFlag', '是否电子章'],
['mainContent', '合同主要内容', 2],
['remark', '其他事项说明', 2],
['remark', '备注', 2],
],
},
{
@@ -212,7 +209,7 @@ export const config = {
label: '流程',
statusProp: 'approvalStatus',
status: ['approved', 'change_approved'],
stage: ['temporary', 'formal', 'terminated'],
stage: ['formal', 'terminated'],
permission: 'contract_manage_view',
},
{
@@ -297,17 +294,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: '合同类别',
@@ -438,17 +435,6 @@ 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',
@@ -482,10 +468,11 @@ export const option = createCrudOption([
{ label: '否', value: 0 },
{ label: '是', value: 1 },
],
clearable: true,
value: 0,
hide: true,
order: 70,
minWidth: 100,
rules: selectRule('是否需要加盖法人章'),
},
{
label: '一式(份)',
@@ -513,10 +500,6 @@ export const option = createCrudOption([
hide: true,
order: 45,
minWidth: 130,
rules: [
{ required: true, message: '请输入合同金额', trigger: 'blur' },
nonNegativeRule('合同金额'),
],
},
{
label: '是否范本',
@@ -634,19 +617,7 @@ export const option = createCrudOption([
minWidth: 110,
},
{
label: '合同主要内容',
prop: 'mainContent',
type: 'textarea',
span: 24,
minRows: 4,
hide: true,
order: 41,
maxlength: 1000,
showWordLimit: true,
rules: textRule('合同主要内容', 1000),
},
{
label: '其他事项说明',
label: '备注',
prop: 'remark',
type: 'textarea',
span: 24,
@@ -655,7 +626,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', 'approved', 'change_approved'],
status: ['reviewing', 'change_reviewing'],
permission: 'project_apply_approve',
mkOnly: true,
},
+2
View File
@@ -400,6 +400,7 @@ export const option = {
span: 6,
order: 350,
minWidth: 150,
rules: [{ required: true, message: '请选择计划开始日期', trigger: 'change' }],
},
{
label: '计划结束日期',
@@ -410,6 +411,7 @@ export const option = {
span: 6,
order: 340,
minWidth: 150,
rules: [{ required: true, message: '请选择计划结束日期', trigger: 'change' }],
},
{
label: '数据来源',
-19
View File
@@ -98,24 +98,6 @@ 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 === '') {
@@ -626,7 +608,6 @@ export const option = {
{
label: '过程节点',
prop: 'currentProcessNode',
formatter: row => formatCurrentProcessNode(row),
minWidth: 140,
addDisplay: false,
editDisplay: false,
+1
View File
@@ -13,6 +13,7 @@ export const invoiceApplicationTableColumns = [
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
];
export const invoiceApplicationDetailColumns = [
+4 -2
View File
@@ -11,6 +11,8 @@ export const invoiceReceiptTableColumns = [
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
{ prop: 'createUserName', label: '创建人', minWidth: 110 },
{ prop: 'createTime', label: '创建时间', minWidth: 170 },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
{ prop: 'kingdeeReceivedInvoiceNo', label: '金蝶单据号', minWidth: 150 },
{ prop: 'kingdeePushStatusName', label: '金蝶状态', minWidth: 130 },
{ prop: 'kingdeePushMsg', label: '失败原因', minWidth: 160 },
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
];
+1
View File
@@ -17,4 +17,5 @@ export const paymentApplicationTableColumns = [
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 120 },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 130 },
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 130 },
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
];
+6
View File
@@ -8,5 +8,11 @@ export const receiptFlowTableColumns = [
{ prop: 'summary', label: '摘要', minWidth: 160 },
{ prop: 'transactionTime', label: '交易时间', minWidth: 170 },
{ prop: 'claimStatusName', label: '认领状态', minWidth: 110, status: true },
{ prop: 'kingdeeClaimStatusName', label: '公告状态', minWidth: 110 },
{ prop: 'claimedAmount', label: '已认领金额', minWidth: 130, money: true },
{ prop: 'unclaimedAmount', label: '金蝶未认领金额', minWidth: 140, money: true },
{ prop: 'pushStatusName', label: '推送状态', minWidth: 110, status: true },
{ prop: 'pushFailReason', label: '推送失败原因', minWidth: 160 },
{ prop: 'kingdeeBillNo', label: '金蝶收款单号', minWidth: 150 },
{ prop: 'kingdeeBillStatusName', label: '金蝶状态', minWidth: 110 },
];
@@ -21,6 +21,7 @@ export const formalSettlementTableColumns = [
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 },
{ label: '金蝶单据状态', prop: 'kingdeeSyncStatus', minWidth: 135 },
{ label: '附件同步', prop: 'attachmentSyncStatusName', minWidth: 100 },
{ label: '创建人', prop: 'createUserName', minWidth: 110 },
{ label: '创建时间', prop: 'createTime', minWidth: 170 },
];
@@ -88,6 +88,8 @@ export const updateFeeFormFields = [
];
export const transferSearchFields = [
{ label: '单据号', prop: 'documentNo', type: 'input' },
{ label: '合同编码', prop: 'contractNo', type: 'input' },
{ label: '合同名称', prop: 'contractName', type: 'input' },
{ label: '批次号', prop: 'batchNo', type: 'input' },
{ label: '生成日期', prop: 'generateDateRange', type: 'daterange' },
-205
View File
@@ -1,205 +0,0 @@
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,
},
],
};
+2 -9
View File
@@ -27,8 +27,7 @@ export const userOption = safe => {
index: true,
selection: true,
addBtn: false,
viewBtn: false,
menuWidth: 160,
viewBtn: true,
dialogType: 'dialog',
dialogWidth: '90%',
labelPosition: 'right',
@@ -38,11 +37,8 @@ export const userOption = safe => {
{
label: '账号名',
prop: 'account',
slot: true,
minWidth: 180,
search: true,
searchOrder: 2,
searchPlaceholder: '支持模糊搜索',
display: false,
},
{
@@ -50,7 +46,6 @@ export const userOption = safe => {
prop: 'realName',
search: true,
searchOrder: 4,
searchPlaceholder: '支持模糊搜索',
display: false,
},
{
@@ -58,7 +53,6 @@ export const userOption = safe => {
prop: 'phone',
search: true,
searchOrder: 3,
searchPlaceholder: '支持模糊搜索',
display: false,
},
{ label: '工号', prop: 'code', display: false },
@@ -66,8 +60,7 @@ export const userOption = safe => {
label: '所属部门',
prop: 'deptName',
slot: true,
minWidth: 200,
overHidden: false,
overHidden: true,
display: false,
},
{ label: '岗位', prop: 'postName', overHidden: true, display: false },
+10 -51
View File
@@ -1,10 +1,7 @@
// 一级 value 与二级 value 都用中文原文:库里 exception_type 存的是大类中文名、
// exception_reason 存子类中文名(小程序上报即如此),此处必须与之一致,
// 否则按异常类型做精确匹配的查询会一行都查不到。
export const exceptionCategoryOptions = [
{
label: '交通异常',
value: '交通异常',
value: 'traffic',
children: [
{ label: '道路拥堵', value: '道路拥堵' },
{ label: '交通事故', value: '交通事故' },
@@ -15,7 +12,7 @@ export const exceptionCategoryOptions = [
},
{
label: '车辆异常',
value: '车辆异常',
value: 'vehicle',
children: [
{ label: '车辆故障维修', value: '车辆故障维修' },
{ label: '车辆事故', value: '车辆事故' },
@@ -27,7 +24,7 @@ export const exceptionCategoryOptions = [
},
{
label: '货物异常',
value: '货物异常',
value: 'cargo',
children: [
{ label: '货损', value: '货损' },
{ label: '货差', value: '货差' },
@@ -38,7 +35,7 @@ export const exceptionCategoryOptions = [
},
{
label: '其他异常',
value: '其他异常',
value: 'other',
children: [
{ label: '变更路线地址', value: '变更路线地址' },
{ label: '其他', value: '其他异常-其他' },
@@ -46,16 +43,6 @@ 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' },
@@ -78,30 +65,20 @@ 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 detailWaybillFields = [
{ label: '运单号', prop: 'waybillNo' },
{ label: '配载单号/总单号', prop: 'loadingOrMasterNo' },
{ label: '车牌号', prop: 'vehicleNo' },
{ label: '项目', prop: 'projectName' },
{ label: '承运商', prop: 'carrierName' },
];
export const detailExceptionFields = [
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' },
{ label: '上报时间', prop: 'reportTime' },
{ label: '异常节点', prop: 'nodeName' },
{ label: '承运商', prop: 'carrierName' },
{ label: '异常类型', prop: 'exceptionTypeName' },
{ label: '异常原因', prop: 'exceptionReason' },
{ label: '上报说明', prop: 'reportDescription', span: 2 },
@@ -112,21 +89,3 @@ 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' },
];
@@ -1,51 +0,0 @@
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',
},
],
};
@@ -1,58 +0,0 @@
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,8 +95,6 @@ export const option = {
align: 'center',
},
{
// 这里是「最新一次提交的状态」缓存,不等于该车当前是否已认证:
// 已认证由生效快照指针决定,曾审核通过即永久保持,重新提交不会让它退回审核中
label: '认证状态',
prop: 'certificationStatus',
slot: true,
@@ -106,10 +104,9 @@ 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,9 +12,6 @@ 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,
@@ -104,7 +101,6 @@ export const option = {
minWidth: 120,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '当前节点',
@@ -112,7 +108,6 @@ export const option = {
minWidth: 120,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '当前处理人',
@@ -120,7 +115,6 @@ export const option = {
minWidth: 130,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '创建人',
@@ -128,7 +122,6 @@ export const option = {
minWidth: 120,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '创建时间',
@@ -139,7 +132,6 @@ export const option = {
minWidth: 170,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
// ========== 新增/编辑表单专用(不进表格)==========
{
@@ -173,7 +165,6 @@ export const option = {
type: 'textarea',
hide: true,
span: 24,
minRows: 2,
maxlength: 200,
showWordLimit: true,
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
+5 -103
View File
@@ -173,10 +173,7 @@
}
.el-dialog .el-col {
// 取消列自身下边距,改由 form-item 统一控制行距;
// flow-root 避免子级 margin 折叠穿透导致换行后行距失效、校验提示被下一行遮挡
margin-bottom: 0;
display: flow-root;
}
.el-dialog .el-form-item,
@@ -184,7 +181,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: 24px;
//margin-bottom: 20px;
}
/* 全站弹窗:底部操作栏浮动效果(参照 business/project-apply)
@@ -365,14 +362,9 @@
}
}
// 4) 弹窗内表单项上下间距:预留绝对定位校验提示(约 14~18px)的可视空间
// 4) 弹窗内表单项默认上下间距 16px(覆盖默认 18px / 22px,与 customer-archive 对齐)
.el-dialog .el-form-item {
margin-bottom: 24px;
}
// 校验提示浮在控件下方,提高层级避免被下一行输入框背景遮挡
.el-form-item__error {
z-index: 2;
//margin-bottom: 16px;
}
// 5) 通用分组白卡组件(<section-card>):标题融入卡头
@@ -454,7 +446,7 @@
.dialog-form--carded {
.el-form-item {
align-items: center;
margin-bottom: 24px;
margin-bottom: 14px;
}
.el-form-item__label {
@@ -652,28 +644,6 @@
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 保证,与项目其他弹窗风格统一
@@ -1025,66 +995,6 @@ 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 指令)
@@ -1163,12 +1073,7 @@ td.el-table-fixed-column--right {
align-items: normal;
}
.el-form-item--default {
margin-bottom: 24px;
}
// 独立表单页(非弹窗)同样避免 el-col 折叠吃掉行距
.el-form .el-col {
display: flow-root;
margin-bottom: 14px;
}
// 配套:星号必须保持「高度 auto + 继承行高」,stretch 才会按首行排布;
@@ -1343,6 +1248,3 @@ td.el-table-fixed-column--right {
cursor: not-allowed;
}
}
.is-ellipsis-link {
overflow: hidden;
}
-434
View File
@@ -1,434 +0,0 @@
/**
* 认证审核通用工具
*
* 认证对象(司机 / 车辆)每次提交产生一条不可变快照记录,「审核」针对的是**一条记录**。
* 记录分两侧:本次**待审快照**与该对象**当前生效快照**(最后一次审核通过的那条)。
* 待审内容在审核通过前不影响生效值,所以审核页必须逐字段对比两侧,而不是只看一条当前值。
*
* 本文件提供三件事:
* 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
@@ -1,14 +0,0 @@
/**
* 合同下拉/选择展示:合同编号/合同名称
*
* @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,92 +13,6 @@ 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
@@ -1,109 +0,0 @@
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),
};
});
}
+22 -99
View File
@@ -6,31 +6,7 @@ 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();
@@ -47,34 +23,8 @@ const isExcelResponse = res => {
);
};
/**
* 解析导入接口响应体为 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);
}
const parseBlobJson = async blob => {
const text = await blob.text();
return text ? JSON.parse(text) : {};
};
@@ -89,21 +39,18 @@ const parseBlobJson = async data => {
* 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。
*/
const resolveErrorMessage = async error => {
const payload = error?.response?.data ?? error?.data;
if (isBusinessJson(payload) && payload.msg) {
return payload.msg;
}
if (isBlobLike(payload) || typeof payload === 'string') {
const blob = error?.response?.data ?? error?.data;
if (blob instanceof Blob) {
try {
const result = await parseBlobJson(payload);
const result = await parseBlobJson(blob);
if (result?.msg) return result.msg;
} catch (ignored) {
// 响应体不是 JSON(如网关返回的 HTML 错误页),退回通用文案
}
}
const fallback = payload?.msg || error?.message;
const fallback = error?.response?.data?.msg || error?.message;
// axios 的英文通用文案对用户没有意义,统一换成中文提示
if (!fallback || /Request failed with status code|\.text is not a function/i.test(fallback)) {
if (!fallback || /Request failed with status code/i.test(fallback)) {
return '导入失败,请检查导入文件后重试';
}
return fallback;
@@ -121,11 +68,8 @@ 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 = typeof blob.arrayBuffer === 'function' ? await blob.arrayBuffer() : blob;
const buf = await blob.arrayBuffer();
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(buf);
await failDetailDecorator(workbook, { businessName });
@@ -133,31 +77,6 @@ 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 组件实例
@@ -189,7 +108,9 @@ 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 标准格式文件');
notifyUploadError(option, new Error('请上传 .xls,.xlsx 标准格式文件'));
if (typeof option.onError === 'function') {
option.onError(new Error('请上传 .xls,.xlsx 标准格式文件'));
}
return;
}
@@ -209,11 +130,12 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
);
vm.$message.warning('部分数据导入失败,已下载失败明细');
} else {
// 成功体常被 axios 拦截器提前解析成对象;优先走业务 JSON,避免再调 Blob.text
const result = isBusinessJson(res?.data) ? res.data : await parseBlobJson(res?.data);
if (Number(result.code) !== 200) {
const result = await parseBlobJson(res.data);
if (result.code !== 200) {
vm.$message.error(result.msg || '导入失败');
notifyUploadError(option, new Error(result.msg || '导入失败'));
if (typeof option.onError === 'function') {
option.onError(new Error(result.msg || '导入失败'));
}
return;
}
vm.$message.success('导入完成');
@@ -227,17 +149,18 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
} else if (typeof vm.initTree === 'function') {
vm.initTree();
}
// 上传组件 onSuccess 约定接收「响应体」,不是完整 axios 响应。
// 若传入 axios 响应对象,部分环境下会按 Fetch Response 调用 .text(),
// 从而在导入已成功后仍弹出「e.text is not a function」。
notifyUploadSuccess(option, res);
if (typeof option.onSuccess === 'function') {
option.onSuccess(res);
}
} catch (error) {
// 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次,
// 只保留一次清晰的中文提示。
if (!error?._notified) {
vm.$message.error(await resolveErrorMessage(error));
}
notifyUploadError(option, error);
if (typeof option.onError === 'function') {
option.onError(error);
}
} finally {
loading.close();
NProgress.done();
+91
View File
@@ -0,0 +1,91 @@
/**
* 金蝶侧发票附件查看/下载公共工具
*
* 票池发票附件为 {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
@@ -1,27 +0,0 @@
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);
});
};
+35 -312
View File
@@ -144,7 +144,7 @@
<script>
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/airport-master';
import { getDetail as getRegionDetail, getLazyTree } from '@/api/base/region';
import { getLazyTree } from '@/api/base/region';
import { exportBlob } from '@/api/common';
import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
@@ -322,20 +322,6 @@ 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',
@@ -402,6 +388,20 @@ 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,51 +578,33 @@ export default {
return targetColumns.find(item => item.prop === targetProp) || null;
},
loadProvinceOptions() {
return getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
this.provinceOptions = (res.data.data || []).map(this.normalizeRegionOption);
const provinceColumn = this.findColumn('provinceCode');
if (provinceColumn) {
provinceColumn.dicData = this.provinceOptions;
}
return this.provinceOptions;
this.findColumn('provinceCode').dicData = this.provinceOptions;
});
},
loadCityOptions(provinceCode) {
if (!provinceCode) {
this.cityOptions = [];
const cityColumn = this.findColumn('cityCode');
if (cityColumn) {
cityColumn.dicData = [];
}
return Promise.resolve([]);
this.findColumn('cityCode').dicData = [];
return Promise.resolve();
}
return getLazyTree(provinceCode).then(res => {
this.cityOptions = (res.data.data || []).map(this.normalizeRegionOption);
const cityColumn = this.findColumn('cityCode');
if (cityColumn) {
cityColumn.dicData = this.cityOptions;
}
return this.cityOptions;
this.findColumn('cityCode').dicData = this.cityOptions;
});
},
loadDistrictOptions(cityCode) {
if (!cityCode) {
this.districtOptions = [];
const districtColumn = this.findColumn('districtCode');
if (districtColumn) {
districtColumn.dicData = [];
}
return Promise.resolve([]);
this.findColumn('districtCode').dicData = [];
return Promise.resolve();
}
this.districtLoading = true;
return getLazyTree(cityCode)
.then(res => {
this.districtOptions = (res.data.data || []).map(this.normalizeRegionOption);
const districtColumn = this.findColumn('districtCode');
if (districtColumn) {
districtColumn.dicData = this.districtOptions;
}
return this.districtOptions;
this.findColumn('districtCode').dicData = this.districtOptions;
})
.finally(() => {
this.districtLoading = false;
@@ -649,22 +631,6 @@ 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);
@@ -676,10 +642,7 @@ export default {
title: row.districtName,
},
];
const districtColumn = this.findColumn('districtCode');
if (districtColumn) {
districtColumn.dicData = this.districtOptions;
}
this.findColumn('districtCode').dicData = this.districtOptions;
},
handleIataChange(value) {
this.form.iataCode = String(value || '')
@@ -735,265 +698,25 @@ 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) {
patch.detailAddress = selection.address;
this.form.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 (
!provinceName &&
!cityName &&
!districtName &&
!(/^\d{6}$/.test(adcode) || String(selection.country || selection.countryName || '').includes('中国'))
selection.longitude !== undefined &&
selection.longitude !== null &&
selection.longitude !== ''
) {
return Promise.resolve();
this.form.longitude = Number(selection.longitude).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();
if (
selection.latitude !== undefined &&
selection.latitude !== null &&
selection.latitude !== ''
) {
this.form.latitude = Number(selection.latitude).toFixed(6);
}
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;
+7 -30
View File
@@ -344,21 +344,15 @@ export default {
getDeptTree(this.userInfo.tenantId).then(res => {
this.deptOptions = this.flattenDept(res.data.data || []);
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
if (deptColumn) {
deptColumn.dicData = this.deptOptions;
}
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: id,
value: item.id,
rawLabel: item.title || item.deptName || item.name,
});
if (item.children && item.children.length) {
@@ -367,19 +361,6 @@ 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;
@@ -986,15 +967,13 @@ export default {
},
searchReset() {
this.query = {};
this.allDept = this.isAdmin ? 1 : 0;
this.page.currentPage = 1;
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = this.normalizeListQuery(params);
this.allDept = this.query.allDept;
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, this.query);
this.onLoad(this.page, params);
done();
},
selectionChange(list) {
@@ -1015,10 +994,7 @@ export default {
},
onLoad(page, params = {}) {
this.loading = true;
const query = this.normalizeListQuery(params);
this.query = query;
this.allDept = query.allDept;
getList(page.currentPage, page.pageSize, query)
getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: this.allDept })
.then(res => {
const data = res.data.data;
this.page.total = data.total;
@@ -1051,7 +1027,8 @@ export default {
},
buildExportParams() {
return {
...this.normalizeListQuery(this.query),
...this.query,
allDept: this.allDept,
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
+83 -326
View File
@@ -264,7 +264,7 @@ import {
changeStatus,
getPortSelect,
} from '@/api/base/port-terminal';
import { getDetail as getRegionDetail, getLazyTree } from '@/api/base/region';
import { 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: 90,
order: 85,
placeholder: '点击选择地图地址',
maxlength: 255,
showWordLimit: true,
@@ -841,60 +841,10 @@ export default {
},
handleCountryNameChange(value) {
const country = this.countryOptions.find(item => item.title === value);
this.setProvinceOptions([]);
this.setCityOptions([]);
this.setDistrictOptions([]);
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;
});
this.loadProvinceOptions(country ? country.id : '');
},
syncCountryName(countryCode) {
const country = this.countryOptions.find(item => item.id === countryCode);
@@ -975,8 +925,10 @@ export default {
return Promise.resolve(this.cityOptionCache[provinceCode]);
}
this.cityLoading = true;
return this.fetchCityOptions(provinceCode)
.then(options => {
return getLazyTree(provinceCode)
.then(res => {
const options = (res.data.data || []).map(this.normalizeRegionOption);
this.cityOptionCache[provinceCode] = options;
this.setCityOptions(options);
return options;
})
@@ -1095,13 +1047,6 @@ 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) {
@@ -1115,224 +1060,17 @@ 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) {
patch.detailAddress = selection.address;
this.form.detailAddress = selection.address;
}
// 换地址时必须同步覆盖经纬度,不能保留上一次选点
if (longitude !== '' && latitude !== '') {
patch.longitude = longitude;
patch.latitude = latitude;
if (selection.longitude !== undefined && selection.longitude !== null && selection.longitude !== '') {
this.form.longitude = Number(selection.longitude).toFixed(6);
}
if (Object.keys(patch).length) {
this.patchDialogForm(patch);
if (selection.latitude !== undefined && selection.latitude !== null && selection.latitude !== '') {
this.form.latitude = Number(selection.latitude).toFixed(6);
}
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) {
@@ -1513,50 +1251,13 @@ 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.resetAddForm();
this.handleCategoryChange('港口', false);
done();
this.$nextTick(() => {
this.$refs.crud?.clearValidate?.();
setTimeout(() => this.disableDialogAutocomplete(), 50);
});
return;
this.form.category = this.form.category || '港口';
this.form.dataSource = this.form.dataSource || '手动录入';
this.form.status = this.form.status || 1;
}
this.handleCategoryChange(this.form.category || '港口');
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
@@ -1591,7 +1292,35 @@ 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(() => {
@@ -1714,14 +1443,42 @@ export default {
</style>
<style lang="scss">
/* 港口码头编辑/新增弹窗:与全局表单项行距保持一致 */
.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;
}
}
/* 港口码头编辑/新增弹窗:对齐 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;
// }
//}
</style>
+45 -307
View File
@@ -144,7 +144,7 @@
<script>
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/railway-station';
import { getDetail as getRegionDetail, getLazyTree } from '@/api/base/region';
import { getLazyTree } from '@/api/base/region';
import { exportBlob } from '@/api/common';
import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
@@ -171,12 +171,7 @@ export default {
}
};
const validateTelegraphCode = (rule, value, callback) => {
const telegraphCode = String(value || '').trim();
if (!telegraphCode) {
callback();
return;
}
if (!/^[A-Z]{3}$/.test(telegraphCode.toUpperCase())) {
if (!/^[A-Z]{3}$/.test(String(value || '').toUpperCase())) {
callback(new Error('电报略码格式不正确或已存在'));
} else {
callback();
@@ -278,7 +273,10 @@ export default {
prop: 'telegraphCode',
minWidth: 100,
maxlength: 3,
rules: [{ validator: validateTelegraphCode, trigger: 'blur' }],
rules: [
{ required: true, message: '电报略码格式不正确或已存在', trigger: 'blur' },
{ validator: validateTelegraphCode, trigger: 'blur' },
],
change: ({ value }) => this.handleTelegraphChange(value),
blur: ({ value }) => this.handleTelegraphChange(value),
},
@@ -295,20 +293,6 @@ 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',
@@ -376,6 +360,20 @@ 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',
@@ -551,51 +549,33 @@ export default {
return targetColumns.find(item => item.prop === targetProp) || null;
},
loadProvinceOptions() {
return getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
this.provinceOptions = (res.data.data || []).map(this.normalizeRegionOption);
const provinceColumn = this.findColumn('provinceCode');
if (provinceColumn) {
provinceColumn.dicData = this.provinceOptions;
}
return this.provinceOptions;
this.findColumn('provinceCode').dicData = this.provinceOptions;
});
},
loadCityOptions(provinceCode) {
if (!provinceCode) {
this.cityOptions = [];
const cityColumn = this.findColumn('cityCode');
if (cityColumn) {
cityColumn.dicData = [];
}
return Promise.resolve([]);
this.findColumn('cityCode').dicData = [];
return Promise.resolve();
}
return getLazyTree(provinceCode).then(res => {
this.cityOptions = (res.data.data || []).map(this.normalizeRegionOption);
const cityColumn = this.findColumn('cityCode');
if (cityColumn) {
cityColumn.dicData = this.cityOptions;
}
return this.cityOptions;
this.findColumn('cityCode').dicData = this.cityOptions;
});
},
loadDistrictOptions(cityCode) {
if (!cityCode) {
this.districtOptions = [];
const districtColumn = this.findColumn('districtCode');
if (districtColumn) {
districtColumn.dicData = [];
}
return Promise.resolve([]);
this.findColumn('districtCode').dicData = [];
return Promise.resolve();
}
this.districtLoading = true;
return getLazyTree(cityCode)
.then(res => {
this.districtOptions = (res.data.data || []).map(this.normalizeRegionOption);
const districtColumn = this.findColumn('districtCode');
if (districtColumn) {
districtColumn.dicData = this.districtOptions;
}
return this.districtOptions;
this.findColumn('districtCode').dicData = this.districtOptions;
})
.finally(() => {
this.districtLoading = false;
@@ -701,269 +681,28 @@ 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) {
patch.detailAddress = selection.address;
this.form.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 (
!provinceName &&
!cityName &&
!districtName &&
!(/^\d{6}$/.test(adcode) || String(selection.country || selection.countryName || '').includes('中国'))
selection.longitude !== undefined &&
selection.longitude !== null &&
selection.longitude !== ''
) {
return Promise.resolve();
this.form.longitude = Number(selection.longitude).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();
if (
selection.latitude !== undefined &&
selection.latitude !== null &&
selection.latitude !== ''
) {
this.form.latitude = Number(selection.latitude).toFixed(6);
}
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();
@@ -996,15 +735,14 @@ export default {
return records.some(item => item[prop] === row[prop] && String(item.id || '') !== rowId);
});
};
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.all([
hasDuplicate({ tmisCode: row.tmisCode }, 'tmisCode'),
hasDuplicate({ telegraphCode: row.telegraphCode }, 'telegraphCode'),
]).then(([tmisExists, telegraphExists]) => {
if (tmisExists) {
return Promise.reject(new Error('该TMIS编码已存在'));
}
if (results[1]) {
if (telegraphExists) {
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: 24px !important;
margin-bottom: 9px !important;
}
</style>
+1 -2
View File
@@ -352,8 +352,7 @@ export default {
return result;
},
normalizeListQuery(params = {}) {
// 搜索条件优先于缓存的 query,避免默认组织 ID 覆盖用户新选择的组织。
const query = { ...this.query, ...params };
const query = { ...params, ...this.query };
const rawDeptId = query.deptId ?? query.searchDeptId;
delete query.searchDeptId;
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
+17 -41
View File
@@ -415,27 +415,19 @@ export default {
});
},
buildDeptTree(tree) {
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);
return tree.map(item => ({
label: item.title || item.deptName || item.name,
value: item.id,
children:
item.children && item.children.length ? this.buildDeptTree(item.children) : undefined,
}));
},
flattenDept(tree, level = 0) {
const result = [];
tree.forEach(item => {
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: id,
value: item.id,
});
if (item.children && item.children.length) {
result.push(...this.flattenDept(item.children, level + 1));
@@ -444,26 +436,7 @@ export default {
return result;
},
defaultDeptId() {
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;
return (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
},
applyDefaultDeptSearch() {
const deptId = this.defaultDeptId();
@@ -1026,9 +999,9 @@ export default {
this.onLoad(this.page, this.query);
},
searchChange(params, done) {
this.query = this.normalizeListQuery(params);
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, this.query);
this.onLoad(this.page, params);
done();
},
selectionChange(list) {
@@ -1051,10 +1024,12 @@ export default {
},
onLoad(page, params = {}) {
this.loading = true;
const query = this.normalizeListQuery(params);
this.query = query;
api
.getList(page.currentPage, page.pageSize, query)
.getList(page.currentPage, page.pageSize, {
...params,
...this.query,
allDept: this.isAdmin ? 1 : 0,
})
.then(res => {
const result = res.data.data;
this.page.total = result.total;
@@ -1067,7 +1042,8 @@ export default {
},
buildExportParams() {
return {
...this.normalizeListQuery(this.query),
...this.query,
allDept: this.isAdmin ? 1 : 0,
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="unitPriceColumnLabel" width="180"
<el-table-column label="单价(元)" 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,7 +379,6 @@ 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));
/** 计费要素与计量单位维度的对应关系(这些要素可选计费单位) */
@@ -387,15 +386,12 @@ const BILLING_ELEMENT_DIMENSION_MAP = {
按重量: '重量',
按体积: '体积',
按数量: '数量',
按里程: '里程',
'按吨·公里': '吨·公里',
'固定金额(整单一口价)': '整车',
};
const BILLING_UNIT_SELECTABLE_ELEMENTS = Object.keys(BILLING_ELEMENT_DIMENSION_MAP);
/** 对应维度下无计量单位时的默认计费单位 */
const DEFAULT_BILLING_UNIT_MAP = {
/** 固定计费单位(不可编辑) */
const FIXED_BILLING_UNIT_MAP = {
按里程: '公里',
'按吨·公里': '吨·公里',
'按吨·公里': '吨公里',
'固定金额(整单一口价)': '单',
};
const defaultRule = () => ({
@@ -432,7 +428,6 @@ export default {
value: { type: Object, default: () => ({}) },
index: { type: Number, default: -1 },
readonly: Boolean,
settlementCurrency: { type: String, default: 'RMB' },
},
emits: ['update:modelValue', 'save'],
data() {
@@ -502,9 +497,6 @@ export default {
emitPath: true,
};
},
unitPriceColumnLabel() {
return `单价(${currencyUnit(this.settlementCurrency)})`;
},
},
mounted() {
this.loadDictionaries();
@@ -660,61 +652,58 @@ export default {
}),
]).finally(() => {
this.unitLoading = false;
(this.draft.rules || []).forEach(row => this.syncBillingUnit(row));
});
},
measurementDimension(billingElement) {
return BILLING_ELEMENT_DIMENSION_MAP[billingElement] || '';
},
defaultBillingUnit(billingElement) {
return DEFAULT_BILLING_UNIT_MAP[String(billingElement || '').trim()] || '';
fixedBillingUnit(billingElement) {
return FIXED_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);
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 }];
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);
}
return options;
return [];
},
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);
const defaultUnit = this.defaultBillingUnit(row?.billingElement);
const current = String(row.billingUnit || '').trim();
if (options.some(item => String(item.value) === current)) {
return;
if (!options.some(item => String(item.value) === String(row.billingUnit || ''))) {
row.billingUnit = '';
}
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="formatContractOptionLabel(item)"
:label="item.contractName"
:value="item.id"
/>
</el-select>
@@ -2065,22 +2065,11 @@
>
<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
class-name="is-ellipsis-link"
>
<el-table-column label="文件名" min-width="200" align="left" show-overflow-tooltip>
<template #default="{ row }">
<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>
<el-link type="primary" @click="previewAttachment(row)">
{{ row.originalName || row.name }}
</el-link>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220" align="center">
@@ -2349,16 +2338,14 @@
<div class="business-crud-page__transport-plan-detail-field is-attachments">
<span>附件</span>
<div class="business-crud-page__transport-plan-detail-attachments">
<span
<el-link
v-for="file in attachmentRows"
:key="file.url || file.name || file.originalName"
class="attachment-file-name"
role="button"
tabindex="0"
type="primary"
@click="previewAttachment(file)"
@keydown.enter="previewAttachment(file)"
@keydown.space.prevent="previewAttachment(file)"
>{{ attachmentName(file) }}</span>
>
{{ attachmentName(file) }}
</el-link>
<span v-if="!attachmentRows.length">-</span>
</div>
</div>
@@ -2544,16 +2531,14 @@
<div class="business-crud-page__waybill-attachments">
<span class="business-crud-page__waybill-label">附件</span>
<div>
<span
<el-link
v-for="file in attachmentRows"
:key="file.url || file.name || file.originalName"
class="attachment-file-name"
role="button"
tabindex="0"
type="primary"
@click="previewAttachment(file)"
@keydown.enter="previewAttachment(file)"
@keydown.space.prevent="previewAttachment(file)"
>{{ file.originalName || file.name || '-' }}</span>
>
{{ file.originalName || file.name || '-' }}
</el-link>
<span v-if="!attachmentRows.length">-</span>
</div>
</div>
@@ -2835,21 +2820,11 @@
</div>
<el-table :data="attachmentRows" empty-text="暂无附件">
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column
label="文件名"
min-width="260"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<el-table-column label="文件名" min-width="260">
<template #default="{ row }">
<span
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(row)"
@keydown.enter="previewAttachment(row)"
@keydown.space.prevent="previewAttachment(row)"
>{{ attachmentName(row) }}</span>
<el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }}
</el-link>
</template>
</el-table-column>
<el-table-column
@@ -3081,7 +3056,7 @@
<el-option
v-for="item in transportPlanImportContractOptions"
:key="item.id"
:label="formatContractOptionLabel(item)"
:label="item.contractName"
:value="item.id"
/>
</el-select>
@@ -4250,22 +4225,11 @@
>
<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
class-name="is-ellipsis-link"
>
<el-table-column label="文件名" min-width="200" align="center" show-overflow-tooltip>
<template #default="{ row }">
<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>
<el-link type="primary" @click="previewAttachment(row, dispatchItemAttachmentRows)">
{{ attachmentName(row) }}
</el-link>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220" align="center">
@@ -5278,10 +5242,8 @@ 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';
@@ -5860,7 +5822,7 @@ export default {
billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'],
reconciliationModeOptions: ['明细逐行核对', '按账单汇总核对', '跳过自动核对'],
packageOptions,
quantityUnitOptions: [],
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
attachmentFileTypes: [
'pdf',
'bmp',
@@ -5903,6 +5865,7 @@ export default {
this.ensureBillingUnitOptions();
}
if (this.config.enableTaskInfoForm) {
this.loadTaskQuantityUnitOptions();
this.loadTaskFeeUnitOptions();
this.ensureBillingCargoTypeOptions();
}
@@ -5923,7 +5886,6 @@ export default {
} else {
this.consumeTemplateCreatePayload();
}
this.loadTaskQuantityUnitOptions();
},
computed: {
...mapGetters(['permission', 'userInfo']),
@@ -6627,7 +6589,6 @@ export default {
},
},
methods: {
formatContractOptionLabel,
initStandaloneFormPage() {
if (!this.isStandaloneFormPage) return;
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
@@ -7828,6 +7789,8 @@ export default {
['contractName', '客户合同'],
['planName', '计划名称'],
['transportType', '运输方式'],
['planStartDate', '计划开始日期'],
['planEndDate', '计划结束日期'],
['departureAddress', '发货地址'],
['arrivalAddress', '收货地址'],
];
@@ -9307,10 +9270,9 @@ export default {
return Promise.resolve(this.taskQuantityUnitOptions);
}
this.taskQuantityUnitLoading = true;
return loadMeasurementUnitOptions()
.then(options => {
this.taskQuantityUnitOptions = options;
this.quantityUnitOptions = options.map(item => item.value);
return getDictionary({ code: 'quantity_unit' })
.then(res => {
this.taskQuantityUnitOptions = this.normalizeDictOptions(res.data.data || []);
return this.taskQuantityUnitOptions;
})
.finally(() => {
@@ -14149,7 +14111,7 @@ export default {
&__dispatch-shipping-form {
:deep(.el-form-item) {
margin-bottom: 24px;
margin-bottom: 14px;
}
}
@@ -14672,7 +14634,7 @@ export default {
width: 100%;
:deep(.el-form-item) {
margin-bottom: 24px;
margin-bottom: 14px;
}
:deep(.el-select),
@@ -16289,7 +16251,7 @@ export default {
}
:global(.business-crud-page__mileage-dialog .business-crud-page__mileage-form .el-form-item) {
margin-bottom: 24px;
margin-bottom: 16px;
}
: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 type="selection" width="55" align="center" />
<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 v-if="attachmentType" label="附件类型" min-width="160" align="center">
<template #default="{ row, $index }">
@@ -45,17 +45,11 @@
align="left"
header-align="left"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<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>
<el-link type="primary" @click="preview?.(row, rows)">{{
attachmentName(row)
}}</el-link>
</template>
</el-table-column>
<el-table-column v-if="description" label="附件描述" min-width="220">
@@ -179,41 +173,13 @@ 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 },
];
export const defaultContractSupportingAttachmentTypeOptions = [
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
];
/** @deprecated 兼容旧引用,实际选项由业务字典动态加载 */
/** @deprecated 兼容旧引用,实际选项由业务字典 contract_attachment_type 动态加载 */
export const contractAttachmentTypeOptions = defaultContractAttachmentTypeOptions;
export const isContractSealMaterialLocation = location => {
const text = String(location || '');
return text === CONTRACT_SEAL_MATERIAL_LOCATION || text === '合同文件';
};
export const attachmentFileTypes = [
'pdf',
'bmp',
@@ -268,11 +234,8 @@ const isOtherAttachmentTypeOption = item => {
const attachmentTypeKeywords = fileType => {
const text = String(fileType || '');
if (text.includes('双章')) return ['双章归档文件', '双章归档', '双章'];
if (
text.includes('用印') ||
(text.includes('合同') && !text.includes('其他') && !text.includes('其它'))
) {
return ['用印材料', '合同文件', '用印', '合同'];
if (text.includes('合同') && !text.includes('其他') && !text.includes('其它')) {
return ['合同文件', '合同'];
}
if (isOtherAttachmentTypeOption({ label: text, value: text })) return [];
return text ? [text] : [];
@@ -291,13 +254,8 @@ export const resolveContractAttachmentType = (
const list = options.length ? options : defaultContractAttachmentTypeOptions;
const otherType = resolveOtherAttachmentType(list);
const values = list.map(item => item.value);
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 (values.includes(row.fileType)) return row.fileType;
const matchedByLabel = list.find(item => String(item.label) === String(row.fileType));
if (matchedByLabel) return matchedByLabel.value;
const fileName = normalizeAttachmentTypeText(attachmentName(row));
if (!fileName) return otherType;
@@ -325,32 +283,17 @@ export const normalizeContractFileRows = (rows, options = defaultContractAttachm
description: row.description || '',
}));
const mapDictOptions = (response, dictCode = CONTRACT_ATTACHMENT_TYPE_DICT) => {
const mapDictOptions = response => {
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 => {
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 };
})
.map(item => ({
label: item.dictValue,
value: item.dictKey,
}))
.filter(item => item.label && item.value);
};
const defaultOptionsByDictCode = dictCode =>
dictCode === CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT
? [...defaultContractSupportingAttachmentTypeOptions]
: [...defaultContractAttachmentTypeOptions];
export default {
name: 'ContractAttachmentSection',
components: { UploadFilled },
@@ -364,13 +307,8 @@ export default {
markApprovedOnUpload: Boolean,
useUploadDialog: Boolean,
dialogOnly: Boolean,
attachmentLocation: { type: String, default: CONTRACT_SEAL_MATERIAL_LOCATION },
attachmentLocation: { type: String, default: '合同文件' },
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'],
@@ -379,9 +317,8 @@ export default {
selected: [],
attachmentFileTypes,
contractAttachmentTypeOptions: [...defaultContractAttachmentTypeOptions],
activeAttachmentTypeDictCode: CONTRACT_ATTACHMENT_TYPE_DICT,
uploadDialogVisible: false,
uploadDialogLocation: CONTRACT_SEAL_MATERIAL_LOCATION,
uploadDialogLocation: '合同文件',
uploadDialogType: resolveOtherAttachmentType(defaultContractAttachmentTypeOptions),
uploadDialogFiles: [],
uploadDialogFileList: [],
@@ -400,91 +337,48 @@ 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.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);
},
if (options.length) return options;
return ['合同文件', '其它附件'];
},
},
created() {
this.loadAttachmentTypeOptions(this.resolvedAttachmentTypeDictCode, true);
this.loadAttachmentTypeOptions();
},
methods: {
attachmentName,
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 })
loadAttachmentTypeOptions() {
return getBizDictionary({ code: 'contract_attachment_type' })
.then(res => {
// 切换字典期间若已发起更新请求,忽略过期响应
if (this.activeAttachmentTypeDictCode !== code) return;
const options = mapDictOptions(res, code);
const options = mapDictOptions(res);
if (options.length) {
// 字典缺少「其他文件」时补上,保证未匹配文件名可默认选中
this.contractAttachmentTypeOptions = options.some(isOtherAttachmentTypeOption)
? options
: [...options, { label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER }];
} 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);
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);
}
}
})
.catch(() => {
if (this.activeAttachmentTypeDictCode !== code) return;
this.contractAttachmentTypeOptions = fallbackOptions;
this.uploadDialogType = this.defaultAttachmentType;
// 字典加载失败时保留默认选项
});
},
isRowLocked(row) {
@@ -494,14 +388,13 @@ export default {
this.$emit('update:rows', Array.isArray(rows) ? rows.map(item => ({ ...item })) : []);
},
enrichUploadedRow(row, existing) {
const uploadUserName = this.currentUploadUserName();
const uploadUserName = this.$store.getters.userInfo?.realName || '';
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
const next = {
...existing,
...row,
description: row.description || existing?.description || '',
// 新上传行始终写入当前用户;已有行保留原上传人
uploadUserName: existing?.uploadUserName || uploadUserName || row.uploadUserName || '',
uploadUserName: existing?.uploadUserName || row.uploadUserName || uploadUserName,
uploadTime: existing?.uploadTime || row.uploadTime || uploadTime,
approved: existing?.approved || row.approved || false,
};
@@ -541,38 +434,20 @@ export default {
resolveTypeByLocation(location) {
const options = this.contractAttachmentTypeOptions || [];
const locationText = String(location || '');
if (
isContractSupportingAttachmentLocation(locationText) ||
locationText.includes('佐证') ||
locationText.includes('其它') ||
locationText.includes('其他')
) {
if (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() {
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();
this.uploadDialogType = this.resolveTypeByLocation(this.uploadDialogLocation);
},
openUploadDialog() {
const options = this.resolvedLocationOptions;
@@ -669,13 +544,13 @@ export default {
this.$message.warning('请选择附件类型');
return;
}
const uploadUserName = this.currentUploadUserName();
const uploadUserName = this.$store.getters.userInfo?.realName || '';
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: uploadUserName || row.uploadUserName || '',
uploadUserName: row.uploadUserName || uploadUserName,
uploadTime: row.uploadTime || uploadTime,
...(this.markApprovedOnUpload ? { approved: true } : {}),
}));
@@ -732,15 +607,7 @@ export default {
downloadFileByUrl(url, attachmentName(row));
},
batchDownload() {
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);
});
(this.selected.length ? this.selected : this.rows || []).forEach(this.download);
},
formatSize(size) {
const value = Number(size);
@@ -44,16 +44,13 @@
<dt>附件</dt>
<dd>
<template v-if="attachments.length">
<span
<el-link
v-for="file in attachments"
:key="file.url || file.link || file.name || file.originalName"
class="attachment-file-name"
role="button"
tabindex="0"
type="primary"
@click="previewAttachment(file)"
@keydown.enter="previewAttachment(file)"
@keydown.space.prevent="previewAttachment(file)"
>{{ attachmentName(file) }}</span>
>{{ attachmentName(file) }}</el-link
>
</template>
<span v-else>-</span>
</dd>
@@ -333,7 +330,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; } .attachment-file-name { font-size: 14px; max-width: 100%; } }
.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; } }
.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 v-for="item in priceUnitOptions(route)" :key="item.value" :label="item.label" :value="item.value" /></el-select></template>
<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>
</el-input>
</el-form-item>
</el-col>
@@ -317,13 +317,6 @@
<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 => {
@@ -354,8 +347,7 @@ export default {
carrierLoading: false,
driverLoading: false,
escortLoading: false,
quantityUnitOptions: [],
quantityUnitLoading: false,
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
};
},
computed: {
@@ -397,24 +389,10 @@ 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;
@@ -434,10 +412,8 @@ 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 {
@@ -451,20 +427,6 @@ 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 : [];
@@ -639,23 +601,8 @@ 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 = {}) {
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}`
);
},
freightGroupPriceUnit(group = {}) { return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${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;
@@ -665,10 +612,6 @@ 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 || '';
});
@@ -691,7 +634,7 @@ export default {
})),
});
},
currencyLabel(value) { return currencyUnit(value); },
currencyLabel(value) { return ({ CNY: '元', RMB: '元', USD: '美元', EUR: '欧元' })[value] || value || '元'; },
quantityNumber(value) { return Number(value || 0); },
goodsKey(goods = {}, includeUnit = true) {
return [
@@ -881,7 +824,6 @@ 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 => {
@@ -893,10 +835,7 @@ export default {
quantity: item.dispatchQuantity || '',
quantityUnit: item.quantityUnit || '',
unitPrice: old.unitPrice || '',
priceUnit:
old.priceUnit ||
resolvePriceUnitByQuantityUnit(item.quantityUnit || '吨', currency) ||
`${currencyUnit(currency)}/${item.quantityUnit || '吨'}`,
priceUnit: old.priceUnit || `元/${item.quantityUnit || '吨'}`,
};
});
},
@@ -949,11 +888,9 @@ export default {
},
carrierOptionLabel(item) {
const duplicateCount = this.carrierOptions.filter(option => option.carrierName === item.carrierName).length;
if (duplicateCount <= 1) {
return item.carrierName;
}
const contractLabel = formatContractOptionLabel(item);
return contractLabel ? `${item.carrierName}(${contractLabel})` : item.carrierName;
return duplicateCount > 1
? `${item.carrierName}(${item.contractName || item.contractId})`
: item.carrierName;
},
driverOptionLabel(item) { return item.driverName || item.name || ''; },
driverOptionKey(item) { return item.id || item.driverId || this.driverOptionLabel(item) || item.mobile; },
@@ -1260,7 +1197,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: 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 { :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__compact-label {
:deep(.el-form-item__label) {
@@ -27,7 +27,7 @@
><el-option
v-for="contract in contracts"
:key="contract.id"
:label="formatContractOptionLabel(contract)"
:label="contract.contractName"
:value="contract.id" /></el-select></el-form-item></el-col
><el-col :span="6"
><el-form-item label="总单号" required
@@ -261,22 +261,11 @@
>
<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
class-name="is-ellipsis-link"
>
<el-table-column label="文件名" min-width="240" align="center" show-overflow-tooltip>
<template #default="{ row }">
<span
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(row)"
@keydown.enter="previewAttachment(row)"
@keydown.space.prevent="previewAttachment(row)"
>{{ attachmentName(row) }}</span>
<el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }}
</el-link>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220" align="center">
@@ -600,7 +589,6 @@ 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';
@@ -609,10 +597,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 }),
@@ -715,11 +703,10 @@ export default {
'zip',
],
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
quantityUnitOptions: [],
quantityUnitLoading: false,
quantityUnitOptions,
form: {
routes: [{ segmentNo: '段1', transportType: '' }],
goods: [{ quantityUnit: '吨' }],
goods: [{ quantityUnit: quantityUnitOptions[0] }],
attachmentsJson: '[]',
transportOrganizationType: '多式联运',
},
@@ -786,7 +773,6 @@ 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);
@@ -796,23 +782,6 @@ 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 || {};
},
@@ -989,7 +958,7 @@ export default {
finalTransportType: hasFinalSegment
? finalSegment.transportType
: detail.finalTransportType || '',
goods: detail.goods || [{ quantityUnit: this.defaultQuantityUnit() }],
goods: detail.goods || [{ quantityUnit: quantityUnitOptions[0] }],
attachmentsJson: detail.attachmentsJson || '[]',
transportOrganizationType: '多式联运',
};
@@ -1690,7 +1659,7 @@ export default {
'',
cargoTypePath,
quantity: row.quantity || '',
quantityUnit: row.quantityUnit || this.defaultQuantityUnit(),
quantityUnit: row.quantityUnit || quantityUnitOptions[0],
packageType: row.packageType || row.package || '',
brand: row.brand || '',
specification: row.specification || row.spec || '',
@@ -1816,7 +1785,7 @@ export default {
},
addGoods(index) {
this.form.goods.splice(index + 1, 0, {
quantityUnit: this.defaultQuantityUnit(),
quantityUnit: quantityUnitOptions[0],
materialCode: '',
deviceCode: '',
remark: '',
@@ -2078,7 +2047,7 @@ export default {
font-size: 16px;
}
.el-form-item {
margin-bottom: 24px;
margin-bottom: 14px;
}
}
.master-editor__quantity-field {
@@ -1,305 +0,0 @@
<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="formatContractOptionLabel(item)"
:label="item.contractName"
:value="item.id"
/>
</el-select>
@@ -454,20 +454,11 @@
>
<el-table-column type="selection" width="55" />
<el-table-column type="index" label="序号" width="70" />
<el-table-column
label="文件名"
min-width="220"
show-overflow-tooltip
class-name="is-ellipsis-link"
<el-table-column label="文件名" min-width="220"
><template #default="{ row }"
><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-link type="primary" @click="previewAttachment(row)">{{
attachmentName(row)
}}</el-link></template
></el-table-column
>
<el-table-column label="附件描述" min-width="220"
@@ -730,21 +721,11 @@
</div>
<el-table :data="detailAttachmentRows" border empty-text="暂无附件">
<el-table-column type="index" label="序号" width="70" />
<el-table-column
label="文件名"
min-width="220"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<el-table-column label="文件名" min-width="220">
<template #default="{ row }">
<span
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(row)"
@keydown.enter="previewAttachment(row)"
@keydown.space.prevent="previewAttachment(row)"
>{{ attachmentName(row) }}</span>
<el-link type="primary" @click="previewAttachment(row)">{{
attachmentName(row)
}}</el-link>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220">
@@ -1117,8 +1098,6 @@ 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';
@@ -1299,8 +1278,7 @@ export default {
cargoTypeRequest: null,
transportCargoRows: [],
packageOptions,
quantityUnitOptions: [],
quantityUnitLoading: false,
quantityUnitOptions: ['吨', '千克', '立方米', '立方', '公里', '单', '件', '车', '箱', '托盘'],
feeUnitOptions: [],
currencyOptions: [],
currencyLoading: false,
@@ -1547,26 +1525,11 @@ 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 || [];
@@ -3543,7 +3506,7 @@ export default {
}
.shipping-template-page__detail-form :deep(.el-form-item) {
min-width: 0;
margin-bottom: 24px;
margin-bottom: 16px;
}
.shipping-template-page__detail-form :deep(.el-form-item__label) {
color: #909399;
@@ -3732,7 +3695,7 @@ export default {
}
:global(.shipping-template-form-page-dialog .avue-group .el-form-item) {
margin-bottom: 24px;
margin-bottom: 16px;
}
:global(.shipping-template-form-page-dialog .avue-form__menu) {
@@ -149,7 +149,7 @@
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="formatContractOptionLabel(item)"
:label="item.contractName"
:value="item.id"
/>
</el-select>
@@ -492,22 +492,11 @@
>
<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
class-name="is-ellipsis-link"
>
<el-table-column label="文件名" min-width="200" align="left" show-overflow-tooltip>
<template #default="{ row }">
<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>
<el-link type="primary" @click="previewAttachment(row)">
{{ row.originalName || row.name }}
</el-link>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220" align="center">
@@ -684,16 +673,14 @@
<div class="transport-plan-page__transport-plan-detail-field is-attachments">
<span>附件</span>
<div class="transport-plan-page__transport-plan-detail-attachments">
<span
<el-link
v-for="file in attachmentRows"
:key="file.url || file.name || file.originalName"
class="attachment-file-name"
role="button"
tabindex="0"
type="primary"
@click="previewAttachment(file)"
@keydown.enter="previewAttachment(file)"
@keydown.space.prevent="previewAttachment(file)"
>{{ attachmentName(file) }}</span>
>
{{ attachmentName(file) }}
</el-link>
<span v-if="!attachmentRows.length">-</span>
</div>
</div>
@@ -868,21 +855,11 @@
</div>
<el-table :data="attachmentRows" empty-text="暂无附件">
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column
label="文件名"
min-width="260"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<el-table-column label="文件名" min-width="260">
<template #default="{ row }">
<span
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(row)"
@keydown.enter="previewAttachment(row)"
@keydown.space.prevent="previewAttachment(row)"
>{{ attachmentName(row) }}</span>
<el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }}
</el-link>
</template>
</el-table-column>
<el-table-column
@@ -1003,7 +980,7 @@
<el-option
v-for="item in transportPlanImportContractOptions"
:key="item.id"
:label="formatContractOptionLabel(item)"
:label="item.contractName"
:value="item.id"
/>
</el-select>
@@ -1767,11 +1744,11 @@
><el-select
:model-value="dispatchFreightGroupPriceUnit(group)"
class="transport-plan-page__dispatch-unit-select"
:placeholder="defaultPriceUnit"
placeholder="元/吨"
@change="value => handleDispatchFreightGroupPriceUnitChange(group, value)"
@visible-change="visible => visible && loadTaskFeeUnitOptions()"
><el-option
v-for="item in taskFeeUnitSelectOptions"
v-for="item in taskFeeUnitOptions"
:key="item.value"
:label="item.label"
:value="item.value" /></el-select
@@ -1992,17 +1969,17 @@
<el-select
v-model="dispatchItemForm.priceUnit"
class="transport-plan-page__dispatch-unit-select"
:placeholder="defaultPriceUnit"
placeholder="元/吨"
:loading="taskFeeUnitLoading"
@visible-change="visible => visible && loadTaskFeeUnitOptions()"
>
<el-option
v-if="!taskFeeUnitSelectOptions.some(item => item.value === defaultPriceUnit)"
:label="defaultPriceUnit"
:value="defaultPriceUnit"
v-if="!taskFeeUnitOptions.some(item => item.value === '元/吨')"
label="元/吨"
value="元/吨"
/>
<el-option
v-for="item in taskFeeUnitSelectOptions"
v-for="item in taskFeeUnitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
@@ -2224,22 +2201,11 @@
>
<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
class-name="is-ellipsis-link"
>
<el-table-column label="文件名" min-width="200" align="center" show-overflow-tooltip>
<template #default="{ row }">
<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>
<el-link type="primary" @click="previewAttachment(row, dispatchItemAttachmentRows)">
{{ attachmentName(row) }}
</el-link>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220" align="center">
@@ -2775,15 +2741,7 @@ 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';
@@ -3248,7 +3206,7 @@ export default {
billingCargoTypeLoading: false,
billingCargoTypeRequest: null,
packageOptions,
quantityUnitOptions: [],
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
attachmentFileTypes: [
'pdf',
'bmp',
@@ -3287,7 +3245,6 @@ export default {
} else {
this.consumeTemplateCreatePayload();
}
this.loadTaskQuantityUnitOptions();
},
computed: {
...mapGetters(['permission', 'userInfo']),
@@ -3652,22 +3609,6 @@ 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);
},
@@ -3778,7 +3719,6 @@ export default {
},
},
methods: {
formatContractOptionLabel,
initStandaloneFormPage() {
if (!this.isStandaloneFormPage) return;
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
@@ -4367,6 +4307,8 @@ export default {
['contractName', '客户合同'],
['planName', '计划名称'],
['transportType', '运输方式'],
['planStartDate', '计划开始日期'],
['planEndDate', '计划结束日期'],
['departureAddress', '发货地址'],
['arrivalAddress', '收货地址'],
];
@@ -4862,49 +4804,19 @@ 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();
@@ -5164,40 +5076,15 @@ export default {
return Promise.resolve(this.taskQuantityUnitOptions);
}
this.taskQuantityUnitLoading = true;
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 getDictionary({ code: 'quantity_unit' })
.then(res => {
this.taskQuantityUnitOptions = this.normalizeDictOptions(res.data.data || []);
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);
@@ -5259,9 +5146,6 @@ export default {
});
},
normalizeTransportCargoRow(row = {}) {
const quantityUnit = String(
row.quantityUnit || row.goodsQuantityUnit || row.unit || ''
).trim();
const cargo = {
...defaultTransportCargo(),
...row,
@@ -5278,14 +5162,10 @@ 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 = []) {
@@ -5303,7 +5183,7 @@ export default {
},
addTransportCargoRow(index = -1, row = {}) {
const nextRow = this.normalizeTransportCargoRow({
priceUnit: this.form.priceUnit || this.defaultPriceUnit,
priceUnit: this.form.priceUnit || '元/吨',
...row,
});
if (
@@ -7714,9 +7594,6 @@ 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
);
@@ -7726,11 +7603,7 @@ export default {
});
const goodsRows = this.parseJsonArray(this.dispatchItemForm.goodsJson);
this.dispatchItemCargoRows = (goodsRows.length ? goodsRows : [this.dispatchItemForm]).map(
item =>
this.normalizeTransportCargoRow({
...item,
quantityUnit: item.quantityUnit || this.dispatchItemForm.quantityUnit || defaultUnit,
})
item => this.normalizeTransportCargoRow(item)
);
const hasMultiplePlanGoods = this.dispatchPlanGoodsRows.length > 1;
const shouldAutoFillFullGoods =
@@ -7740,18 +7613,11 @@ 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: '' })
)
).map(goods =>
this.normalizeTransportCargoRow({
...goods,
quantityUnit: goods.quantityUnit || defaultUnit || TRANSPORT_PLAN_QUANTITY_UNIT,
})
);
this.dispatchItemCargoRows = unscheduledGoodsRows.length
? unscheduledGoodsRows
: this.dispatchPlanGoodsRows.map(goods =>
this.normalizeTransportCargoRow({ ...goods, quantity: '' })
);
}
if (!this.dispatchItemCargoRows.length) {
this.dispatchItemCargoRows = [this.normalizeTransportCargoRow()];
@@ -7766,10 +7632,7 @@ export default {
this.dispatchItemCargoRows = this.dispatchItemCargoRows.map((cargo, itemIndex) => ({
...cargo,
unitPrice: cargo.unitPrice || freightInfo.freightItems[itemIndex]?.unitPrice || '',
priceUnit:
cargo.priceUnit ||
freightInfo.freightItems[itemIndex]?.priceUnit ||
this.defaultPriceUnit,
priceUnit: cargo.priceUnit || freightInfo.freightItems[itemIndex]?.priceUnit || '元/吨',
freightAmount:
cargo.freightAmount || freightInfo.freightItems[itemIndex]?.freightAmount || '',
}));
@@ -8018,7 +7881,7 @@ export default {
return value;
},
addDispatchCargoRow(index = -1) {
const nextRow = this.normalizeTransportCargoRow({ priceUnit: this.defaultPriceUnit });
const nextRow = this.normalizeTransportCargoRow({ priceUnit: '元/吨' });
if (index > -1) this.dispatchItemCargoRows.splice(index + 1, 0, nextRow);
else this.dispatchItemCargoRows.push(nextRow);
},
@@ -8062,14 +7925,7 @@ export default {
return prices[0];
},
dispatchFreightGroupPriceUnit(group = {}) {
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}`
);
return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : '');
},
dispatchFreightGroupAmount(group = {}) {
const amount = (group.rows || []).reduce(
@@ -8453,10 +8309,7 @@ export default {
this.dispatchItemForm.cargoName ||
'',
quantity: quantityText,
quantityUnit:
firstGoods.quantityUnit ||
this.dispatchItemForm.quantityUnit ||
TRANSPORT_PLAN_QUANTITY_UNIT,
quantityUnit: firstGoods.quantityUnit || this.dispatchItemForm.quantityUnit || '',
unitPrice: firstGoods.unitPrice || '',
priceUnit: firstGoods.priceUnit || '',
freightTotal: this.dispatchItemFreightTotal,
@@ -8471,7 +8324,7 @@ export default {
unitPrice: cargo.unitPrice || '',
priceUnit: cargo.priceUnit || '',
quantity: cargo.quantity || '',
quantityUnit: cargo.quantityUnit || firstGoods.quantityUnit || TRANSPORT_PLAN_QUANTITY_UNIT,
quantityUnit: cargo.quantityUnit || '',
freightAmount: this.dispatchCargoFreightAmount(cargo),
})),
}),
@@ -8560,40 +8413,26 @@ export default {
return {
id: this.dispatchRow.id,
mode,
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([
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([
{
cargoName: row.cargoName,
cargoType: row.cargoType,
quantity: row.quantity,
quantityUnit,
quantityUnit: row.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) {
@@ -8841,7 +8680,7 @@ export default {
&__dispatch-shipping-form {
:deep(.el-form-item) {
margin-bottom: 24px;
margin-bottom: 14px;
}
}
@@ -10032,7 +9871,7 @@ export default {
}
:global(.transport-plan-form-page-dialog .avue-group .el-form-item) {
margin-bottom: 24px;
margin-bottom: 16px;
}
:global(.transport-plan-form-page-dialog .avue-group .dialog-section-title) {
@@ -270,22 +270,15 @@
prop="mileage"
label="里程(km)"
width="100"
>
<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
/><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
prop="actualStartDate"
label="实际发货时间"
width="160"
@@ -381,7 +374,7 @@
<el-option
v-for="item in contracts"
:key="item.id"
:label="formatContractOptionLabel(item)"
:label="item.contractName"
:value="item.id"
/>
</el-select>
@@ -617,7 +610,6 @@ 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';
@@ -1087,40 +1079,19 @@ 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(buildBatchQueryParams());
const res = await api.getImportBatches({ ...query, current: page.current, size: page.size });
batches.value = res.data?.data?.records || [];
page.total = res.data?.data?.total || 0;
};
onMounted(() => {
if (props.standalone) {
loadListOptions();
loadBatches();
}
if (props.standalone) loadBatches();
if (props.createPage) openCreatePage();
});
watch(
() => props.modelValue,
value => {
if (value) {
loadListOptions();
loadBatches();
}
if (value) loadBatches();
}
);
const resetQuery = () => {
@@ -1135,7 +1106,7 @@ const loadDetails = async () => {
current: detailPage.current,
size: detailPage.size,
});
details.value = (res.data?.data?.records || []).map(normalizeDetailRow);
details.value = res.data?.data?.records || [];
detailPage.total = res.data?.data?.total || 0;
};
const openCreate = () => prepareCreateForm(true);
@@ -1184,7 +1155,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) => ({
...normalizeDetailRow(item),
...item,
_key: `${id || 'row'}-${index}`,
batchNo: item.batchNo || form.batchNo,
}));
@@ -1326,43 +1297,6 @@ 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('请先选择项目、客户合同和承运信息,再导入运单明细');
@@ -1436,9 +1370,6 @@ 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);
@@ -1687,7 +1618,6 @@ const downloadTemplate = async () => {
ElMessage.error('模板下载失败');
}
};
const isValidMobile = value => /^\d{11}$/.test(String(value || '').trim());
const validateImportRows = () => {
if (!rows.value.length) {
ElMessage.warning('请上传至少一条运单明细');
@@ -1699,11 +1629,6 @@ 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;
};
@@ -1731,6 +1656,31 @@ 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
+16 -83
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,15 +63,6 @@
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
@@ -100,13 +91,12 @@
<template #suffix>天</template>
</el-input>
</el-form-item>
<el-form-item label="合同金额" prop="contractAmount">
<el-form-item label="合同金额">
<el-input
:model-value="contractAmountDisplay"
placeholder="请输入"
clearable
@input="handleContractAmountInput"
@blur="formatContractAmountField"
@clear="form.contractAmount = null"
/>
</el-form-item>
@@ -164,7 +154,7 @@
</el-input>
</el-form-item>
<el-form-item label="是否需要加盖法人章">
<el-select v-model="form.legalSealFlag" placeholder="请选择" clearable>
<el-select v-model="form.legalSealFlag" placeholder="请选择">
<el-option label="否" :value="0" />
<el-option label="是" :value="1" />
</el-select>
@@ -183,30 +173,20 @@
<el-input v-model="form.handlerUserName" disabled />
</el-form-item>
</div>
<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-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"
@@ -271,20 +251,13 @@
<el-button type="primary" plain @click="addPaymentRatioRow">添加</el-button>
</section>
<billing-plan-editor
v-model="planDialogVisible"
:value="planEditor"
:index="planEditorIndex"
:settlement-currency="form.settlementCurrency"
@save="savePlan"
/>
<billing-plan-editor v-model="planDialogVisible" :value="planEditor" :index="planEditorIndex" @save="savePlan" />
<contract-attachment-section
title="附件(佐证材料等)"
title="其它附件"
description
attachment-type
attachment-location="附件(佐证材料等)"
attachment-type-dict-code="contract_supporting_attachment_type"
attachment-location="其它附件"
:attachment-location-options="changeAttachmentLocationOptions"
:rows="attachments"
:preview="previewAttachment"
@@ -335,8 +308,6 @@ 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';
@@ -387,19 +358,6 @@ 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 = []) =>
@@ -466,21 +424,6 @@ 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' }],
},
};
@@ -490,7 +433,7 @@ export default {
return this.$route.query.name || '合同变更';
},
changeAttachmentLocationOptions() {
return ['用印材料', '附件(佐证材料等)', '变更材料'];
return ['合同文件', '其它附件', '变更材料'];
},
contractAmountDisplay() {
const value = this.form.contractAmount;
@@ -555,9 +498,6 @@ 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' }),
@@ -600,7 +540,7 @@ export default {
copyCount: normalizeOptionalPositiveInteger(data.copyCount),
invoiceCycle: normalizeOptionalPositiveInteger(data.invoiceCycle),
paymentDays: normalizeOptionalPositiveInteger(data.paymentDays),
contractAmount: formatContractAmountInput(data.contractAmount),
contractAmount: normalizeOptionalAmount(data.contractAmount),
templateFlag: data.templateFlag ?? 0,
electronicSealFlag: data.electronicSealFlag ?? 0,
archiveStatus: data.archiveStatus || '未归档',
@@ -661,7 +601,7 @@ export default {
return [];
}
},
// 审核通过后上一次的变更材料归集到「附件(佐证材料等)」,按文件地址/文件名去重,避免重复展示
// 审核通过后上一次的变更材料归集到「其它附件」,按文件地址/文件名去重,避免重复展示
mergeChangeAttachments(attachments = [], changeAttachments = []) {
const key = item => this.attachmentUrl(item) || this.attachmentName(item);
const exists = new Set(attachments.map(key));
@@ -715,11 +655,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 (isContractSealMaterialLocation(location)) {
if (location === '合同文件') {
this.contractFileRows = [...(this.contractFileRows || []), ...rows];
return;
}
if (isContractSupportingAttachmentLocation(location)) {
if (location === '其它附件') {
this.attachments = [...(this.attachments || []), ...rows];
return;
}
@@ -752,18 +692,11 @@ 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: 24px !important;
&:last-child {
margin-bottom: 0 !important;
}
}
.contract-basic-section__remark { width: 100%; 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: 24px; }
.contract-basic-section :deep(.el-form-item) { align-items: center; margin-bottom: 16px; }
.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),
+47 -194
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,9 +140,6 @@
<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>
@@ -174,17 +171,14 @@
<el-descriptions-item label="是否电子章">
{{ detailElectronicSealText }}
</el-descriptions-item>
<el-descriptions-item label="合同主要内容" :span="3">{{
detailValue('mainContent')
}}</el-descriptions-item>
<el-descriptions-item label="其他事项说明" :span="3">{{
<el-descriptions-item label="备注" :span="3">{{
detailValue('remark')
}}</el-descriptions-item>
</el-descriptions>
</section>
<contract-attachment-section
title="用印材料"
title="合同文件"
description
attachment-type
readonly
@@ -300,7 +294,7 @@
</section>
<contract-attachment-section
title="附件(佐证材料等)"
title="其它附件"
description
readonly
:rows="detailAttachmentRows"
@@ -383,14 +377,8 @@
<el-form-item label="合同编号">
<el-input
v-model="form.contractNo"
disabled
:placeholder="
formMode === 'add'
? form.organizationId
? '系统按部门规则自动生成'
: '请先选择合同负责部门'
: ''
"
:disabled="formMode === 'edit'"
placeholder="不填写,则系统自动生成"
/>
</el-form-item>
<el-form-item label="合同名称" prop="contractName">
@@ -429,7 +417,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"
@@ -483,16 +471,6 @@
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
@@ -519,13 +497,12 @@
@input="value => integerInput('paymentDays', value)"
><template #suffix>天</template></el-input>
</el-form-item>
<el-form-item label="合同金额" prop="contractAmount">
<el-form-item label="合同金额">
<el-input
:model-value="contractAmountDisplay"
placeholder="请输入"
clearable
@input="handleContractAmountInput"
@blur="formatContractAmountField"
@clear="form.contractAmount = null"
/>
</el-form-item>
@@ -580,7 +557,7 @@
>
</el-form-item>
<el-form-item label="是否需要加盖法人章" prop="legalSealFlag">
<el-select v-model="form.legalSealFlag" placeholder="请选择" clearable>
<el-select v-model="form.legalSealFlag" placeholder="请选择">
<el-option label="否" :value="0" />
<el-option label="是" :value="1" />
</el-select>
@@ -599,30 +576,20 @@
<el-input v-model="form.handlerUserName" disabled />
</el-form-item>
</div>
<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-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"
@@ -842,11 +809,10 @@
</section>
<contract-attachment-section
title="附件(佐证材料等)"
title="其它附件"
description
attachment-type
attachment-location="附件(佐证材料等)"
attachment-type-dict-code="contract_supporting_attachment_type"
attachment-location="其它附件"
:attachment-location-options="contractAttachmentLocationOptions"
:readonly="isAttachmentUploadMode"
:rows="attachmentRows"
@@ -954,19 +920,18 @@
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"
@@ -1023,7 +988,6 @@
v-model="detailBillingPlanBox"
:value="detailBillingPlanForm"
:index="detailBillingPlanIndex"
:settlement-currency="detailRow.settlementCurrency || form.settlementCurrency"
readonly
/>
@@ -1055,15 +1019,12 @@ 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 { getMKApprovalUrl } from '@/api/system/business-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import { 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';
@@ -1099,19 +1060,6 @@ 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;
@@ -1144,9 +1092,8 @@ const defaultForm = () => ({
partyB: '',
handlerUserName: '',
signDate: '',
applyDate: '',
contractFormat: '',
legalSealFlag: null,
legalSealFlag: 0,
copyCount: '',
settlementCurrency: '',
invoiceCycle: '',
@@ -1162,7 +1109,6 @@ const defaultForm = () => ({
effectiveType: '',
feeGenerationMode: 'system',
archiveStatus: '未归档',
mainContent: '',
remark: '',
});
// 复制新增:仅保留可编辑字段与明细,剔除主键、编号、审批与流程信息
@@ -1174,7 +1120,6 @@ const createCopiedDetail = (detail = {}) => {
return {
...form,
contractNo: '',
applyDate: '',
archiveStatus: '未归档',
feeGenerationMode: detail.feeGenerationMode || '',
billingEnabled: detail.billingEnabled,
@@ -1269,12 +1214,13 @@ 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) => {
@@ -1289,22 +1235,6 @@ 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: [],
@@ -1371,7 +1301,6 @@ export default {
flowBox: false,
flowUrl: '',
processInstanceId: '',
mkEnabled: false,
// 页面形态在实例创建时锁定,避免 keep-alive 缓存后随全局 $route 翻转
formPageLocked: false,
detailPageLocked: false,
@@ -1414,8 +1343,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;
@@ -1482,7 +1411,7 @@ export default {
expandTrigger: 'click',
};
},
// 合同负责部门级联:selectedOrganizationId 存部门 id,级联控件用根到节点的 id 路径,
// 所属组织级联:selectedOrganizationId 存部门 id,级联控件用根到节点的 id 路径,
// 两者在此双向转换;依赖 organizationOptions,树加载完成后回显自动刷新
organizationCascaderValue: {
get() {
@@ -1591,13 +1520,6 @@ 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();
@@ -1808,7 +1730,7 @@ export default {
copyCount: normalizeOptionalInteger(detail.copyCount, ''),
invoiceCycle: normalizeOptionalInteger(detail.invoiceCycle, ''),
paymentDays: normalizeOptionalInteger(detail.paymentDays, ''),
contractAmount: formatContractAmountInput(detail.contractAmount, null),
contractAmount: normalizeOptionalAmount(detail.contractAmount, null),
archiveStatus: detail.archiveStatus || '未归档',
feeGenerationMode:
detail.feeGenerationMode || (Number(detail.billingEnabled) === 0 ? 'manual' : 'system'),
@@ -1844,7 +1766,6 @@ 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() {
@@ -1859,10 +1780,9 @@ export default {
['contractName', '合同名称'],
['contractCategory', '合同类型'],
['projectName', '所属项目'],
['organizationName', '合同负责部门'],
['organizationName', '所属组织'],
['partyA', '甲方'],
['partyB', '乙方'],
['contractAmount', '合同金额'],
];
const empty = required.find(([prop]) => {
const value = this.form[prop];
@@ -1870,16 +1790,10 @@ export default {
});
if (empty) {
this.$message.warning(
['contractName', 'contractAmount'].includes(empty[0])
? `请输入${empty[1]}`
: `请选择${empty[1]}`
empty[0] === 'contractName' ? `请输入${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()) {
@@ -1902,7 +1816,7 @@ export default {
if (action !== 'formal' && action !== 'edit') return true;
const required = [
['projectName', '所属项目'],
['organizationName', '合同负责部门'],
['organizationName', '所属组织'],
['partyA', '甲方'],
['partyB', '乙方'],
['startDate', '合同开始日期'],
@@ -1910,20 +1824,10 @@ export default {
['signDate', '签订日期'],
['settlementMode', '结算方式'],
['contractFormat', '合同格式'],
['contractAmount', '合同金额'],
];
const empty = required.find(([prop]) => {
const value = this.form[prop];
return value === undefined || value === null || String(value).trim() === '';
});
const empty = required.find(([prop]) => !this.form[prop]);
if (empty) {
this.$message.warning(
empty[0] === 'contractAmount' ? `请输入${empty[1]}` : `请选择或输入${empty[1]}`
);
return false;
}
if (Number(this.form.contractAmount) < 0) {
this.$message.warning('合同金额不能小于0');
this.$message.warning(`请选择或输入${empty[1]}`);
return false;
}
return (
@@ -2113,11 +2017,11 @@ export default {
},
handleListAttachmentUploadToLocation({ location, rows = [] }) {
if (!rows.length) return;
if (isContractSealMaterialLocation(location)) {
if (location === '合同文件') {
this.listUploadContractFileRows = [...(this.listUploadContractFileRows || []), ...rows];
return;
}
if (isContractSupportingAttachmentLocation(location)) {
if (location === '其它附件') {
this.listUploadAttachmentRows = [...(this.listUploadAttachmentRows || []), ...rows];
}
},
@@ -2237,9 +2141,6 @@ 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;
@@ -2302,29 +2203,6 @@ 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;
@@ -2338,7 +2216,7 @@ export default {
this.organizationFlatOptions.unshift(item);
},
renderOrganizationSearch(scope) {
// 搜索区合同负责部门:单选级联(与新增/编辑表单一致)
// 搜索区所属组织:单选级联(与新增/编辑表单一致)
// 级联返回 id 路径数组,提交时由 normalizeSearch 取末级 id 转组织名称
return h(ElCascader, {
modelValue: Array.isArray(scope.row?.organizationName) ? scope.row.organizationName : [],
@@ -2357,7 +2235,7 @@ export default {
filterable: true,
clearable: true,
style: 'width: 100%',
placeholder: '请选择 合同负责部门',
placeholder: '请选择 所属组织',
});
},
integerInput(prop, value) {
@@ -2370,9 +2248,6 @@ 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;
@@ -2405,11 +2280,11 @@ export default {
},
handleAttachmentUploadToLocation({ location, rows = [] }) {
if (!rows.length) return;
if (isContractSealMaterialLocation(location)) {
if (location === '合同文件') {
this.contractFileRows = [...(this.contractFileRows || []), ...rows];
return;
}
if (isContractSupportingAttachmentLocation(location)) {
if (location === '其它附件') {
this.attachmentRows = [...(this.attachmentRows || []), ...rows];
}
},
@@ -2770,16 +2645,15 @@ export default {
templateFlag: '是否范本',
originalContractNo: '原件合同编号',
electronicSealFlag: '是否电子章',
mainContent: '合同主要内容',
remark: '其他事项说明',
remark: '备注',
feeGenerationMode: '费用生成模式',
billingPlanJson: '计费方案',
settlementRuleJson: '结算单规则',
preSettlementConfigJson: '预结算配置',
formalSettlementConfigJson: '正式结算配置',
paymentRatioJson: '付款比例设置',
contractFileJson: '用印材料',
attachmentsJson: '附件(佐证材料等)',
contractFileJson: '合同文件',
attachmentsJson: '其它附件',
changeAttachmentsJson: '变更材料',
};
return labels[field] || field;
@@ -2946,8 +2820,13 @@ export default {
detailValue(prop) {
const value = this.detailRow[prop];
if (prop === 'contractAmount') {
const amountText = formatContractAmountInput(value, '');
return amountText === '' ? '-' : amountText;
return value === undefined ||
value === null ||
value === '' ||
Number(value) === -1 ||
String(value) === '-1'
? '-'
: value;
}
if (
prop === 'contractStage' ||
@@ -2979,10 +2858,6 @@ export default {
return;
}
if (operation.action === 'flow') {
if (this.mkEnabled) {
this.handleMkAudit(row);
return;
}
this.openFlow(row);
return;
}
@@ -3036,24 +2911,6 @@ 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) {
@@ -3260,11 +3117,7 @@ export default {
&__remark {
width: 100%;
margin-bottom: 24px !important;
&:last-child {
margin-bottom: 0 !important;
}
margin-bottom: 0 !important;
}
&__date-range {
@@ -3338,7 +3191,7 @@ export default {
:deep(.el-form-item) {
align-items: center;
margin-bottom: 24px;
margin-bottom: 14px;
}
:deep(.el-form-item__label) {
+88 -165
View File
@@ -1,6 +1,6 @@
<template>
<basic-container class="loading-manage-page">
<div v-if="!isStandalonePage && searchVisible" class="loading-manage-page__search">
<div v-if="!isStandalonePage" 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,13 +228,17 @@
<el-button type="primary" plain @click="handleExport">批量导出</el-button>
<el-button type="primary" plain @click="handleBatchComplete">批量完成</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="loadTable"
/>
<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>
</div>
<el-table
@@ -250,12 +254,7 @@
>
<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
v-if="isColumnVisible('loadingNo')"
label="配载单号"
prop="loadingNo"
min-width="170"
>
<el-table-column label="配载单号" prop="loadingNo" min-width="170">
<template #default="{ row }">
<el-link
type="primary"
@@ -265,12 +264,7 @@
>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('loadingSubNos')"
label="运单号"
prop="loadingSubNos"
min-width="220"
>
<el-table-column label="运单号" prop="loadingSubNos" min-width="220">
<template #default="{ row }">
<span class="loading-manage-page__sub-nos">
<template v-if="splitText(row.loadingSubNos).length">
@@ -289,40 +283,11 @@
</span>
</template>
</el-table-column>
<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"
>
<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">
<template #default="{ row }">
<el-tooltip
v-if="formatFullRouteAddress(row, 'departure')"
@@ -339,12 +304,7 @@
</div>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('transitAddress')"
label="途经地"
prop="transitAddress"
min-width="190"
>
<el-table-column label="途经地" prop="transitAddress" min-width="190">
<template #default="{ row }">
<el-tooltip
v-if="formatFullRouteAddress(row, 'transit')"
@@ -361,12 +321,7 @@
</div>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('arrivalAddress')"
label="到货地"
prop="arrivalAddress"
min-width="180"
>
<el-table-column label="到货地" prop="arrivalAddress" min-width="180">
<template #default="{ row }">
<el-tooltip
v-if="formatFullRouteAddress(row, 'arrival')"
@@ -383,40 +338,20 @@
</div>
</template>
</el-table-column>
<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
>
<el-table-column label="承运商" prop="carrierName" min-width="160" show-overflow-tooltip />
<el-table-column 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"
@@ -424,7 +359,6 @@
sortable
/>
<el-table-column
v-if="isColumnVisible('updateTime')"
label="更新时间"
prop="updateTime"
min-width="170"
@@ -432,20 +366,12 @@
sortable
/>
<el-table-column
v-if="isColumnVisible('currentProcessNode')"
label="过程节点"
prop="currentProcessNode"
min-width="130"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('businessStatus')"
label="状态"
prop="businessStatus"
width="110"
fixed="right"
align="center"
>
<el-table-column label="状态" prop="businessStatus" width="110" fixed="right" align="center">
<template #default="{ row }">
<span :class="['loading-manage-page__status', `is-${row.businessStatus || 'unknown'}`]">
{{ statusText(row) }}
@@ -558,16 +484,12 @@
<div class="loading-detail__field loading-detail__attachment-field">
<span class="loading-detail__label">附件</span>
<span class="loading-detail__value"
><span
><el-link
v-for="file in detailAttachments"
:key="file.url || file.name"
class="attachment-file-name"
role="button"
tabindex="0"
type="primary"
@click="previewAttachment(file)"
@keydown.enter="previewAttachment(file)"
@keydown.space.prevent="previewAttachment(file)"
>{{ file.name }}</span
>{{ file.name }}</el-link
><span v-if="!detailAttachments.length">-</span></span
>
</div>
@@ -633,12 +555,15 @@
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
@@ -740,6 +665,13 @@
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"
@@ -769,12 +701,22 @@
>
备注:{{ record.remark }}
</div>
<punch-record-photos
<div
v-if="record.photos && record.photos.length"
:photos="record.photos"
lazy
@preview="previewDriverUploadPhoto(record.photos, $event)"
/>
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>
</template>
<div v-else class="loading-detail__punch-record-line">暂未打卡</div>
</div>
@@ -1124,7 +1066,6 @@
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)">
@@ -1416,7 +1357,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" />
@@ -1459,11 +1400,7 @@
<el-option
v-for="item in taskCarrierOptions"
:key="item.id || item.carrierName"
:label="
dialogForm.carrierType === '承运商'
? formatTaskCarrierContractLabel(item)
: item.carrierName || item.partyB
"
:label="item.carrierName || item.partyB"
:value="
dialogForm.carrierType === '承运商'
? item.id
@@ -1779,14 +1716,12 @@ 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,
@@ -1795,8 +1730,7 @@ import {
} from '@/option/business/loading-manage';
import { option as waybillOption } from '@/option/business/waybill-manage';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Location, Rank, Search } from '@element-plus/icons-vue';
import TableHeaderTools from '@/components/table-header-tools/main.vue';
import { Location, Rank, Refresh, Search, Setting } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
import { isMobile } from '@/utils/validate';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
@@ -1993,44 +1927,21 @@ 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,
@@ -2278,14 +2189,6 @@ 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;
@@ -2345,8 +2248,13 @@ export default {
selectionChange(rows) {
this.selectionRows = rows || [];
},
isColumnVisible(prop) {
return this.visibleColumnKeys.includes(prop);
toggleTableSize() {
const order = ['default', 'small', 'large'];
const index = order.indexOf(this.tableSize);
this.tableSize = order[(index + 1) % order.length];
},
handleColumnSetting() {
ElMessage.info('列设置按原型保留入口,当前默认展示全部列表字段');
},
statusText(row) {
return row.businessStatusName || loadingStatusMap[row.businessStatus] || '未知';
@@ -2849,6 +2757,7 @@ export default {
if (status === 'draft') {
return [
{ type: 'edit', label: '编辑' },
{ type: 'copy', label: '复制' },
{ type: 'delete', label: '删除' },
];
}
@@ -2860,24 +2769,36 @@ export default {
if (row.driverRejected) {
actions.push({ type: 'reassign', label: '重新派单' });
}
actions.push({ type: 'edit', label: '编辑' }, { type: 'cancel', label: '取消' });
actions.push(
{ type: 'edit', label: '编辑' },
{ type: 'copy', 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: '详情' }];
return [
{ type: 'view', label: '详情' },
{ type: 'copy', label: '复制' },
];
}
return [{ type: 'view', label: '详情' }];
return [
{ type: 'view', label: '详情' },
{ type: 'copy', 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),
@@ -4552,7 +4473,8 @@ export default {
margin: 12px 0;
}
.loading-manage-page__toolbar-left {
.loading-manage-page__toolbar-left,
.loading-manage-page__toolbar-right {
display: flex;
align-items: center;
gap: 12px;
@@ -4811,7 +4733,7 @@ export default {
}
:deep(.el-form-item) {
margin-bottom: 24px;
margin-bottom: 14px;
}
:deep(.el-form-item__label) {
@@ -5006,6 +4928,7 @@ export default {
margin-left: 12px;
}
}
.loading-detail__punch-record-photos,
.loading-detail__driver-upload-grid,
.loading-detail__voucher-image-grid {
display: grid;
+4 -24
View File
@@ -1,7 +1,7 @@
<template>
<basic-container class="master-order-page">
<template v-if="mode === 'list'">
<div v-show="searchVisible" class="master-order-page__search">
<div 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,16 +55,8 @@
</div>
<section class="master-list-panel">
<div class="toolbar">
<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"
/>
<el-button type="primary" @click="goCreate()">新建多联总单</el-button
><el-button type="primary" plain @click="download">批量导出</el-button>
</div>
<div v-loading="loading" class="master-cards">
<article v-for="row in records" :key="row.id" class="master-card">
@@ -155,13 +147,12 @@
<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: { TableHeaderTools, MasterOrderEditor, MasterOrderDispatch, MasterOrderDetail },
components: { MasterOrderEditor, MasterOrderDispatch, MasterOrderDetail },
data() {
return {
// 实例创建时锁定所属路由。keep-alive 缓存后 $route 会变成其它页面,
@@ -169,7 +160,6 @@ export default {
//(mode=add 且无 id)会落入详情分支并请求缺少 id 的详情接口。
routePathLocked: this.$route.path,
searchExpanded: false,
searchVisible: true,
loading: false,
records: [],
query: { businessStatus: '' },
@@ -643,18 +633,8 @@ 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;
+21 -57
View File
@@ -429,7 +429,7 @@ const NODE_TEMPLATES = [
type: 'confirm',
supportVoucher: false,
},
{ key: 'arrive_scene', name: '到场', type: 'punch', enabled: false, supportCargo: false },
{ key: 'arrive_scene', name: '到场', type: 'punch', enabled: false },
{
key: 'load',
name: '装货',
@@ -485,7 +485,6 @@ const NODE_TEMPLATES = [
key: 'return',
name: '回单',
type: 'confirm',
supportCargo: false,
supportVoucher: true,
voucherOptions: ['磅单', '签收单'],
},
@@ -566,13 +565,7 @@ export default {
});
},
defaultDeptId() {
const raw = (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
return (
String(raw)
.split(',')
.map(item => item.trim())
.filter(Boolean)[0] || ''
);
return (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
},
applyDefaultDeptSearch() {
const deptId = this.defaultDeptId();
@@ -587,32 +580,15 @@ export default {
});
},
flattenDept(tree, level = 0) {
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;
return tree.flatMap(item => [
{
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
value: item.id,
},
...(item.children && item.children.length
? this.flattenDept(item.children, level + 1)
: []),
]);
},
displayStatus(status) {
const statusMap = {
@@ -655,9 +631,7 @@ export default {
enabled: false,
punch: true,
location: true,
// 不支持货量的节点(到场/回单/接单等)不得默认勾上,否则会写出
// 「上传货量为是、货量类型为空」的配置,而这类节点在界面上没有货量开关可关
uploadCargo: !!template?.supportCargo,
uploadCargo: true,
cargoTypes: [],
uploadVoucher: undefined,
voucherTypes: [],
@@ -672,10 +646,6 @@ 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') {
@@ -948,15 +918,6 @@ 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() {
@@ -1113,9 +1074,9 @@ export default {
this.onLoad(this.page, this.query);
},
searchChange(params, done) {
this.query = this.normalizeListQuery(params);
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, this.query);
this.onLoad(this.page, params);
done();
},
selectionChange(list) {
@@ -1138,10 +1099,12 @@ export default {
},
onLoad(page, params = {}) {
this.loading = true;
const query = this.normalizeListQuery(params);
this.query = query;
api
.getList(page.currentPage, page.pageSize, query)
.getList(page.currentPage, page.pageSize, {
...params,
...this.query,
allDept: this.isAdmin ? 1 : 0,
})
.then(res => {
const result = res.data.data || {};
this.page.total = result.total || 0;
@@ -1154,7 +1117,8 @@ export default {
},
buildExportParams() {
return {
...this.normalizeListQuery(this.query),
...this.query,
allDept: this.isAdmin ? 1 : 0,
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
+13 -45
View File
@@ -573,7 +573,7 @@
type="textarea"
:rows="2"
placeholder="请输入"
maxlength="1000"
maxlength="200"
show-word-limit
/>
</el-form-item>
@@ -583,7 +583,7 @@
type="textarea"
:rows="2"
placeholder="请输入"
maxlength="1000"
maxlength="200"
show-word-limit
/>
</el-form-item>
@@ -593,7 +593,7 @@
type="textarea"
:rows="2"
placeholder="请输入"
maxlength="1000"
maxlength="200"
show-word-limit
/>
</el-form-item>
@@ -603,7 +603,7 @@
type="textarea"
:rows="2"
placeholder="请输入"
maxlength="1000"
maxlength="200"
show-word-limit
/>
</el-form-item>
@@ -645,22 +645,11 @@
</el-select>
</template>
</el-table-column>
<el-table-column
label="文件名"
min-width="220"
align="center"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<el-table-column label="文件名" min-width="220" align="center" show-overflow-tooltip>
<template #default="{ row }">
<span
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(row)"
@keydown.enter="previewAttachment(row)"
@keydown.space.prevent="previewAttachment(row)"
>{{ attachmentName(row) }}</span>
<el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }}
</el-link>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="240" align="center">
@@ -857,15 +846,15 @@
v-model="userQuery.realName"
placeholder="用户姓名"
clearable
@keyup.enter="searchUsers"
@keyup.enter="loadUsers"
/>
<el-input
v-model="userQuery.phone"
placeholder="手机号"
clearable
@keyup.enter="searchUsers"
@keyup.enter="loadUsers"
/>
<el-button type="primary" @click="searchUsers">查询</el-button>
<el-button type="primary" @click="loadUsers">查询</el-button>
<el-button @click="resetUserQuery">重置</el-button>
</div>
<el-table :data="userData" border v-loading="userLoading" @row-dblclick="selectUser">
@@ -1199,18 +1188,6 @@ 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: [],
@@ -2779,20 +2756,11 @@ 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;
const params = {
realName: String(this.userQuery.realName || '').trim(),
phone: String(this.userQuery.phone || '').trim(),
};
getUserList(this.userPage.currentPage, this.userPage.pageSize, params)
getUserList(this.userPage.currentPage, this.userPage.pageSize, this.userQuery)
.then(res => {
const result = res.data.data || {};
this.userData = result.records || [];
@@ -2842,7 +2810,7 @@ export default {
}
:deep(.el-form-item) {
//margin-bottom: 24px;
margin-bottom: 14px;
}
:deep(.el-form-item__label) {
+10 -50
View File
@@ -99,21 +99,11 @@
<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
class-name="is-ellipsis-link"
>
<el-table-column label="文件名称" min-width="240" show-overflow-tooltip>
<template #default="{ row }">
<span
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(row)"
@keydown.enter="previewAttachment(row)"
@keydown.space.prevent="previewAttachment(row)"
>{{ attachmentName(row) }}</span>
<el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }}
</el-link>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220">
@@ -194,7 +184,6 @@
title="查看临时额度申请"
append-to-body
destroy-on-close
align-center
:width="formDialogWidth"
class="temporary-credit-limit-dialog temporary-credit-limit-detail-dialog"
>
@@ -227,21 +216,11 @@
</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
class-name="is-ellipsis-link"
>
<el-table-column label="文件名称" min-width="240" show-overflow-tooltip>
<template #default="{ row }">
<span
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(row)"
@keydown.enter="previewAttachment(row)"
@keydown.space.prevent="previewAttachment(row)"
>{{ attachmentName(row) }}</span>
<el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }}
</el-link>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220" show-overflow-tooltip>
@@ -636,12 +615,6 @@ 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) {
@@ -1215,10 +1188,10 @@ export default {
}
// 新增/编辑/查看:宽度与高度规则统一(Avue setPx 对 '1100px' 会拼成 '1100pxpx',故 option 用数字 1100)
// margin:auto 配合下方 unscoped 的 overlay flex,实现垂直水平居中
.temporary-credit-limit-dialog.el-dialog {
width: 1100px !important;
margin: auto !important;
margin-top: 20px !important;
margin-bottom: 20px !important;
height: auto !important;
max-height: calc(100vh - 40px) !important;
display: flex;
@@ -1356,16 +1329,3 @@ 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>
+22 -51
View File
@@ -585,10 +585,6 @@ export default {
this.$message.warning(`第${index + 1}条调度明细缺少车牌号`);
return;
}
if (!this.resolveDispatchQuantityUnit(row)) {
this.$message.warning(`第${index + 1}条调度明细缺少数量单位`);
return;
}
}
this.$confirm(
@@ -604,57 +600,32 @@ 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 => {
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),
};
}),
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,
},
]),
})),
};
dispatch(payload)
@@ -668,7 +639,7 @@ export default {
})
.catch(err => {
console.error('调度失败:', err);
this.$message.error(err?.response?.data?.msg || err?.message || '调度失败');
this.$message.error('调度失败');
})
.finally(() => {
this.saving = false;
+2 -4
View File
@@ -51,7 +51,7 @@
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="formatContractOptionLabel(item)"
:label="item.contractName"
:value="item.id"
/>
</el-select>
@@ -165,7 +165,6 @@ 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';
@@ -272,7 +271,6 @@ export default {
this.loadProjectOptions();
},
methods: {
formatContractOptionLabel,
loadProjectOptions() {
if (this.projectLoading) return;
this.projectLoading = true;
@@ -653,7 +651,7 @@ export default {
}
:deep(.el-form-item) {
margin-bottom: 24px;
margin-bottom: 16px;
}
}
+4 -18
View File
@@ -96,25 +96,11 @@ 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 part = file.slice((partNumber - 1) * chunkSize, Math.min(partNumber * chunkSize, file.size));
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('"', '');
}
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('"', '');
if (!etag) throw new Error('未获取到分片 ETag');
await api.updateFileTask({ id: task.id, partNumber, etag });
replaceProgress.value = Math.round((partNumber / task.chunkTotal) * 100);
+22 -139
View File
@@ -1,6 +1,6 @@
<template>
<basic-container class="voucher-manage-page">
<section v-show="searchVisible" class="voucher-manage-page__search">
<section 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,25 +55,10 @@
</section>
<section>
<div class="voucher-manage-page__toolbar">
<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"
/>
<el-button type="primary" @click="openUpload">批量导入凭证</el-button
><el-button plain @click="openProgress">上传进度</el-button>
</div>
<el-table
v-loading="loading"
:data="rows"
:size="tableSize"
border
class="voucher-manage-page__table"
>
<el-table v-loading="loading" :data="rows" border class="voucher-manage-page__table">
<el-table-column type="selection" width="52" fixed="left" align="center" /><el-table-column
type="index"
label="序号"
@@ -81,47 +66,27 @@
fixed="left"
align="center"
/>
<el-table-column
v-if="isColumnVisible('voucherBatchNo')"
prop="voucherBatchNo"
label="凭证批次号"
min-width="150"
fixed="left"
<el-table-column 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
v-if="isColumnVisible('waybillBatchNo')"
prop="waybillBatchNo"
label="运单批次号"
min-width="170"
/><el-table-column
v-if="isColumnVisible('fileName')"
<el-table-column prop="waybillBatchNo" label="运单批次号" min-width="170" /><el-table-column
prop="fileName"
label="文件名"
min-width="200"
show-overflow-tooltip
/>
<el-table-column
v-if="!isCarrier && isColumnVisible('uploadSource')"
prop="uploadSource"
label="上传来源"
width="100"
/><el-table-column
v-if="!isCarrier && isColumnVisible('carrierName')"
<el-table-column v-if="!isCarrier" prop="uploadSource" label="上传来源" width="100" /><el-table-column
v-if="!isCarrier"
prop="carrierName"
label="承运商名称"
min-width="160"
><template #default="{ row }">{{ row.carrierName || '-' }}</template></el-table-column
>
<el-table-column
v-if="isColumnVisible('processStatus')"
prop="processStatus"
label="处理状态"
width="120"
<el-table-column prop="processStatus" label="处理状态" width="120"
><template #default="{ row }"
><span
:class="{ 'voucher-manage-page__process-failed': row.processStatus === '处理失败' }"
@@ -129,35 +94,17 @@
></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
v-if="isColumnVisible('unRelatedWaybillCount')"
prop="unRelatedWaybillCount"
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('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"
@@ -565,7 +512,6 @@ 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();
@@ -593,47 +539,6 @@ 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();
});
@@ -837,30 +742,6 @@ 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;
@@ -875,13 +756,21 @@ 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 etag = await uploadPartWithFallback(task.id, partNumber, part, abortController.signal);
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('"', '');
if (!etag) throw new Error('MinIO 未返回分片 ETag,请确认跨域配置已暴露 ETag 响应头');
await api.updateFileTask({ id: task.id, partNumber, etag });
ensureUploadSession(sessionId, task.id);
@@ -1440,15 +1329,9 @@ 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%;
}
@@ -1487,7 +1370,7 @@ load();
display: none;
}
&__upload-form :deep(.el-form-item) {
margin-bottom: 24px;
margin-bottom: 14px;
}
&__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: 24px;
margin-bottom: 14px;
}
.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: 24px;
margin-bottom: 14px;
}
.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 &&
['reviewing', 'approved'].includes(row.approvalStatus) &&
row.approvalStatus === 'reviewing' &&
hasPermission('bill_payment_approve')
"
type="primary"
@@ -499,6 +499,7 @@ const emptyLine = () => ({
localId: ++localId,
goodsCategory: '',
goodsName: '',
taxClassificationCode: '',
unit: '吨',
quantity: 0,
unitPriceNoTax: 0,
@@ -742,9 +743,11 @@ 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);
},
@@ -752,6 +755,7 @@ 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);
@@ -761,9 +765,11 @@ 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) {
@@ -1159,6 +1165,7 @@ 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,
+30 -6
View File
@@ -61,6 +61,7 @@
v-if="hasPermission('invoice_application_sync')"
type="primary"
plain
:loading="batchSyncing"
@click="handleBatchSync"
>批量同步</el-button
>
@@ -154,7 +155,7 @@
v-if="
mkEnabled &&
hasPermission('invoice_application_approve') &&
['reviewing', 'approved'].includes(row.approvalStatus)
row.approvalStatus === 'reviewing'
"
type="primary"
@click="handleMkAudit(row)"
@@ -244,6 +245,7 @@ export default {
rows: [],
selection: [],
loading: false,
batchSyncing: false,
page: { current: 1, size: 10, total: 0 },
mkEnabled: false,
};
@@ -375,8 +377,13 @@ export default {
},
async handleApprove(row) {
await this.$confirm('确认审批通过该开票申请?', '提示', { type: 'warning' });
await api.approve({ id: row.id });
this.$message.success('审批通过');
const res = await api.approve({ id: row.id });
// data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
if (res.data.data === false) {
this.$message.warning(res.data.msg);
} else {
this.$message.success(res.data.msg || '审批通过');
}
this.loadTable();
},
async handleReturn(row) {
@@ -392,14 +399,31 @@ 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;
}
await Promise.all(rows.map(row => api.syncKingdee(row.id)));
this.$message.success(`已同步${rows.length}条开票申请`);
this.loadTable();
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;
}
},
async handleExport() {
const data = this.unwrapData(await api.getList(1, 100000, this.query));
+33 -241
View File
@@ -47,7 +47,7 @@
<el-col :span="6">
<el-form-item label="开票金额">
<el-input
:model-value="form.kingdeeInvoicePoolId ? formatMoney(form.invoiceAmount) : ''"
:model-value="form.invoiceNo ? formatMoney(form.invoiceAmount) : ''"
disabled
placeholder="从金蝶票据池带出"
/>
@@ -56,7 +56,7 @@
<el-col :span="6">
<el-form-item label="税额">
<el-input
:model-value="form.kingdeeInvoicePoolId ? formatMoney(form.taxAmount) : ''"
:model-value="form.invoiceNo ? formatMoney(form.taxAmount) : ''"
disabled
placeholder="从金蝶票据池带出"
/>
@@ -118,7 +118,13 @@
</section-card>
<section-card title="附件信息">
<vehicle-attachment-table v-model="form.attachments" :readonly="readonly" />
<!-- 金蝶票池认领的发票附件为 piaozone path 型,需带发票号走 A801-2 代理查看/下载 -->
<vehicle-attachment-table
v-model="form.attachments"
:readonly="readonly"
show-view-actions
:kingdee-invoice-no="form.invoiceNo"
/>
</section-card>
<section-card title="备注">
@@ -149,62 +155,13 @@
</div>
</el-form>
<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>
<kingdee-invoice-pool-dialog
v-model="invoiceDialog.visible"
title="查询金蝶票据池"
:initial-invoice-no="form.invoiceNo"
:query-api="apiQueryInvoicePool"
@confirm="handleInvoicePicked"
/>
<el-dialog
v-model="settlementDialog.visible"
@@ -259,82 +216,13 @@
import { Search } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/invoiceReceipt';
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
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: '',
@@ -346,8 +234,6 @@ const emptyForm = () => ({
bankName: '',
bankAccount: '',
issuingBank: '',
kingdeeBillNo: '',
kingdeeStatus: 'unsynced',
phone: '',
customerEmails: [],
departmentEmails: [],
@@ -363,24 +249,17 @@ 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,
@@ -426,6 +305,12 @@ export default {
this.initialize();
},
methods: {
apiQueryInvoicePool(params) {
return api.queryInvoicePool(params);
},
handleInvoicePicked(rows) {
this.confirmInvoice(rows[0]);
},
hasPermission(code) {
return this.permission?.[code] !== false;
},
@@ -483,7 +368,7 @@ export default {
};
},
validateInvoice(rule, value, callback) {
if (!value || !this.form.kingdeeInvoicePoolId) {
if (!value) {
callback(new Error('请选择金蝶票据池发票'));
return;
}
@@ -509,99 +394,17 @@ export default {
}
callback();
},
async openInvoiceDialog() {
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?.id ? row : this.invoiceDialog.current;
const selected = row;
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,
@@ -613,8 +416,6 @@ 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
@@ -725,7 +526,6 @@ export default {
payload() {
return {
id: this.form.id,
kingdeeInvoicePoolId: this.form.kingdeeInvoicePoolId,
invoiceNo: this.form.invoiceNo,
invoiceDate: this.form.invoiceDate,
invoiceType: this.form.invoiceType,
@@ -737,8 +537,6 @@ 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(';'),
@@ -782,13 +580,12 @@ export default {
this.$message.warning('请先选择金蝶票据池发票');
return;
}
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);
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);
if (!invoice) {
this.$message.warning('金蝶票据池中未查询到该发票');
this.$message.warning('金蝶票池未查询到该发票');
return;
}
if (!this.confirmInvoice(invoice, { preserveUserInput: true })) return;
@@ -813,7 +610,7 @@ export default {
padding-bottom: 72px;
}
.receipt-form-page__form :deep(.el-form-item) {
margin-bottom: 24px;
margin-bottom: 14px;
}
.receipt-form-page__form :deep(.el-select),
.receipt-form-page__form :deep(.el-input-number) {
@@ -851,11 +648,6 @@ 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;
}
+37 -15
View File
@@ -40,10 +40,10 @@
/>
</el-select>
</el-form-item>
<el-form-item v-if="searchExpanded" label="金蝶单据状态">
<el-select v-model="query.kingdeeStatus" clearable placeholder="请选择">
<el-form-item v-if="searchExpanded" label="金蝶状态">
<el-select v-model="query.kingdeePushStatus" clearable placeholder="请选择">
<el-option
v-for="item in kingdeeStatusOptions"
v-for="item in kingdeePushStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
@@ -70,7 +70,8 @@
v-if="hasPermission('invoice_receipt_sync')"
type="primary"
plain
@click="handleBatchSync"
:loading="pushLoading"
@click="handlePushKingdee"
>批量同步</el-button
>
<el-button v-if="hasPermission('invoice_receipt_add')" type="primary" @click="openCreate"
@@ -149,7 +150,7 @@
v-if="
mkEnabled &&
hasPermission('invoice_receipt_approve') &&
['reviewing', 'approved'].includes(row.approvalStatus)
row.approvalStatus === 'reviewing'
"
type="primary"
@click="handleMkAudit(row)"
@@ -218,7 +219,7 @@ const emptyQuery = () => ({
deptName: '',
deptPath: [],
approvalStatus: '',
kingdeeStatus: '',
kingdeePushStatus: '',
});
export default {
@@ -232,11 +233,12 @@ export default {
query: emptyQuery(),
searchExpanded: false,
approvalStatusOptions: api.approvalStatusOptions,
kingdeeStatusOptions: api.kingdeeStatusOptions,
kingdeePushStatusOptions: api.kingdeePushStatusOptions,
columns: invoiceReceiptTableColumns,
rows: [],
selection: [],
loading: false,
pushLoading: false,
page: { current: 1, size: 10, total: 0 },
mkEnabled: false,
};
@@ -340,8 +342,13 @@ export default {
},
async handleApprove(row) {
await this.$confirm('确认审批通过该收票登记?', '提示', { type: 'warning' });
await api.approve({ id: row.id });
this.$message.success('审批通过');
const res = await api.approve({ id: row.id });
// data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
if (res.data.data === false) {
this.$message.warning(res.data.msg);
} else {
this.$message.success(res.data.msg || '审批通过');
}
this.loadTable();
},
async handleReturn(row) {
@@ -356,15 +363,29 @@ export default {
this.$message.success('作废成功');
this.loadTable();
},
async handleBatchSync() {
async handlePushKingdee() {
if (this.pushLoading) return;
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
if (!rows.length) {
this.$message.warning('请选择至少一条审批通过的收票登记');
return;
}
await Promise.all(rows.map(row => api.syncKingdee(row.id)));
this.$message.success(`已同步${rows.length}条收票登记`);
this.loadTable();
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;
}
},
async handleExport() {
const data = this.unwrapData(await api.getList(1, 100000, this.query));
@@ -381,8 +402,9 @@ export default {
当前处理人: item.currentProcessor,
创建人: item.createUserName,
创建时间: item.createTime,
金蝶单据号: item.kingdeeBillNo,
金蝶单据状态: item.kingdeeStatusName,
金蝶单据号: item.kingdeeReceivedInvoiceNo,
金蝶状态: item.kingdeePushStatusName,
失败原因: item.kingdeePushMsg,
}));
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(exportRows), '收票登记');
+91 -24
View File
@@ -100,7 +100,7 @@
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="formatContractOptionLabel(item)"
:label="item.contractName || item.contractNo"
:value="item.id"
/>
</el-select>
@@ -281,8 +281,18 @@
<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" @click="viewInvoiceAttachment(row)">查看</el-link>
<el-link type="primary" @click="downloadInvoiceAttachment(row)">下载</el-link>
<el-link
type="primary"
:disabled="kingdeeAttachmentBusy"
@click="viewInvoiceAttachment(row)"
>查看</el-link
>
<el-link
type="primary"
:disabled="kingdeeAttachmentBusy"
@click="downloadInvoiceAttachment(row)"
>下载</el-link
>
<el-link
v-if="!readonly"
type="danger"
@@ -345,22 +355,19 @@
value="special_approval" /><el-option
label="其他"
value="other" /></el-select></template></el-table-column
><el-table-column
label="文件名"
min-width="220"
show-overflow-tooltip
class-name="is-ellipsis-link"
><el-table-column label="文件名" min-width="220"
><template #default="{ row }"
><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
><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
></el-table-column
><el-table-column label="附件描述" min-width="220"
><template #default="{ row }"
@@ -664,9 +671,13 @@ 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 = [
@@ -747,6 +758,8 @@ export default {
form: emptyForm(),
amountSyncing: false,
transferPayload: null,
// 金蝶侧发票附件查看/下载进行中(行组级 loading 守卫)
kingdeeAttachmentBusy: false,
referenceRows: [],
paymentRecords: [],
referenceVisible: false,
@@ -1025,7 +1038,6 @@ export default {
this.initialize();
},
methods: {
formatContractOptionLabel,
hasPermission(code) {
return this.permission?.[code] !== false;
},
@@ -1589,8 +1601,19 @@ export default {
this.attachmentFileName(this.invoiceAttachment(row)) || `${row.invoiceNo || '发票'}.pdf`
);
},
viewInvoiceAttachment(row) {
async 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;
@@ -1600,8 +1623,20 @@ export default {
[attachment]
);
},
downloadInvoiceAttachment(row) {
const url = this.invoiceAttachmentUrl(row);
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);
if (!url) {
this.$message.warning('当前发票暂无可下载附件');
return;
@@ -2752,6 +2787,38 @@ 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;
}
@@ -2759,7 +2826,7 @@ export default {
display: inline-flex;
}
.payment-form-page :deep(.el-form-item) {
margin-bottom: 24px;
margin-bottom: 14px;
}
.payment-form-page :deep(.el-input-number .el-input__inner) {
text-align: left;
+64 -7
View File
@@ -81,9 +81,18 @@
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"
@@ -163,7 +172,7 @@
v-if="
mkEnabled &&
hasPermission('payment_application_approve') &&
['reviewing', 'approved'].includes(row.approvalStatus)
row.approvalStatus === 'reviewing'
"
type="primary"
@click="handleMkAudit(row)"
@@ -260,6 +269,8 @@ export default {
rows: [],
selection: [],
loading: false,
syncLoading: false,
resultSyncing: false,
page: { current: 1, size: 10, total: 0 },
mkEnabled: false,
};
@@ -376,8 +387,13 @@ export default {
this.loadTable();
},
async handleApprove(row) {
await api.approve({ id: row.id });
this.$message.success('审批通过');
const res = await api.approve({ id: row.id });
// data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
if (res.data.data === false) {
this.$message.warning(res.data.msg);
} else {
this.$message.success(res.data.msg || '审批通过');
}
this.loadTable();
},
handleMkAudit(row) {
@@ -413,17 +429,58 @@ export default {
async handleSyncRow(row) {
const data = this.unwrapData(await api.syncKingdee(row.id));
this.$message.success(`已生成金蝶付款单:${data}`);
this.loadTable();
await this.loadTable();
this.warnAttachmentNotUploaded([row.id]);
},
async handleSync() {
if (this.syncLoading) return;
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
if (!rows.length) {
this.$message.warning('请选择至少一条审批通过的付款申请');
return;
}
await api.syncKingdeeBatch(rows.map(row => row.id));
this.$message.success(`已同步${rows.length}条付款申请`);
this.loadTable();
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;
}
},
handleExport() {
const headers = this.columns.map(item => item.label);
+46 -6
View File
@@ -56,6 +56,16 @@
<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
@@ -80,6 +90,11 @@
</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
@@ -107,7 +122,11 @@
<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'"
v-if="
hasPermission('receipt_claim_record_void') &&
row.claimStatus === 'claimed' &&
row.pushStatus !== 'pushed'
"
type="danger"
@click="handleVoid(row)"
>作废</el-link
@@ -152,6 +171,7 @@ const emptyQuery = () => ({
claimDateRange: [],
claimerDeptName: '',
kingdeeBillStatus: '',
pushStatus: '',
});
export default {
@@ -165,9 +185,11 @@ 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 },
};
},
@@ -182,8 +204,9 @@ export default {
},
methods: {
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
// 数字 0 是合法返回值(如回写/同步 0 条),须用 ?? 判空避免 0 被信封对象顶掉导致 Number() 得 NaN
const body = response?.data ?? response ?? {};
return body?.data ?? body;
},
queryParams() {
const transactionRange = this.query.transactionDateRange || [];
@@ -227,14 +250,24 @@ export default {
},
async handleVoid(row) {
await this.$confirm(
'作废后将回退本次认领金额,并生成审核通过状态的金蝶认领冲单,是否继续?',
'作废后将回退本次认领金额,是否继续?',
'作废认领记录',
{ type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }
);
const kingdeeBillNo = this.unwrapData(await api.voidClaim(row.id));
this.$message.success(`作废成功,金蝶冲单号:${kingdeeBillNo}`);
await api.voidClaim(row.id);
this.$message.success('作废成功');
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);
},
@@ -279,6 +312,13 @@ 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,6 +43,9 @@
<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">
@@ -336,11 +339,11 @@ export default {
for (const flow of this.flows) {
const total = this.flowAllocatedTotal(flow.id);
const remaining = Math.max(
Number(flow.receiptAmount || 0) - Number(flow.claimedAmount || 0),
Number(flow.unclaimedAmount || 0) - Number(flow.claimedAmount || 0),
0
);
if (total > remaining + 0.001) {
this.$message.warning(`流水${flow.receiptNoticeNo}的分摊金额不能超过剩余可认领金额`);
this.$message.warning(`流水${flow.receiptNoticeNo}的累计认领金额不能超过公告未认领金额`);
return;
}
}
+17 -2
View File
@@ -47,6 +47,14 @@
><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>
@@ -197,6 +205,7 @@ const emptyForm = () => ({
transactionTime: '',
detailSerialNo: '',
claimedAmount: 0,
unclaimedAmount: 0,
claimerName: '',
claimerDeptName: '',
claimDate: '',
@@ -242,6 +251,12 @@ export default {
0
);
},
claimableAmount() {
return Math.max(
Number(this.form.unclaimedAmount || 0) - Number(this.form.claimedAmount || 0),
0
);
},
},
created() {
if (!this.batchMode) this.initialize();
@@ -331,8 +346,8 @@ export default {
this.$message.warning('分摊收款金额必须大于0');
return;
}
if (this.allocatedTotal > amount - Number(this.form.claimedAmount || 0) + 0.001) {
this.$message.warning('本次分摊金额不能超过流水剩余可认领金额');
if (this.allocatedTotal > this.claimableAmount + 0.001) {
this.$message.warning('累计认领金额不能超过公告未认领金额(剩余可认领金额)');
return;
}
for (const row of this.form.settlements)
+156 -82
View File
@@ -9,16 +9,20 @@
<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 v-show="searchExpanded" label="摘要">
<el-input v-model="query.summary" clearable placeholder="请输入" />
<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>
<el-form-item v-show="searchExpanded" label="认领状态">
<el-form-item label="认领状态">
<el-select v-model="query.claimStatus" clearable placeholder="请选择">
<el-option
v-for="item in claimStatusOptions"
@@ -28,7 +32,7 @@
/>
</el-select>
</el-form-item>
<el-form-item v-show="searchExpanded" label="交易时间">
<el-form-item label="交易时间">
<el-date-picker
v-model="query.transactionTimeRange"
type="datetimerange"
@@ -39,13 +43,54 @@
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>
@@ -90,7 +135,7 @@
</el-link>
<el-tag
v-else-if="column.status"
:type="statusType(row.claimStatus)"
:type="statusType(row, column)"
class="status-text"
>
{{ displayValue(row[column.prop]) }}
@@ -99,10 +144,23 @@
<span v-else>{{ displayValue(row[column.prop]) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right" align="center">
<el-table-column label="操作" width="170" fixed="right" align="center">
<template #default="{ row }">
<el-link v-if="canClaim(row)" type="primary" @click="openClaim(row)">认领</el-link>
<span v-else>-</span>
<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>
</template>
</el-table-column>
</el-table>
@@ -153,7 +211,7 @@
{{ displayValue(detailDialog.data.detailSerialNo) }}
</el-descriptions-item>
<el-descriptions-item label="认领状态">
<el-tag :type="statusType(detailDialog.data.claimStatus)">
<el-tag :type="statusType(detailDialog.data, { prop: 'claimStatusName' })">
{{ displayValue(detailDialog.data.claimStatusName) }}
</el-tag>
</el-descriptions-item>
@@ -163,6 +221,23 @@
<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>
@@ -177,7 +252,9 @@
</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 }">{{ claimStatusChange(row) }}</template>
<template #default="{ row }">
{{ row.actionType === 'push' ? row.actionName : claimStatusChange(row) }}
</template>
</el-table-column>
<el-table-column prop="content" label="认领说明" min-width="220" />
</el-table>
@@ -190,76 +267,25 @@
</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: [],
});
@@ -267,13 +293,18 @@ 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: [] },
@@ -290,8 +321,9 @@ export default {
},
methods: {
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
// 数字 0 是合法返回值(如回写/同步 0 条),须用 ?? 判空避免 0 被信封对象顶掉导致 Number() 得 NaN
const body = response?.data ?? response ?? {};
return body?.data ?? body;
},
async loadTable() {
this.loading = true;
@@ -327,9 +359,8 @@ export default {
async handleSync() {
this.syncing = true;
try {
const mockRows = createReceiptFlowMockRows();
const syncedCount = Number(this.unwrapData(await api.sync({ flows: mockRows })) || 0);
this.$message.success(`流水同步完成,共同步${syncedCount}条`);
const syncedCount = Number(this.unwrapData(await api.sync()) || 0);
this.$message.success(`流水同步完成,共同步${syncedCount}条认领公告`);
await this.loadTable();
} finally {
this.syncing = false;
@@ -388,8 +419,41 @@ export default {
canClaim(row) {
return this.hasPermission('receipt_flow_claim') && Number(row.remainingAmount || 0) > 0;
},
statusType(status) {
return { claimed: 'success', partial: 'warning', unclaimed: 'info' }[status] || '';
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] || ''
);
},
claimStatusChange(row) {
const label = status =>
@@ -422,7 +486,14 @@ 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-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;
width: 100%;
}
.receipt-flow-page__search-actions {
@@ -452,6 +523,9 @@ 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,6 +22,7 @@
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择"
@change="fetchBfiExchangeRate"
/>
<el-select
v-else-if="field.type === 'project' && editable"
@@ -52,7 +53,7 @@
<el-option
v-for="item in contracts"
:key="item.id"
:label="formatContractOptionLabel(item)"
:label="`${item.contractNo} / ${item.contractName}`"
:value="item.id"
/>
</el-select>
@@ -298,23 +299,15 @@
<span v-else>{{ attachmentTypeName(row.type) }}</span>
</template>
</el-table-column>
<el-table-column
label="文件名"
min-width="220"
align="left"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<el-table-column label="文件名" min-width="220" align="left" show-overflow-tooltip>
<template #default="{ row }">
<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>
<el-link
:disabled="!getAttachmentUrl(row)"
type="primary"
@click="previewAttachment(row)"
>
{{ row.originalName || row.name || '-' }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="uploadUserName" label="上传人" min-width="120" align="center">
@@ -424,78 +417,61 @@
<span v-else>{{ formatMoney(row.matchedAmount) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center">
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row, $index }">
<div class="formal-editor__links">
<el-link type="primary" @click="viewInvoiceAttachment(row)">查看</el-link>
<el-link type="primary" @click="downloadInvoiceAttachment(row)">下载</el-link>
<el-link
type="primary"
:disabled="kingdeeAttachmentBusy"
@click="viewInvoiceAttachment(row)"
>查看</el-link
>
<el-link
type="primary"
:disabled="kingdeeAttachmentBusy"
@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>
<el-dialog
<kingdee-invoice-pool-dialog
v-model="invoiceClaimDialog.visible"
title="查询可认领发票"
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>
confirm-text="确认认领"
multiple
:initial-invoice-no="String(invoiceClaimNo || '').trim()"
:query-api="invoicePoolQueryApi"
@confirm="handleInvoiceClaimConfirm"
/>
<section-card
v-if="readonly"
@@ -966,6 +942,7 @@ import {
getNextNo,
getReceiptClaims,
claimInvoices as claimInvoicesApi,
queryInvoicePool,
save,
} from '@/api/settlement/formalSettlement';
import {
@@ -973,6 +950,7 @@ 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,
@@ -988,13 +966,18 @@ 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 },
components: { InfoFilled, KingdeeInvoicePoolDialog },
props: {
modelValue: Boolean,
recordId: [String, Number],
@@ -1026,15 +1009,16 @@ export default {
attachments: [],
selectedAttachmentRows: [],
invoices: [],
// 开票申请经金蝶回写的发票结果(只读展示,含板式文件/数电PDF下载地址)
issuedInvoices: [],
invoiceClaimNo: '',
invoiceClaimLoading: false,
// 金蝶侧发票附件查看/下载进行中(行组级 loading 守卫)
kingdeeAttachmentBusy: false,
invoiceClaimDialog: {
visible: false,
loading: false,
confirming: false,
rows: [],
selected: [],
page: { current: 1, size: 10, total: 0 },
confirming: false,
},
attachmentUploadFiles: [],
attachmentTypeOptions: [],
@@ -1188,11 +1172,6 @@ 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: {
@@ -1224,7 +1203,6 @@ export default {
},
},
methods: {
formatContractOptionLabel,
async loadFormalDetail(id) {
const response = this.publicMode
? await getMkPublicDetail('formal-settlement', id)
@@ -1254,6 +1232,7 @@ 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);
},
@@ -1283,9 +1262,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 = {
@@ -1309,6 +1288,7 @@ export default {
if (this.initialData) await this.applyInitialData();
Object.assign(this.form, newRecordAudit);
await this.refreshFormalSettlementNo();
await this.fetchBfiExchangeRate();
return;
}
this.loading = true;
@@ -1543,6 +1523,7 @@ export default {
(settlementType === 'receivable' ? contract.partyA : contract.partyB),
settlementType,
settlementTypeName: settlementType === 'receivable' ? '应收' : '应付',
currency: contract.settlementCurrency || 'RMB',
});
this.sources = [];
this.details = [];
@@ -1550,6 +1531,23 @@ 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;
@@ -2274,79 +2272,19 @@ 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;
this.invoiceClaimDialog.loading = false;
},
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();
},
async confirmInvoiceClaims() {
if (!this.invoiceClaimDialog.selected.length) {
@@ -2372,7 +2310,11 @@ export default {
if (Number(this.form.settlementAmount || 0) > 0) {
remaining = Math.max(0, remaining - matchedAmount);
}
return { ...row, matchedAmount };
return {
...row,
invoiceType: row.invoiceTypeName || row.invoiceType || '',
matchedAmount,
};
})
.filter(row => row.matchedAmount > 0);
if (!invoices.length) {
@@ -2458,16 +2400,64 @@ export default {
const attachment = this.invoiceAttachment(row);
return attachment.originalName || attachment.name || `${row.invoiceNo || '发票'}.pdf`;
},
viewInvoiceAttachment(row) {
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;
}
const url = this.invoiceAttachmentUrl(row);
if (!url) return this.$message.warning('当前Mock发票暂无可查看附件');
if (!url) return this.$message.warning('当前发票暂无可查看附件');
window.open(url, '_blank');
},
downloadInvoiceAttachment(row) {
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.invoiceAttachmentUrl(row);
if (!url) return this.$message.warning('当前Mock发票暂无可下载附件');
if (!url) return this.$message.warning('当前发票暂无可下载附件');
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,
@@ -2872,7 +2862,7 @@ export default {
gap: 8px;
}
.formal-editor :deep(.el-form-item) {
margin-bottom: 24px;
margin-bottom: 14px;
}
.formal-editor :deep(.el-form-item__content) {
line-height: normal;
@@ -2949,6 +2939,14 @@ 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