Compare commits

..
Author SHA1 Message Date
b2894lxlx 96b675b3be Merge remote-tracking branch 'websoft/master' 2026-10-16 16:05:55 +08:00
b2894lxlxandCursor 6067d24e3b 🐛 修复列表二次搜索与结算导入回列表,并完善付款申请多结算单展示
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-16 16:03:26 +08:00
liuquanjia e051327f2d Merge pull request 'fix: 批量认领补充剩余可认领金额展示与流水维度分摊上限' (#5) from feature/bfi-api-rate into master
Reviewed-on: #5
2026-10-16 15:34:35 +08:00
刘泉佳 f7759fd9e7 fix: 批量认领补充剩余可认领金额展示与流水维度分摊上限 2026-10-16 15:26:56 +08:00
b2894lxlx 4287133f84 Merge remote-tracking branch 'websoft/dev' 2026-10-16 11:48:10 +08:00
b2894lxlxandCursor 4b01316f64 ✨ 新增项目预付核销 UI,并修复常用线路新增不显示与付款提交后刷新
结算单附件增加项目预付核销;常用线路超管默认查全部组织并按筛选归属新增;付款申请提交后关页并刷新列表;平台公司选项 ID 按字符串绑定。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-16 02:02:00 +08:00
b2894lxlx 0b170fe5c1 Merge remote-tracking branch 'websoft/master' 2026-10-16 00:57:59 +08:00
b2894lxlx 4f29b1e0eb Merge pull request '客商审核通过后推送金蝶失败原因以警告提示展示、以及 付款申请/开票申请/正式结算单列表展示金蝶解锁终态标识' (#4) from feature/bfi-api-rate into master
Reviewed-on: #4
2026-10-16 00:57:51 +08:00
b2894lxlx 6db599400e Merge remote-tracking branch 'websoft/master' 2026-10-16 00:57:19 +08:00
b2894lxlxandCursor c5a74acd00 ✨ 新增导入结算单页面,并优化 MK 开关缓存与客商同步入口
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-16 00:56:55 +08:00
刘泉佳 f23d65e41e :fix: 客商审核通过后推送金蝶失败原因以警告提示展示 2026-10-15 21:14:06 +08:00
weicw-dshsa 6b0cb37699 ✨ 结算明细的单据号、运单号支持跳转应收应付明细与运单详情 2026-10-15 20:41:20 +08:00
weicw-dshsa c867c5d7a5 ⚡ 新增预结算与正式结算合同币种为人民币时隐藏汇率日期、结算汇率与本位币合计 2026-10-15 20:12:05 +08:00
weicw-dshsa 903e0a4d7b 📝 自检端口修正为 2888 2026-10-15 18:55:46 +08:00
weicw-dshsa 6461c3d80f 💄 正式结算单号改为蓝色显示 2026-10-15 18:04:56 +08:00
weicw-dshsa 8380351529 🐛 修复新增预结算页收付方、创建信息与预结算单号为空 2026-10-15 16:56:52 +08:00
weicw-dshsa f8a238d0c0 ⚡ 批量转结算弹窗去掉预结算单号与正式结算单号列 2026-10-15 15:32:06 +08:00
weicw-dshsa 5c6fcbe951 ⚡ 维修记录里程单位改由车船类型自动推导,表单去掉里程单位项
- 里程/航程数标签去掉「(公里)」,表头不再需要 renderHeader 覆盖
- 里程单位列不再出现在表单(display: false),无需人工选择
- 单位仍按车船类型推导:车辆=公里、船舶=海里,保存时再兜底推导一次
2026-10-15 15:14:03 +08:00
weicw-dshsa 7997089b16 🐛 总单列表悬浮气泡展示完整地址、创建时间只保留日期 2026-10-15 14:45:00 +08:00
gxwebsoft 089b74479b Merge remote-tracking branch 'origin/master' 2026-10-15 14:24:34 +08:00
gxwebsoft cf542313f7 style(contract-manage): 注释掉表单项底部外边距
- 将.el-form-item的margin-bottom属性注释,调整样式间距
- 保持对齐方式不变以确保布局稳定
- 便于后续样式调整和优化维护
2026-10-15 14:24:31 +08:00
b2894lxlxandCursor a19097d1be ✨ 组织同步文案与客商固定组织对齐白名单规则
更新部门全量同步说明为 id=1 与二级公司白名单,客商所属组织改为广西现代物流集团有限公司。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-15 12:47:23 +08:00
gxwebsoft 87aa64d0c1 fix(payment): 修复付款申请表单结算单号字段交互体验
- 结算单号由原来的 `el-select multiple` 改为不可编辑的 `el-input readonly`
- 点击输入框触发弹窗选择,保留右侧「选择」按钮
- 占位符改为「请输入」,纠正不可输入与提示不符的问题
- 根据已选单号数量动态显示单号或“已选 N 张结算单”,多张显示全称放 tooltip 提示
- 删除无用的选项选择及相关方法,简化组件逻辑和样式
- 调整样式避免宽度不足导致数据显示被截断问题
- 临时验证页面引入完整 Vue 编译版本确保字符串模板支持
- 更新依赖锁中的 TypeScript 版本等补丁信息
2026-10-15 11:33:10 +08:00
刘泉佳 fbad28b717 feat: 付款申请/开票申请/正式结算单列表展示金蝶解锁终态标识
- lockTag 增加解锁终态分支:金蝶删单红tag、金蝶解锁橙tag,tooltip 展示操作人与解锁时间
- 数据来自后端 fillUnlockMarks 回填的 kingdeeUnlockTime/kingdeeUnlockOperator/kingdeeUnlockOperationName
2026-10-15 11:29:29 +08:00
gxwebsoft 19972aa8d8 🐛 付款申请表单结算单号改为只读输入框+整框可点开选择弹窗,消除请输入歧义 2026-10-15 10:50:25 +08:00
b2894lxlx 5569f373c5 Merge pull request 'Tms对接金蝶接口-前端' (#3) from feature/bfi-api-rate into master
Reviewed-on: #3
2026-10-14 17:28:16 +08:00
刘泉佳 7887ae2342 Merge remote-tracking branch 'origin/master' into feature/bfi-api-rate
# Conflicts:
#	src/views/payment/receipt-flow.vue
2026-10-14 17:18:42 +08:00
刘泉佳 9315431273 🔧 构建跳过 @napi-rs 原生模块修复 build,开发代理切回本地网关 2026-10-14 15:59:49 +08:00
刘泉佳 2c9ce33c10 🐛 补复合并遗漏的金蝶锁状态与附件同步逻辑,修复认领记录页残留重复表格标签 2026-10-14 15:58:57 +08:00
刘泉佳 85602ff98f Merge branch 'master' into feature/bfi-api-rate
# Conflicts:
#	src/components/vehicle-attachment-table/main.vue
#	src/option/payment/paymentApplication.js
#	src/views/payment/payment-application.vue
#	src/views/payment/receipt-claim-record.vue
#	src/views/vehicle/customer-archive.vue
2026-10-14 14:41:40 +08:00
刘泉佳 d8f4f22b9c Merge branch 'feature/payment-apply-bfi-autoSync' into feature/bfi-api-rate 2026-10-12 10:54:57 +08:00
刘泉佳 b077ba5be4 fix: 审批通过提示改用后端返回文案,金蝶自动同步失败弹警告 2026-10-11 14:35:57 +08:00
刘泉佳 b9b579dc14 feat: 付款申请列表新增金蝶锁状态标识并拦截锁定态作废入口 2026-10-10 16:00:32 +08:00
刘泉佳 6ae23bb959 feat: 结算单/开票申请列表新增金蝶锁状态标识与失败原因提示 2026-10-10 15:19:10 +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
79 changed files with 5089 additions and 882 deletions
+1
View File
@@ -1,5 +1,6 @@
.DS_Store .DS_Store
node_modules node_modules
pnpm-lock.yaml
/dist /dist
/tests/e2e/videos/ /tests/e2e/videos/
+16
View File
@@ -124,3 +124,19 @@
另:自定义组件接模板字符串时,切出的模板里 `row.content` 要替换成 `content`(插槽作用域不存在于独立组件里),否则报 `Cannot read properties of undefined (reading 'content')`。 另:自定义组件接模板字符串时,切出的模板里 `row.content` 要替换成 `content`(插槽作用域不存在于独立组件里),否则报 `Cannot read properties of undefined (reading 'content')`。
- 校验:编译三个 .vue 的 template + scss 全过;临时验证页 `__route-change-verify.html` 与 8899 端口静态服务已删除/停止。 - 校验:编译三个 .vue 的 template + scss 全过;临时验证页 `__route-change-verify.html` 与 8899 端口静态服务已删除/停止。
- 未处理(等用户确认):备注列的 `show-overflow-tooltip` 保持原样(属全站约定),用户若要求连备注一起去掉再单独改。 - 未处理(等用户确认):备注列的 `show-overflow-tooltip` 保持原样(属全站约定),用户若要求连备注一起去掉再单独改。
## 16:45 付款申请表单「结算单号」字段交互歧义修复(方案 A)
- 现象(用户截图圈出):`/payment/payment-application/form` 结算单号是 `el-select multiple`,placeholder 写「请输入」但**打不进去字**,必须先猜到要点右边「选择」才有效。
- 根因(双重的):
1. 该 `el-select` 的 `:model-value` 绑只读 computed `referenceSettlementNos`(= 已选单号本身),**options 就是它自己** → 点开下拉、点选项,值不变;代码里 `handleSettlementSelectVisible` 甚至主动 blur 掉下拉。真正改值的只有右侧「选择」按钮。
2. placeholder 写「请输入」但控件不可输入(未开 `filterable`),且 `.el-tag__close{display:none}` 藏了删除叉 → 视觉上更像输入框。全站规矩是「请输入/请选择」二选一,这里是选择语义。
- 落地(只改 `views/payment/payment-application-form.vue`,全站 `openReference`/`referenceSettlementNos` 无第二处引用):
- 模板:`el-select` → `el-input readonly` + 外层 `el-tooltip`,`:model-value="referenceDisplayText"`,`placeholder="请输入"`,`:disabled="readonly"`,**`@click="!readonly && openReference()"` 让整框可点开弹窗**;右侧「选择」按钮保留。
- 新增 computed:`referenceSettlementCount`、`referenceDisplayText`(单张显示单号;**多张只显示 `已选 N 张结算单`**,完整单号靠 tooltip,明细看下方「结算信息」表)。
- 删除 `handleSettlementSelectVisible` 方法与 `settlementNoSelectRef`;样式 `.payment-form-page__settlement-select{,:deep(...)}` → `.payment-form-page__settlement-input`(+ `cursor:pointer` / disabled 时 `not-allowed`)。
- ⚠️ 踩坑:第一版把「共 N 张」做成 `el-input` 的 `#suffix` 插槽 → span=6 的窄列里后缀挤占宽度,单号被截成 `JS202610`,比不改还差。**窄列(≤span6)不要在输入框内塞后缀/前缀,把信息编进 `:model-value` 文本本身**。
- 验证(项目根临时 html + `vue/dist/vue.esm-bundler.js`,用完即删):
- ⚠️ 临时页必须 import `vue/dist/vue.esm-bundler.js`,默认 `vue` 是 runtime-only,字符串 template 报 “runtime compilation is not supported”。
- 4 态实测:空值 placeholder 正确 / 单张显单号 / 多张显「已选 3 张结算单」且不截断 / 查看态 disabled 且按钮隐藏;输入框宽 132px、行高 32px。
- 点击探针验证:可编辑态点 `.el-input` 命中 1 次(el-tooltip 不拦截 click),查看态命中 0 次 ✓。
+7
View File
@@ -0,0 +1,7 @@
# 2026-10-15
## 10:46 付款申请表单「结算单号」修复丢失 → 重放并提交
- 现象:用户反馈昨天改好的「结算单号」(只读输入框 + 整框可点开选择弹窗)今天又变回旧的 `el-select` 写法。
- 根因:昨天的改动**一直没有 commit**,是纯工作区改动;用户 pull(HEAD 到 9919fb9,两个 settlement 样式提交包含该文件旧版)后被覆盖。教训同 2026-09-18「漏提交新文件」一役:**本仓库改动必须尽快显式 commit**,`git status` 干净 ≠ 改动还在。
- 处理:按昨天方案 A 原样重放 4 处修改(模板 el-select→el-input readonly+tooltip+整框可点、新增 `referenceSettlementCount`/`referenceDisplayText` computed、删 `handleSettlementSelectVisible`、样式类改名 `__settlement-input` + cursor 规则),curl 自检 vue/style 均 200。
- **这次只提交该单文件**:`git commit -m ":bug: ..." -- src/views/payment/payment-application-form.vue` → `db03ced`(`.workbuddy/memory` 不入库,留在工作区)。尚未 push,等用户确认。
+1 -1
View File
@@ -66,7 +66,7 @@
## 表单 / 自检 / 导出 ## 表单 / 自检 / 导出
- placeholder 只写 `请输入`/`请选择`;例外:`请输入或选择车辆`、日期区间 `起`/`止`、示例值。校验 message 须带字段名。 - placeholder 只写 `请输入`/`请选择`;例外:`请输入或选择车辆`、日期区间 `起`/`止`、示例值。校验 message 须带字段名。
- `business-crud-page.vue`:option 经 `cloneOption` 克隆,独立表单页走 `PageAvueForm`。 - `business-crud-page.vue`:option 经 `cloneOption` 克隆,独立表单页走 `PageAvueForm`。
- 自检:`curl "localhost:2889/src/xxx.vue"` 与 `"...?vue&type=style&index=0&scoped=true&lang.scss"`(200 / 500)。`vite build` 在 `facelogin.vue` 被沙箱中断,不能作唯一校验。dev `VITE_APP_API=/api` 代理 `172.16.203.228:8000`。 - 自检:`curl "localhost:2888/src/xxx.vue"` 与 `"...?vue&type=style&index=0&scoped=true&lang.scss"`(200 / 500)。`vite build` 在 `facelogin.vue` 被沙箱中断,不能作唯一校验。dev `VITE_APP_API=/api` 代理 `172.16.203.228:8000`。
- 导出:前端 `exportColumns` 后端未用,以 `XxxExportExcel.java` 为准;BladeX `BeanUtil` 类型不兼容静默跳过(`Date` → `LocalDateTime` 丢值,用 `DateUtil.fromDate`)。 - 导出:前端 `exportColumns` 后端未用,以 `XxxExportExcel.java` 为准;BladeX `BeanUtil` 类型不兼容静默跳过(`Date` → `LocalDateTime` 丢值,用 `DateUtil.fromDate`)。
## Element Plus 图标两种用法的机制差异(易误判) ## Element Plus 图标两种用法的机制差异(易误判)
+5 -2
View File
@@ -6,7 +6,8 @@
"prod": "vite --mode production", "prod": "vite --mode production",
"build": "vite build", "build": "vite build",
"build:prod": "vite build --mode production", "build:prod": "vite build --mode production",
"serve": "vite preview --host" "serve": "vite preview --host",
"typecheck": "vue-tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
@@ -47,9 +48,11 @@
"prettier": "^2.8.7", "prettier": "^2.8.7",
"sass": "^1.85.1", "sass": "^1.85.1",
"terser": "^5.31.1", "terser": "^5.31.1",
"typescript": "^7.0.2",
"unplugin-auto-import": "^0.11.2", "unplugin-auto-import": "^0.11.2",
"vite": "^5.4.19", "vite": "^5.4.19",
"vite-plugin-compression": "^0.5.1", "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"
} }
} }
+415 -95
View File
File diff suppressed because it is too large Load Diff
+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,
},
});
};
+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 returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data }); export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
export const syncKingdee = id => 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 = [ export const invoiceTypeOptions = [
{ label: '电子专票', value: 'electronic_special' }, { label: '电子专票', value: 'electronic_special' },
+18 -8
View File
@@ -5,8 +5,17 @@ const baseUrl = '/blade-transport/invoice-receipt';
export const getList = (current, size, params) => export const getList = (current, size, params) =>
request({ url: `${baseUrl}/list`, method: 'get', params: { 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 getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
export const getInvoicePool = keyword => export const queryInvoicePool = params =>
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params: { keyword } }); 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) => export const getSettlementCandidates = (keyword, receiptId) =>
request({ request({
url: `${baseUrl}/settlement-candidates`, 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 approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
export const returnBill = data => request({ url: `${baseUrl}/return`, 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 voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
export const syncKingdee = id => export const pushKingdee = ids =>
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } }); request({ url: `${baseUrl}/push-kingdee`, method: 'post', timeout: 30000, data: ids });
export const approvalStatusOptions = [ export const approvalStatusOptions = [
{ label: '草稿', value: 'draft' }, { label: '草稿', value: 'draft' },
@@ -36,8 +45,9 @@ export const approvalStatusOptions = [
{ label: '已作废', value: 'voided' }, { label: '已作废', value: 'voided' },
]; ];
export const kingdeeStatusOptions = [ export const kingdeePushStatusOptions = [
{ label: '未同步', value: 'unsynced' }, { label: '未推送', value: 'unsynced' },
{ label: '已同步', value: 'synced' }, { label: '已保存收票单', value: 'pushed' },
{ label: '同步失败', value: 'failed' }, { label: '已指定应付', value: 'assigned' },
{ label: '推送失败', value: 'failed' },
]; ];
+17 -2
View File
@@ -10,16 +10,28 @@ export const getReferenceAmount = (paymentType, referenceId, excludeId) =>
method: 'get', method: 'get',
params: { paymentType, referenceId, excludeId: excludeId || undefined }, params: { paymentType, referenceId, excludeId: excludeId || undefined },
}); });
/** 按合同查询可选项目预付核销列表(审批通过且仍有可核销余额) */
export const getProjectAdvanceWriteOffOptions = params =>
request({
url: `${baseUrl}/project-advance-write-off-options`,
method: 'get',
params,
});
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data }); export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } }); export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data }); export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data }); export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data }); export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
export const voidBill = data => request({ url: `${baseUrl}/void`, 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 => 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 => 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 = [ export const paymentTypeOptions = [
{ label: '项目预付', value: 'project_advance' }, { label: '项目预付', value: 'project_advance' },
@@ -36,5 +48,8 @@ export const approvalStatusOptions = [
export const kingdeeStatusOptions = [ export const kingdeeStatusOptions = [
{ label: '未生成', value: 'unsynced' }, { label: '未生成', value: 'unsynced' },
{ label: '已生成', value: 'synced' }, { label: '已生成', value: 'synced' },
{ label: '付款中', value: 'paying' },
{ label: '已付款', value: 'paid' },
{ label: '已关闭', value: 'closed' },
{ label: '生成失败', value: 'failed' }, { label: '生成失败', value: 'failed' },
]; ];
+10
View File
@@ -8,14 +8,24 @@ export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get'
export const updateAttachments = data => export const updateAttachments = data =>
request({ url: `${baseUrl}/attachments`, method: 'post', data }); request({ url: `${baseUrl}/attachments`, method: 'post', data });
export const voidClaim = id => request({ url: `${baseUrl}/void`, method: 'post', params: { id } }); 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 = [ export const claimStatusOptions = [
{ label: '已认领', value: 'claimed' }, { label: '已认领', value: 'claimed' },
{ label: '已作废', value: 'voided' }, { label: '已作废', value: 'voided' },
]; ];
export const pushStatusOptions = [
{ label: '未推送', value: 'none' },
{ label: '已推送', value: 'pushed' },
{ label: '推送失败', value: 'failed' },
];
export const kingdeeBillStatusOptions = [ export const kingdeeBillStatusOptions = [
{ label: '未生成', value: 'none' }, { label: '未生成', value: 'none' },
{ label: '已推送', value: 'pushed' },
{ label: '审核通过', value: 'approved' }, { label: '审核通过', value: 'approved' },
{ label: '已收款', value: 'received' },
{ label: '处理失败', value: 'failed' }, { label: '处理失败', value: 'failed' },
]; ];
+16 -1
View File
@@ -12,10 +12,25 @@ export const getSettlementCandidates = (keyword, flowId) =>
params: { keyword, flowId }, params: { keyword, flowId },
}); });
export const claim = data => request({ url: `${baseUrl}/claim`, method: 'post', data }); 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 = [ export const claimStatusOptions = [
{ label: '未认领', value: 'unclaimed' }, { label: '未认领', value: 'unclaimed' },
{ label: '部分认领', value: 'partial' }, { label: '部分认领', value: 'partial' },
{ label: '认领完成', value: 'claimed' }, { 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 approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
export const returnBill = data => request({ url: `${baseUrl}/return`, 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 voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
// 同步金蝶涉及多次金蝶远程接口调用(A102-4 预检 + A102-2 保存),可达 20s+,覆盖全局 10s 超时
export const syncKingdee = id => 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 => export const getDetailFees = detailId =>
request({ url: `${baseUrl}/detail-fees`, method: 'get', params: { detailId } }); request({ url: `${baseUrl}/detail-fees`, method: 'get', params: { detailId } });
export const getReceiptClaims = formalSettlementId => export const getReceiptClaims = formalSettlementId =>
@@ -48,3 +49,5 @@ export const applyPayments = data =>
request({ url: `${baseUrl}/apply-payments`, method: 'post', data }); request({ url: `${baseUrl}/apply-payments`, method: 'post', data });
export const claimInvoices = data => export const claimInvoices = data =>
request({ url: `${baseUrl}/claim-invoices`, method: 'post', data }); request({ url: `${baseUrl}/claim-invoices`, method: 'post', data });
export const queryInvoicePool = params =>
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params });
+2
View File
@@ -24,6 +24,8 @@ export const getContractList = (current, size, params) =>
export const getFeeOptions = () => request({ url: `${baseUrl}/fee-options`, method: 'get' }); export const getFeeOptions = () => request({ url: `${baseUrl}/fee-options`, method: 'get' });
export const getNextNo = () => request({ url: `${baseUrl}/next-no`, method: 'get' });
export const getCandidateDetails = (current, size, params) => export const getCandidateDetails = (current, size, params) =>
request({ request({
url: `${baseUrl}/candidate-details`, url: `${baseUrl}/candidate-details`,
+55
View File
@@ -0,0 +1,55 @@
import request from '@/axios';
const baseUrl = '/blade-transport/settlement-import';
export const getOptions = () => request({ url: `${baseUrl}/options`, method: 'get' });
export const getNextCode = () => request({ url: `${baseUrl}/next-code`, method: 'get' });
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 exportTemplate = () =>
request({
url: `${baseUrl}/export-template`,
method: 'get',
responseType: 'blob',
});
export const parseFile = file => {
const data = new FormData();
data.append('file', file);
return request({
url: `${baseUrl}/parse`,
method: 'post',
data,
timeout: 120000,
});
};
export const validate = data =>
request({ url: `${baseUrl}/validate`, method: 'post', data, timeout: 120000 });
export const exportFailures = data =>
request({
url: `${baseUrl}/export-failures`,
method: 'post',
data,
responseType: 'blob',
timeout: 120000,
});
export const saveDraft = data =>
request({ url: `${baseUrl}/draft`, method: 'post', data, timeout: 120000 });
export const confirm = data =>
request({ url: `${baseUrl}/confirm`, method: 'post', data, timeout: 120000 });
export const voidBatch = data => request({ url: `${baseUrl}/void`, method: 'post', data });
+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 => { export const recognizeBusinessLicenseOcr = imageUrl => {
return request({ return request({
url: '/blade-transport/baidu-ocr/recognize-url', url: '/blade-transport/baidu-ocr/recognize-url',
+9 -1
View File
@@ -271,10 +271,18 @@ axios.interceptors.response.use(
}, },
error => { error => {
NProgress.done(); 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 // 保留原始 axios 错误对象:下游(如导入失败明细解析)需要 error.response
// 读取响应体。若包装成 new Error(error),response 会丢失、message 退化成 // 读取响应体。若包装成 new Error(error),response 会丢失、message 退化成
// "[object Object]",用户只能看到无意义的通用提示。 // "[object Object]",用户只能看到无意义的通用提示。
return Promise.reject(error);
} }
); );
@@ -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>
@@ -43,9 +43,23 @@
</el-table-column> </el-table-column>
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" /> <el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
<el-table-column prop="uploadTime" label="上传时间" width="180" align="center" sortable /> <el-table-column prop="uploadTime" label="上传时间" width="180" align="center" sortable />
<el-table-column v-if="!readonly" label="操作" width="100" align="center" fixed="right"> <el-table-column
<template #default="{ $index }"> v-if="!readonly || showViewActions"
<el-link type="danger" @click="removeAttachment($index)">删除</el-link> 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> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -71,6 +85,7 @@
width="90%" width="90%"
top="4vh" top="4vh"
class="vehicle-attachment-table__viewer-dialog" class="vehicle-attachment-table__viewer-dialog"
@closed="handlePreviewClosed"
> >
<pdf-preview <pdf-preview
v-if="documentPreviewVisible && previewFile.url && previewFile.isPdf" v-if="documentPreviewVisible && previewFile.url && previewFile.isPdf"
@@ -111,6 +126,11 @@ import '@open-file-viewer/core/style.css';
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url'; import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
import PdfPreview from '@/components/pdf-preview/main.vue'; import PdfPreview from '@/components/pdf-preview/main.vue';
import { downloadFileByUrl } from '@/utils/util'; import { downloadFileByUrl } from '@/utils/util';
import {
downloadKingdeeInvoiceFile,
isKingdeePathAttachment,
requestKingdeeFile,
} from '@/utils/kingdee-attachment';
const attachmentFileTypes = [ const attachmentFileTypes = [
'pdf', 'pdf',
@@ -156,6 +176,16 @@ export default {
type: Number, type: Number,
default: 500, default: 500,
}, },
// 金蝶侧附件(piaozone path 型)代理下载所依赖的发票号;行内自带 invoiceNo 时优先用行内值
kingdeeInvoiceNo: {
type: String,
default: '',
},
// 查看态也展示「查看/下载」操作列(编辑态始终展示,含删除)
showViewActions: {
type: Boolean,
default: false,
},
}, },
emits: ['update:modelValue'], emits: ['update:modelValue'],
data() { data() {
@@ -167,6 +197,10 @@ export default {
imagePreviewIndex: 0, imagePreviewIndex: 0,
documentPreviewVisible: false, documentPreviewVisible: false,
previewFile: {}, previewFile: {},
// 金蝶侧附件代理请求进行中(行级按钮共享 loading 守卫,防重复触发)
kingdeeBusy: false,
// 金蝶附件预览用的 objectURL,弹窗关闭时回收
kingdeeObjectUrl: '',
viewerPlugins, viewerPlugins,
viewerToolbar: { viewerToolbar: {
download: true, download: true,
@@ -201,6 +235,12 @@ export default {
attachmentUrl(row = {}) { attachmentUrl(row = {}) {
return row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || row.src || ''; 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 = {}) { attachmentExtension(row = {}) {
const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0]; const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0];
const index = source.lastIndexOf('.'); const index = source.lastIndexOf('.');
@@ -248,6 +288,10 @@ export default {
return `${(value / 1024 / 1024).toFixed(1)}MB`; return `${(value / 1024 / 1024).toFixed(1)}MB`;
}, },
downloadAttachment(row) { downloadAttachment(row) {
// 金蝶侧附件(piaozone path 型)经后端代理 A801-2 下载,OSS 直链维持前端直连下载
if (this.isKingdeeRow(row)) {
return this.downloadKingdeeAttachment(row);
}
const url = this.attachmentUrl(row); const url = this.attachmentUrl(row);
if (!url) { if (!url) {
this.$message.warning('附件地址为空,无法下载'); this.$message.warning('附件地址为空,无法下载');
@@ -255,20 +299,47 @@ export default {
} }
downloadFileByUrl(url, this.attachmentName(row)); 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() { batchDownload() {
const rows = this.selectedRows.length ? this.selectedRows : this.rows; const rows = this.selectedRows.length ? this.selectedRows : this.rows;
const downloadableRows = rows.filter(row => this.attachmentUrl(row)); const kingdeeRows = rows.filter(row => this.isKingdeeRow(row));
if (!downloadableRows.length) { const urlRows = rows.filter(row => !this.isKingdeeRow(row) && this.attachmentUrl(row));
if (!kingdeeRows.length && !urlRows.length) {
this.$message.warning( this.$message.warning(
this.selectedRows.length ? '所选附件暂无可下载地址' : '暂无可下载附件' this.selectedRows.length ? '所选附件暂无可下载地址' : '暂无可下载附件'
); );
return; return;
} }
downloadableRows.forEach((row, index) => { urlRows.forEach((row, index) => {
window.setTimeout(() => this.downloadAttachment(row), index * 500); 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) { previewAttachment(row) {
// 金蝶侧附件经 A801-2 代理取流后进弹窗预览,OSS 直链维持原逻辑
if (this.isKingdeeRow(row)) {
return this.previewKingdeeAttachment(row);
}
const url = this.attachmentUrl(row); const url = this.attachmentUrl(row);
if (!url) { if (!url) {
this.$message.warning('附件地址为空,无法预览'); this.$message.warning('附件地址为空,无法预览');
@@ -291,6 +362,42 @@ export default {
}; };
this.documentPreviewVisible = true; 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() { handlePreviewUnsupported() {
this.$message.warning('当前文件暂不支持在线预览'); this.$message.warning('当前文件暂不支持在线预览');
}, },
@@ -311,6 +418,27 @@ export default {
margin-bottom: 16px; 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 { &__upload {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
+2
View File
@@ -13,6 +13,8 @@ export const invoiceApplicationTableColumns = [
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 }, { prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 }, { prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 }, { prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
{ prop: 'kingdeeLockStatus', label: '金蝶锁状态', minWidth: 150 },
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
]; ];
export const invoiceApplicationDetailColumns = [ export const invoiceApplicationDetailColumns = [
+4 -2
View File
@@ -11,6 +11,8 @@ export const invoiceReceiptTableColumns = [
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 }, { prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
{ prop: 'createUserName', label: '创建人', minWidth: 110 }, { prop: 'createUserName', label: '创建人', minWidth: 110 },
{ prop: 'createTime', label: '创建时间', minWidth: 170 }, { prop: 'createTime', label: '创建时间', minWidth: 170 },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 }, { prop: 'kingdeeReceivedInvoiceNo', label: '金蝶单据号', minWidth: 150 },
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 }, { prop: 'kingdeePushStatusName', label: '金蝶状态', minWidth: 130 },
{ prop: 'kingdeePushMsg', label: '失败原因', minWidth: 160 },
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
]; ];
+2
View File
@@ -161,5 +161,7 @@ export const paymentApplicationOption = ({
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 120, formatter: textFormatter }, { label: '当前处理人', prop: 'currentProcessor', minWidth: 120, formatter: textFormatter },
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 130, formatter: textFormatter }, { label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 130, formatter: textFormatter },
{ label: '金蝶单据状态', prop: 'kingdeeStatusName', minWidth: 130, formatter: textFormatter }, { label: '金蝶单据状态', prop: 'kingdeeStatusName', minWidth: 130, formatter: textFormatter },
{ label: '金蝶锁状态', prop: 'kingdeeLockStatus', slot: true, minWidth: 150 },
{ label: '附件同步', prop: 'attachmentSyncStatusName', minWidth: 100, formatter: textFormatter },
], ],
}); });
+6
View File
@@ -8,5 +8,11 @@ export const receiptFlowTableColumns = [
{ prop: 'summary', label: '摘要', minWidth: 160 }, { prop: 'summary', label: '摘要', minWidth: 160 },
{ prop: 'transactionTime', label: '交易时间', minWidth: 170 }, { prop: 'transactionTime', label: '交易时间', minWidth: 170 },
{ prop: 'claimStatusName', label: '认领状态', minWidth: 110, status: true }, { prop: 'claimStatusName', label: '认领状态', minWidth: 110, status: true },
{ prop: 'kingdeeClaimStatusName', label: '公告状态', minWidth: 110 },
{ prop: 'claimedAmount', label: '已认领金额', minWidth: 130, money: true }, { 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,8 @@ export const formalSettlementTableColumns = [
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 }, { label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 }, { label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 },
{ label: '金蝶单据状态', prop: 'kingdeeSyncStatus', minWidth: 135 }, { label: '金蝶单据状态', prop: 'kingdeeSyncStatus', minWidth: 135 },
{ label: '金蝶锁状态', prop: 'kingdeeLockStatus', minWidth: 150 },
{ label: '附件同步', prop: 'attachmentSyncStatusName', minWidth: 100 },
{ label: '创建人', prop: 'createUserName', minWidth: 110 }, { label: '创建人', prop: 'createUserName', minWidth: 110 },
{ label: '创建时间', prop: 'createTime', minWidth: 170 }, { label: '创建时间', prop: 'createTime', minWidth: 170 },
]; ];
@@ -33,8 +35,8 @@ export const sourceColumns = [
]; ];
export const detailColumns = [ export const detailColumns = [
{ label: '单据号', prop: 'documentNo', minWidth: 180 }, { label: '单据号', prop: 'documentNo', minWidth: 180, link: true },
{ label: '运单号', prop: 'waybillNo', minWidth: 160 }, { label: '运单号', prop: 'waybillNo', minWidth: 160, link: true },
{ label: '车号', prop: 'vehicleNo', minWidth: 130 }, { label: '车号', prop: 'vehicleNo', minWidth: 130 },
{ label: '发货地址', prop: 'departureAddress', minWidth: 180 }, { label: '发货地址', prop: 'departureAddress', minWidth: 180 },
{ label: '到货地址', prop: 'arrivalAddress', minWidth: 180 }, { label: '到货地址', prop: 'arrivalAddress', minWidth: 180 },
+1 -1
View File
@@ -40,7 +40,7 @@ export const settlementSummaryColumns = [
export const settlementDetailColumns = [ export const settlementDetailColumns = [
{ label: '单据号', prop: 'documentNo', minWidth: 180, link: true }, { label: '单据号', prop: 'documentNo', minWidth: 180, link: true },
{ label: '运单号', prop: 'waybillNo', minWidth: 160 }, { label: '运单号', prop: 'waybillNo', minWidth: 160, link: true },
{ label: '车号', prop: 'vehicleNo', minWidth: 130 }, { label: '车号', prop: 'vehicleNo', minWidth: 130 },
{ label: '发货地址', prop: 'departureAddress', minWidth: 180 }, { label: '发货地址', prop: 'departureAddress', minWidth: 180 },
{ label: '到货地址', prop: 'arrivalAddress', minWidth: 180 }, { label: '到货地址', prop: 'arrivalAddress', minWidth: 180 },
@@ -88,6 +88,8 @@ export const updateFeeFormFields = [
]; ];
export const transferSearchFields = [ export const transferSearchFields = [
{ label: '单据号', prop: 'documentNo', type: 'input' },
{ label: '合同编码', prop: 'contractNo', type: 'input' },
{ label: '合同名称', prop: 'contractName', type: 'input' }, { label: '合同名称', prop: 'contractName', type: 'input' },
{ label: '批次号', prop: 'batchNo', type: 'input' }, { label: '批次号', prop: 'batchNo', type: 'input' },
{ label: '生成日期', prop: 'generateDateRange', type: 'daterange' }, { label: '生成日期', prop: 'generateDateRange', type: 'daterange' },
+15
View File
@@ -119,6 +119,21 @@ export default [
}, },
], ],
}, },
{
path: '/settlement/settlement-import/form',
component: Layout,
children: [
{
path: '',
name: '新增导入结算单',
meta: {
keepAlive: false,
activeMenu: '/settlement/settlement-import',
},
component: loadView(() => import('@/views/settlement/settlement-import-form.vue')),
},
],
},
{ {
path: '/settlement/transport-reconciliation/form', path: '/settlement/transport-reconciliation/form',
component: Layout, component: Layout,
+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 || '金蝶附件预览失败');
}
};
+65 -13
View File
@@ -61,9 +61,18 @@ export const MK_BIZ = {
const MK_SWITCH_NAME = '开启MK'; const MK_SWITCH_NAME = '开启MK';
async function loadMkTemplateList() { /** 进程内缓存,避免每次提交都打字典;登录后同一会话开关一般不变 */
let mkEnabledCache = null;
let mkEnabledPromise = null;
let mkTemplateListCache = null;
async function loadMkTemplateList(force = false) {
if (!force && mkTemplateListCache) {
return mkTemplateListCache;
}
const res = await getDictionary({ code: 'mk_template' }); const res = await getDictionary({ code: 'mk_template' });
return res?.data?.data || []; mkTemplateListCache = res?.data?.data || [];
return mkTemplateListCache;
} }
/** 业务字典 mk_template:名称「开启MK」且键值为 1 时才请求 MK */ /** 业务字典 mk_template:名称「开启MK」且键值为 1 时才请求 MK */
@@ -72,10 +81,37 @@ export function isMkEnabled(list = []) {
return String(matched?.dictKey ?? '').trim() === '1'; return String(matched?.dictKey ?? '').trim() === '1';
} }
/** 加载业务字典并判断是否开启 MK */ /** 清除 MK 开关缓存(字典变更后可调用) */
export async function loadMkEnabled() { export function clearMkEnabledCache() {
const list = await loadMkTemplateList(); mkEnabledCache = null;
return isMkEnabled(list); mkEnabledPromise = null;
mkTemplateListCache = null;
}
/**
* 加载业务字典并判断是否开启 MK
* @param {boolean} [force=false] 是否强制刷新缓存
*/
export async function loadMkEnabled(force = false) {
if (!force && mkEnabledCache !== null) {
return mkEnabledCache;
}
if (!force && mkEnabledPromise) {
return mkEnabledPromise;
}
mkEnabledPromise = loadMkTemplateList(force)
.then(list => {
mkEnabledCache = isMkEnabled(list);
return mkEnabledCache;
})
.catch(error => {
mkEnabledCache = false;
throw error;
})
.finally(() => {
mkEnabledPromise = null;
});
return mkEnabledPromise;
} }
export async function resolveMkTemplateCode(dictName, list) { export async function resolveMkTemplateCode(dictName, list) {
@@ -99,19 +135,22 @@ export function isMkApprovedStatus(bizType, approvalStatus) {
} }
/** /**
* 提交 MK 审核流 * 提交 MK 审核流。未开启 MK 时直接跳过,不请求流程实例相关接口。
* @param {Object} options * @param {Object} options
* @param {string} options.bizType 业务类型 * @param {string} options.bizType 业务类型
* @param {string|number} options.bizId 业务主键(后端据此生成/复用 formInstanceId) * @param {string|number} options.bizId 业务主键
* @param {string} [options.subjectName] 标题后缀 * @param {string} [options.subjectName] 标题后缀
* @param {string} [options.approvalStatus] 提交前的审批状态(须在业务 submit 改状态前取值;通过态强制新建实例) * @param {string} [options.approvalStatus] 提交前的审批状态
* @param {boolean} [options.mkEnabled] 调用方已知开关时传入,false 时立即跳过
* @returns {Promise<boolean>} true=已发起/复用 MK 流程,false=未开启或已跳过
*/ */
export async function submitMkApprovalFlow({ export async function submitMkApprovalFlow({
bizType, bizType,
bizId, bizId,
subjectName = '', subjectName = '',
approvalStatus = '', approvalStatus = '',
}) { mkEnabled,
} = {}) {
const conf = MK_BIZ[bizType]; const conf = MK_BIZ[bizType];
if (!conf) { if (!conf) {
return Promise.reject(new Error(`不支持的MK业务类型:${bizType}`)); return Promise.reject(new Error(`不支持的MK业务类型:${bizType}`));
@@ -119,17 +158,29 @@ export async function submitMkApprovalFlow({
if (bizId == null || bizId === '') { if (bizId == null || bizId === '') {
return Promise.reject(new Error('业务id不能为空')); return Promise.reject(new Error('业务id不能为空'));
} }
// 调用方显式关闭,或字典未开启:不请求 hasMkProcessInstance / processSubmit 等 MK 接口
if (mkEnabled === false) {
return false;
}
const enabled = mkEnabled === true ? true : await loadMkEnabled();
if (!enabled) {
return false;
}
const list = await loadMkTemplateList(); const list = await loadMkTemplateList();
if (!isMkEnabled(list)) { if (!isMkEnabled(list)) {
return; mkEnabledCache = false;
return false;
} }
const bizIdText = String(bizId); const bizIdText = String(bizId);
const forceNewFormInstance = isMkApprovedStatus(bizType, approvalStatus); const forceNewFormInstance = isMkApprovedStatus(bizType, approvalStatus);
// 审核通过后再提交必须新建流程;其余场景已有有效实例则跳过 // 审核通过后再提交必须新建流程;其余场景已有有效实例则跳过
if (!forceNewFormInstance) { if (!forceNewFormInstance) {
const existsRes = await hasMkProcessInstance({ bizId: bizIdText }); const existsRes = await hasMkProcessInstance({ bizId: bizIdText });
if (existsRes?.data?.data) { if (existsRes?.data?.data) {
return; return true;
} }
} }
if ((conf.rejected || []).includes(approvalStatus)) { if ((conf.rejected || []).includes(approvalStatus)) {
@@ -139,11 +190,12 @@ export async function submitMkApprovalFlow({
const subject = subjectName const subject = subjectName
? `${conf.subjectPrefix}:${subjectName}` ? `${conf.subjectPrefix}:${subjectName}`
: `${conf.subjectPrefix}:${bizIdText}`; : `${conf.subjectPrefix}:${bizIdText}`;
return processSubmit({ await processSubmit({
templateCode, templateCode,
bizId: bizIdText, bizId: bizIdText,
subject, subject,
bizType, bizType,
forceNewFormInstance, forceNewFormInstance,
}); });
return true;
} }
+32
View File
@@ -1,3 +1,35 @@
/**
* 合并列表搜索条件:新条件覆盖旧条件,本次显式清空的字段从查询中移除。
*/
export const applyIncomingSearch = (current = {}, incoming = {}) => {
const merged = { ...current, ...incoming };
Object.keys(incoming || {}).forEach(key => {
const value = incoming[key];
if (
value === '' ||
value === null ||
value === undefined ||
(Array.isArray(value) && value.length === 0)
) {
delete merged[key];
}
});
return merged;
};
/**
* 启停状态下拉「全部」在数字类型下会被转成 0/-1,不能当作查询条件。
* 状态 0 表示草稿等有效值的页面不要调用。
*/
export const omitInvalidListStatus = (params = {}) => {
const values = { ...params };
const status = values.status;
if (status === 0 || status === '0' || status === -1 || status === '-1') {
delete values.status;
}
return values;
};
export const normalizeSearchRangeParams = (params = {}, rangeMap = {}) => { export const normalizeSearchRangeParams = (params = {}, rangeMap = {}) => {
const values = { ...params }; const values = { ...params };
+51
View File
@@ -0,0 +1,51 @@
import { getList as getReceivablePayableDetailList } from '@/api/settlement/receivable-payable-detail';
import { getList as getWaybillList } from '@/api/business/waybill-manage';
// 结算明细的「单据号」是应收应付明细单据号(应收 YS…/应付 YF…),按结算类型落到对应菜单页
const SETTLEMENT_DETAIL_PATHS = {
receivable: '/settlement/receivable-detail',
payable: '/settlement/payable-detail',
};
const unwrapPage = response => {
const body = response?.data || response || {};
return body?.data || body;
};
const firstRecordId = data => (Array.isArray(data) ? data : data?.records || [])[0]?.id || '';
export const settlementDetailPath = settlementType =>
SETTLEMENT_DETAIL_PATHS[settlementType] || SETTLEMENT_DETAIL_PATHS.payable;
/**
* 结算明细行「单据号」对应的应收应付明细ID:已落库的明细自带 sourceDetailId,未落库的行按单据号回查。
*/
export const resolveReceivablePayableDetailId = async (row = {}, settlementType) => {
if (row.sourceDetailId) return row.sourceDetailId;
if (!row.documentNo) return '';
try {
const data = unwrapPage(
await getReceivablePayableDetailList(1, 1, {
documentNo: row.documentNo,
settlementType,
})
);
return firstRecordId(data);
} catch (error) {
return '';
}
};
/**
* 结算明细行「运单号」对应的运单ID:总单来源的明细没有 waybillId(运单号列存的是总单号),按运单号回查。
*/
export const resolveWaybillId = async (row = {}) => {
if (row.waybillId) return row.waybillId;
if (!row.waybillNo) return '';
try {
const data = unwrapPage(await getWaybillList(1, 1, { waybillNo: row.waybillNo }));
return firstRecordId(data);
} catch (error) {
return '';
}
};
+2 -1
View File
@@ -150,6 +150,7 @@ import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util'; import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel'; import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit'; import { formatUpdateUserName } from '@/utils/audit';
import { omitInvalidListStatus } from '@/utils/search-range';
import { getToken, isAdministrator } from '@/utils/auth'; import { getToken, isAdministrator } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate'; import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import AddressMapPicker from '@/components/address-map-picker/main.vue'; import AddressMapPicker from '@/components/address-map-picker/main.vue';
@@ -1182,7 +1183,7 @@ export default {
}, },
onLoad(page, params = {}) { onLoad(page, params = {}) {
this.loading = true; this.loading = true;
getList(page.currentPage, page.pageSize, { ...params, ...this.query }) getList(page.currentPage, page.pageSize, omitInvalidListStatus({ ...params, ...this.query }))
.then(res => { .then(res => {
const data = res.data.data; const data = res.data.data;
this.page.total = data.total; this.page.total = data.total;
+2 -1
View File
@@ -104,6 +104,7 @@ import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util'; import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel'; import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit'; import { formatUpdateUserName } from '@/utils/audit';
import { omitInvalidListStatus } from '@/utils/search-range';
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
@@ -507,7 +508,7 @@ export default {
this.onLoad(this.page, this.query); this.onLoad(this.page, this.query);
}, },
buildQuery(params = {}) { buildQuery(params = {}) {
const values = { ...params, ...this.query }; const values = omitInvalidListStatus({ ...params, ...this.query });
const rangeMap = { const rangeMap = {
effectiveDateRange: ['effectiveDateStart', 'effectiveDateEnd'], effectiveDateRange: ['effectiveDateStart', 'effectiveDateEnd'],
expiryDateRange: ['expiryDateStart', 'expiryDateEnd'], expiryDateRange: ['expiryDateStart', 'expiryDateEnd'],
+2 -1
View File
@@ -63,6 +63,7 @@
<script> <script>
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/customer-type'; import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/customer-type';
import { omitInvalidListStatus } from '@/utils/search-range';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
export default { export default {
@@ -290,7 +291,7 @@ export default {
}, },
onLoad(page, params = {}) { onLoad(page, params = {}) {
this.loading = true; this.loading = true;
getList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(res => { getList(page.currentPage, page.pageSize, omitInvalidListStatus(Object.assign(params, this.query))).then(res => {
const data = res.data.data; const data = res.data.data;
this.page.total = data.total; this.page.total = data.total;
this.data = data.records; this.data = data.records;
+2 -1
View File
@@ -97,6 +97,7 @@ import { exportBlob } from '@/api/common';
import { getDictionaryAll } from '@/api/system/dictbiz'; import { getDictionaryAll } from '@/api/system/dictbiz';
import { getDeptTree } from '@/api/system/dept'; import { getDeptTree } from '@/api/system/dept';
import { downloadXls } from '@/utils/util'; import { downloadXls } from '@/utils/util';
import { omitInvalidListStatus } from '@/utils/search-range';
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import { Download } from '@element-plus/icons-vue'; import { Download } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
@@ -569,7 +570,7 @@ export default {
}, },
onLoad(page, params = {}) { onLoad(page, params = {}) {
this.loading = true; this.loading = true;
getList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(res => { getList(page.currentPage, page.pageSize, omitInvalidListStatus(Object.assign(params, this.query))).then(res => {
const data = res.data.data; const data = res.data.data;
this.page.total = data.total; this.page.total = data.total;
this.data = data.records; this.data = data.records;
+2 -1
View File
@@ -82,6 +82,7 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { changeStatus, getDetail, getList, remove, submit } from '@/api/base/measurement-unit'; import { changeStatus, getDetail, getList, remove, submit } from '@/api/base/measurement-unit';
import { createOption } from '@/option/base/measurement-unit'; import { createOption } from '@/option/base/measurement-unit';
import { omitInvalidListStatus } from '@/utils/search-range';
import { isAdministrator } from '@/utils/auth'; import { isAdministrator } from '@/utils/auth';
export default { export default {
@@ -218,7 +219,7 @@ export default {
}, },
onLoad(page = this.page, params = this.query) { onLoad(page = this.page, params = this.query) {
this.loading = true; this.loading = true;
return getList(page.currentPage, page.pageSize, params) return getList(page.currentPage, page.pageSize, omitInvalidListStatus(params))
.then(res => { .then(res => {
const pageData = res.data.data || {}; const pageData = res.data.data || {};
this.data = pageData.records || []; this.data = pageData.records || [];
+2 -1
View File
@@ -270,6 +270,7 @@ import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util'; import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel'; import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit'; import { formatUpdateUserName } from '@/utils/audit';
import { omitInvalidListStatus } from '@/utils/search-range';
import { getToken, isAdministrator } from '@/utils/auth'; import { getToken, isAdministrator } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate'; import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import AddressMapPicker from '@/components/address-map-picker/main.vue'; import AddressMapPicker from '@/components/address-map-picker/main.vue';
@@ -1636,7 +1637,7 @@ export default {
}, },
onLoad(page, params = {}) { onLoad(page, params = {}) {
this.loading = true; this.loading = true;
getList(page.currentPage, page.pageSize, { ...params, ...this.query }) getList(page.currentPage, page.pageSize, omitInvalidListStatus({ ...params, ...this.query }))
.then(res => { .then(res => {
const data = res.data.data; const data = res.data.data;
this.page.total = data.total; this.page.total = data.total;
+2 -1
View File
@@ -150,6 +150,7 @@ import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util'; import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel'; import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit'; import { formatUpdateUserName } from '@/utils/audit';
import { omitInvalidListStatus } from '@/utils/search-range';
import { getToken, isAdministrator } from '@/utils/auth'; import { getToken, isAdministrator } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate'; import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import AddressMapPicker from '@/components/address-map-picker/main.vue'; import AddressMapPicker from '@/components/address-map-picker/main.vue';
@@ -1157,7 +1158,7 @@ export default {
}, },
onLoad(page, params = {}) { onLoad(page, params = {}) {
this.loading = true; this.loading = true;
getList(page.currentPage, page.pageSize, { ...params, ...this.query }) getList(page.currentPage, page.pageSize, omitInvalidListStatus({ ...params, ...this.query }))
.then(res => { .then(res => {
const data = res.data.data; const data = res.data.data;
this.page.total = data.total; this.page.total = data.total;
+52 -7
View File
@@ -396,8 +396,11 @@ export default {
return this.selectionList.map(item => item.id).join(','); return this.selectionList.map(item => item.id).join(',');
}, },
}, },
created() { created() {
this.applyDefaultDeptSearch(); // 超管默认查全部组织,不预填组织筛选;普通用户预填当前组织。
if (!this.isAdmin) {
this.applyDefaultDeptSearch();
}
this.initDeptTree(); this.initDeptTree();
}, },
methods: { methods: {
@@ -455,10 +458,23 @@ export default {
normalizeListQuery(params = {}) { normalizeListQuery(params = {}) {
// 搜索条件优先于缓存的 query,避免默认组织 ID 覆盖用户新选择的组织。 // 搜索条件优先于缓存的 query,避免默认组织 ID 覆盖用户新选择的组织。
const query = { ...this.query, ...params }; const query = { ...this.query, ...params };
const rawDeptId = query.deptId ?? query.searchDeptId; // 本次搜索显式带了组织字段时,以本次为准(含清空);否则沿用缓存。
const hasDeptInParams =
Object.prototype.hasOwnProperty.call(params, 'deptId') ||
Object.prototype.hasOwnProperty.call(params, 'searchDeptId');
const rawDeptId = hasDeptInParams
? params.deptId ?? params.searchDeptId
: query.deptId ?? query.searchDeptId;
delete query.searchDeptId; delete query.searchDeptId;
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') { if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
query.deptId = String(rawDeptId).split(',')[0].trim(); // 级联可能回传路径数组,统一取末级并保持字符串,避免雪花 ID 精度丢失。
const deptValue = Array.isArray(rawDeptId) ? rawDeptId.at(-1) : rawDeptId;
query.deptId = String(deptValue ?? '')
.split(',')[0]
.trim();
if (!query.deptId) {
delete query.deptId;
}
} else { } else {
delete query.deptId; delete query.deptId;
} }
@@ -466,6 +482,23 @@ export default {
return query; return query;
}, },
applyDefaultDeptSearch() { applyDefaultDeptSearch() {
// 超管默认不限组织,避免预填组织后新增落在其他组织时列表看不见。
if (this.isAdmin) {
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
if (deptColumn) {
deptColumn.searchValue = undefined;
}
const nextQuery = { ...this.query };
delete nextQuery.deptId;
delete nextQuery.searchDeptId;
this.query = nextQuery;
this.$nextTick(() => {
if (this.$refs.crud?.searchForm) {
this.$refs.crud.searchForm.searchDeptId = undefined;
}
});
return;
}
const deptId = this.defaultDeptId(); const deptId = this.defaultDeptId();
const deptColumn = this.findColumn(this.option.column, 'searchDeptId'); const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
if (!deptId || !deptColumn) return; if (!deptId || !deptColumn) return;
@@ -477,6 +510,11 @@ export default {
} }
}); });
}, },
resolveDeptName(deptId) {
if (!deptId) return '';
const matched = this.deptOptions.find(item => String(item.value) === String(deptId));
return matched ? String(matched.label || '').trim() : '';
},
resetAddressQuery() { resetAddressQuery() {
this.addressQuery = { this.addressQuery = {
addressName: '', addressName: '',
@@ -933,7 +971,8 @@ export default {
} }
api.submit(submitRow).then( api.submit(submitRow).then(
() => { () => {
this.onLoad(this.page); this.page.currentPage = 1;
this.onLoad(this.page, this.query);
this.$message({ type: 'success', message: '操作成功!' }); this.$message({ type: 'success', message: '操作成功!' });
done(); done();
}, },
@@ -951,7 +990,7 @@ export default {
} }
api.submit(submitRow).then( api.submit(submitRow).then(
() => { () => {
this.onLoad(this.page); this.onLoad(this.page, this.query);
this.$message({ type: 'success', message: '操作成功!' }); this.$message({ type: 'success', message: '操作成功!' });
done(); done();
}, },
@@ -1006,7 +1045,13 @@ export default {
beforeOpen(done, type) { beforeOpen(done, type) {
this.dialogReadonly = type === 'view'; this.dialogReadonly = type === 'view';
if (type === 'add') { if (type === 'add') {
this.form = {}; const form = {};
// 超管若当前按某组织筛选,新增默认归属该组织,避免保存后被筛选条件滤掉。
if (this.isAdmin && this.query.deptId) {
form.deptId = String(this.query.deptId);
form.deptName = this.resolveDeptName(form.deptId);
}
this.form = form;
done(); done();
return; return;
} }
@@ -332,7 +332,7 @@
<script> <script>
import * as api from '@/api/business/contract-manage'; import * as api from '@/api/business/contract-manage';
import { submitMkApprovalFlow } from '@/utils/mk-approval'; import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import BillingPlanEditor from './components/billing-plan-editor.vue'; import BillingPlanEditor from './components/billing-plan-editor.vue';
import ContractAttachmentSection, { import ContractAttachmentSection, {
isContractSealMaterialLocation, isContractSealMaterialLocation,
@@ -738,7 +738,7 @@ export default {
removeCustomPeriodRow(index) { if (index <= 0) return; const periods = [...(this.settlementRule.customPeriods || [])]; periods.splice(index, 1); this.settlementRule.customPeriods = normalizeCustomPeriods(periods); }, removeCustomPeriodRow(index) { if (index <= 0) return; const periods = [...(this.settlementRule.customPeriods || [])]; periods.splice(index, 1); this.settlementRule.customPeriods = normalizeCustomPeriods(periods); },
validateCustomPeriods(periods = [], label) { const rows = normalizeCustomPeriods(periods); if (!rows.length) { this.$message.warning(`${label}:请至少配置一段自定义周期`); return false; } for (let index = 0; index < rows.length; index += 1) { const row = rows[index]; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间开始日`); return false; } if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间结束日`); return false; } if (endDay < startDay) { this.$message.warning(`${label}:第${index + 1}行结束日不能早于开始日`); return false; } if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) { this.$message.warning(`${label}:自定义多周期区间必须连续,不允许重叠或存在日期缺口`); return false; } } return true; }, validateCustomPeriods(periods = [], label) { const rows = normalizeCustomPeriods(periods); if (!rows.length) { this.$message.warning(`${label}:请至少配置一段自定义周期`); return false; } for (let index = 0; index < rows.length; index += 1) { const row = rows[index]; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间开始日`); return false; } if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间结束日`); return false; } if (endDay < startDay) { this.$message.warning(`${label}:第${index + 1}行结束日不能早于开始日`); return false; } if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) { this.$message.warning(`${label}:自定义多周期区间必须连续,不允许重叠或存在日期缺口`); return false; } } return true; },
validateSettlementRule(rule, label) { if (Number(rule.autoGenerate) !== 1) return true; if (!rule.billStartDate || !rule.settlementType) { this.$message.warning(`${label}:请完整填写账单起始日和结算类型`); return false; } if (rule.settlementType === '月结' && !rule.billCycleType) { this.$message.warning(`${label}:请选择结算周期`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '固定截单日' && !rule.billCutoffDay) { this.$message.warning(`${label}:请选择账单截单日`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') return this.validateCustomPeriods(rule.customPeriods, label); if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) { this.$message.warning(`${label}:请选择周期天数`); return false; } return true; }, validateSettlementRule(rule, label) { if (Number(rule.autoGenerate) !== 1) return true; if (!rule.billStartDate || !rule.settlementType) { this.$message.warning(`${label}:请完整填写账单起始日和结算类型`); return false; } if (rule.settlementType === '月结' && !rule.billCycleType) { this.$message.warning(`${label}:请选择结算周期`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '固定截单日' && !rule.billCutoffDay) { this.$message.warning(`${label}:请选择账单截单日`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') return this.validateCustomPeriods(rule.customPeriods, label); if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) { this.$message.warning(`${label}:请选择周期天数`); return false; } return true; },
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); await submitMkApprovalFlow({ bizType: 'contract-manage', bizId: this.form.id, subjectName: this.form.contractName || '', approvalStatus: this.form.approvalStatus || 'change_rejected' }); this.$message.success('变更已提交'); this.$router.back(); }, async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); await submitMkApprovalFlow({ bizType: 'contract-manage', bizId: this.form.id, subjectName: this.form.contractName || '', approvalStatus: this.form.approvalStatus || 'change_rejected', mkEnabled: await loadMkEnabled().catch(() => false) }); this.$message.success('变更已提交'); this.$router.back(); },
}, },
}; };
</script> </script>
+6 -2
View File
@@ -1986,7 +1986,9 @@ export default {
bizType: 'contract-manage', bizType: 'contract-manage',
bizId: id, bizId: id,
subjectName: saved.contractName || submit.contractName || '', subjectName: saved.contractName || submit.contractName || '',
approvalStatus: previousApprovalStatus || saved.approvalStatus || submit.approvalStatus || '', approvalStatus:
previousApprovalStatus || saved.approvalStatus || submit.approvalStatus || '',
mkEnabled: this.mkEnabled,
}) })
); );
} }
@@ -2025,6 +2027,7 @@ export default {
bizId: payload.id || this.form.id, bizId: payload.id || this.form.id,
subjectName: payload.contractName || this.form.contractName || '', subjectName: payload.contractName || this.form.contractName || '',
approvalStatus: this.form.approvalStatus || 'change_rejected', approvalStatus: this.form.approvalStatus || 'change_rejected',
mkEnabled: this.mkEnabled,
}).then(() => { }).then(() => {
this.$message.success('已重新提交变更审批'); this.$message.success('已重新提交变更审批');
this.closeForm(); this.closeForm();
@@ -3001,6 +3004,7 @@ export default {
bizId: row.id, bizId: row.id,
subjectName: row.contractName || '', subjectName: row.contractName || '',
approvalStatus: row.approvalStatus || '', approvalStatus: row.approvalStatus || '',
mkEnabled: this.mkEnabled,
}) })
) )
: request(); : request();
@@ -3338,7 +3342,7 @@ export default {
:deep(.el-form-item) { :deep(.el-form-item) {
align-items: center; align-items: center;
margin-bottom: 24px; //margin-bottom: 24px;
} }
:deep(.el-form-item__label) { :deep(.el-form-item__label) {
+29 -7
View File
@@ -79,7 +79,7 @@
<p>{{ timeRange(row) }}</p> <p>{{ timeRange(row) }}</p>
<p>{{ goodsSummary(row) }}</p> <p>{{ goodsSummary(row) }}</p>
<p>创建人:{{ row.createUserName || '-' }}</p> <p>创建人:{{ row.createUserName || '-' }}</p>
<p>创建时间:{{ row.createTime || '-' }}</p> <p>创建时间:{{ formatDate(row.createTime) }}</p>
</section> </section>
<section class="route-progress"> <section class="route-progress">
<div class="route-track"> <div class="route-track">
@@ -155,6 +155,7 @@
<script> <script>
import * as api from '@/api/business/master-order'; import * as api from '@/api/business/master-order';
import { splitRegionTokens } from '@/utils/region-address';
import TableHeaderTools from '@/components/table-header-tools/main.vue'; import TableHeaderTools from '@/components/table-header-tools/main.vue';
import MasterOrderEditor from './components/master-order-editor.vue'; import MasterOrderEditor from './components/master-order-editor.vue';
import MasterOrderDispatch from './components/master-order-dispatch.vue'; import MasterOrderDispatch from './components/master-order-dispatch.vue';
@@ -332,6 +333,10 @@ export default {
? `${String(row.planStartTime).slice(0, 10)} - ${String(row.planEndTime).slice(0, 10)}` ? `${String(row.planStartTime).slice(0, 10)} - ${String(row.planEndTime).slice(0, 10)}`
: '-'; : '-';
}, },
// 创建时间只展示日期。
formatDate(value) {
return value ? String(value).slice(0, 10) : '-';
},
goodsSummary(row) { goodsSummary(row) {
const item = (row.goods || [])[0] || {}; const item = (row.goods || [])[0] || {};
if (!item.cargoName && !item.cargoType) return '-'; if (!item.cargoName && !item.cargoType) return '-';
@@ -391,6 +396,26 @@ export default {
}`; }`;
return `${city} ${district}`; return `${city} ${district}`;
}, },
// 悬停气泡里的地址:起讫点若只保存了名称或详细地址的一半,气泡就会显示成
// “柳南区”“安徽”这类残缺地址,故把两个字段互为补充后展示。
completeAddress(name, address) {
const rawName = String(name || '').trim();
const rawAddress = String(address || '').trim();
if (!rawName) return rawAddress;
if (!rawAddress) return rawName;
// 详细地址已含省市,本身就是完整地址,不再拼接名称,避免重复卡片上已有的站点名。
if (/(?:省|自治区|特别行政区|市)/.test(rawAddress)) return rawAddress;
const normalizedName = rawName.replace(/\s+/g, '');
const normalizedAddress = rawAddress.replace(/\s+/g, '');
if (normalizedAddress.includes(normalizedName)) return rawAddress;
if (normalizedName.includes(normalizedAddress)) return rawName;
// “珠海1号港口”与“珠海港口1号”这类同字异序写法只保留一个。
if ([...normalizedName].sort().join('') === [...normalizedAddress].sort().join('')) {
return normalizedName.length >= normalizedAddress.length ? rawName : rawAddress;
}
// 区域在前、详细地址在后。
return splitRegionTokens(rawName) ? `${rawName}${rawAddress}` : `${rawAddress}${rawName}`;
},
routeNodeName(value, address, transportType, region = {}) { routeNodeName(value, address, transportType, region = {}) {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (!text) return '-'; if (!text) return '-';
@@ -457,15 +482,12 @@ export default {
if (this.isStationLikeName(rawAddress) && !this.isStationLikeName(rawName)) { if (this.isStationLikeName(rawAddress) && !this.isStationLikeName(rawName)) {
return { return {
name: rawAddress, name: rawAddress,
address: rawName || regionText || rawAddress, address: this.completeAddress(rawName, rawAddress) || regionText || rawAddress,
}; };
} }
const displayName = rawName || rawAddress || '-'; const displayName = rawName || rawAddress || '-';
const tipAddress = const tipAddress =
(rawAddress && rawAddress !== displayName ? rawAddress : '') || this.completeAddress(rawName, rawAddress) || regionText || rawAddress || displayName;
regionText ||
rawAddress ||
displayName;
return { name: displayName, address: tipAddress }; return { name: displayName, address: tipAddress };
} }
return { return {
@@ -475,7 +497,7 @@ export default {
siteCode, siteCode,
nextTransportType, nextTransportType,
}), }),
address: rawAddress || rawName || regionText || '-', address: this.completeAddress(rawName, rawAddress) || regionText || '-',
}; };
}, },
routeNodes(row = {}) { routeNodes(row = {}) {
+3 -1
View File
@@ -416,6 +416,7 @@ import { getList as getProjectList } from '@/api/business/project-apply';
import { getDeptTree } from '@/api/system/dept'; import { getDeptTree } from '@/api/system/dept';
import { exportBlob } from '@/api/common'; import { exportBlob } from '@/api/common';
import { config, option } from '@/option/business/process-config'; import { config, option } from '@/option/business/process-config';
import { applyIncomingSearch } from '@/utils/search-range';
import { getToken, isAdministrator } from '@/utils/auth'; import { getToken, isAdministrator } from '@/utils/auth';
import { downloadXls } from '@/utils/util'; import { downloadXls } from '@/utils/util';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
@@ -603,7 +604,8 @@ export default {
}); });
}, },
normalizeListQuery(params = {}) { normalizeListQuery(params = {}) {
const query = { ...params, ...this.query }; // 新条件覆盖旧条件,清空的筛选项不再沿用上一次的值。
const query = applyIncomingSearch(this.query, params);
const rawDeptId = query.deptId ?? query.searchDeptId; const rawDeptId = query.deptId ?? query.searchDeptId;
delete query.searchDeptId; delete query.searchDeptId;
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') { if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
+6 -2
View File
@@ -1750,6 +1750,7 @@ export default {
bizId: row.id, bizId: row.id,
subjectName: row.projectName || '', subjectName: row.projectName || '',
approvalStatus: row.approvalStatus || '', approvalStatus: row.approvalStatus || '',
mkEnabled: this.mkEnabled,
}).then(() => request()) }).then(() => request())
: request(); : request();
afterMk.then(res => { afterMk.then(res => {
@@ -1956,6 +1957,7 @@ export default {
bizId: id, bizId: id,
subjectName: name, subjectName: name,
approvalStatus: status, approvalStatus: status,
mkEnabled: this.mkEnabled,
}).then(() => { }).then(() => {
this.$message.success('提交成功!'); this.$message.success('提交成功!');
this.closeProjectForm(); this.closeProjectForm();
@@ -2027,6 +2029,7 @@ export default {
bizId: id, bizId: id,
subjectName: this.form.projectName || '', subjectName: this.form.projectName || '',
approvalStatus: this.form.approvalStatus || 'change_rejected', approvalStatus: this.form.approvalStatus || 'change_rejected',
mkEnabled: this.mkEnabled,
}).then(() => { }).then(() => {
this.$message.success('提交成功!'); this.$message.success('提交成功!');
this.closeProjectForm(); this.closeProjectForm();
@@ -2503,8 +2506,9 @@ export default {
const list = res.data.data || []; const list = res.data.data || [];
this.platformCompanyOptions = list.map(item => ({ this.platformCompanyOptions = list.map(item => ({
label: item.deptName || item.fullName || item.title || item.name, label: item.deptName || item.fullName || item.title || item.name,
value: item.id, // 雪花 ID 必须以字符串绑定,避免 JS Number 精度丢失导致多选提交异常
})); value: item.id == null || item.id === '' ? '' : String(item.id),
})).filter(item => item.value);
this.ensureCurrentPlatformOption(); this.ensureCurrentPlatformOption();
}); });
}, },
+2 -1
View File
@@ -140,7 +140,7 @@ import * as api from '@/api/payment/billPayment';
import * as billLedgerApi from '@/api/payment/billLedger'; import * as billLedgerApi from '@/api/payment/billLedger';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue'; import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { getMkPublicDetail } from '@/api/mk-process'; import { getMkPublicDetail } from '@/api/mk-process';
import { submitMkApprovalFlow } from '@/utils/mk-approval'; import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
const emptyForm = () => ({ const emptyForm = () => ({
id: null, id: null,
@@ -348,6 +348,7 @@ export default {
bizId: this.form.id, bizId: this.form.id,
subjectName: this.form.paymentNo || this.form.billNo || '', subjectName: this.form.paymentNo || this.form.billNo || '',
approvalStatus: previousApprovalStatus, approvalStatus: previousApprovalStatus,
mkEnabled: await loadMkEnabled().catch(() => false),
}); });
this.$message.success('提交成功'); this.$message.success('提交成功');
this.goBack(); this.goBack();
+1
View File
@@ -132,6 +132,7 @@ export default {
bizId: row.id, bizId: row.id,
subjectName: row.paymentNo || row.billNo || '', subjectName: row.paymentNo || row.billNo || '',
approvalStatus: previousApprovalStatus, approvalStatus: previousApprovalStatus,
mkEnabled: this.mkEnabled,
}); });
this.$message.success('提交成功'); this.$message.success('提交成功');
this.loadTable(); this.loadTable();
@@ -492,13 +492,14 @@ import { getDictionary } from '@/api/system/dictbiz';
import { invoiceApplicationDetailColumns } from '@/option/payment/invoiceApplication'; import { invoiceApplicationDetailColumns } from '@/option/payment/invoiceApplication';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue'; import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { getMkPublicDetail } from '@/api/mk-process'; import { getMkPublicDetail } from '@/api/mk-process';
import { submitMkApprovalFlow } from '@/utils/mk-approval'; import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
let localId = 0; let localId = 0;
const emptyLine = () => ({ const emptyLine = () => ({
localId: ++localId, localId: ++localId,
goodsCategory: '', goodsCategory: '',
goodsName: '', goodsName: '',
taxClassificationCode: '',
unit: '吨', unit: '吨',
quantity: 0, quantity: 0,
unitPriceNoTax: 0, unitPriceNoTax: 0,
@@ -742,9 +743,11 @@ export default {
const item = this.findInvoiceItem(line); const item = this.findInvoiceItem(line);
if (item) { if (item) {
line.taxRate = Number(item.defaultTaxRate || 0); line.taxRate = Number(item.defaultTaxRate || 0);
line.taxClassificationCode = item.taxClassificationCode || '';
} else if (clearInvalid) { } else if (clearInvalid) {
line.goodsName = ''; line.goodsName = '';
line.taxRate = 0; line.taxRate = 0;
line.taxClassificationCode = '';
} }
this.recalculateLine(line); this.recalculateLine(line);
}, },
@@ -752,6 +755,7 @@ export default {
const names = this.invoiceItemNames(line.goodsCategory); const names = this.invoiceItemNames(line.goodsCategory);
if (!names.some(item => item.shortName === line.goodsName)) line.goodsName = ''; if (!names.some(item => item.shortName === line.goodsName)) line.goodsName = '';
line.taxRate = 0; line.taxRate = 0;
line.taxClassificationCode = this.findInvoiceItem(line)?.taxClassificationCode || '';
if (names.length === 1) { if (names.length === 1) {
line.goodsName = names[0].shortName; line.goodsName = names[0].shortName;
this.handleGoodsNameChange(line); this.handleGoodsNameChange(line);
@@ -761,9 +765,11 @@ export default {
const item = this.findInvoiceItem(line); const item = this.findInvoiceItem(line);
if (!item) { if (!item) {
line.taxRate = 0; line.taxRate = 0;
line.taxClassificationCode = '';
return; return;
} }
line.taxRate = Number(item.defaultTaxRate || 0); line.taxRate = Number(item.defaultTaxRate || 0);
line.taxClassificationCode = item.taxClassificationCode || '';
this.recalculateLine(line); this.recalculateLine(line);
}, },
validateSettlements(rule, value, callback) { validateSettlements(rule, value, callback) {
@@ -1159,6 +1165,7 @@ export default {
lines: sheet.lines.map(line => ({ lines: sheet.lines.map(line => ({
goodsCategory: line.goodsCategory, goodsCategory: line.goodsCategory,
goodsName: line.goodsName, goodsName: line.goodsName,
taxClassificationCode: line.taxClassificationCode,
unit: line.unit, unit: line.unit,
quantity: line.quantity, quantity: line.quantity,
unitPriceNoTax: line.unitPriceNoTax, unitPriceNoTax: line.unitPriceNoTax,
@@ -1201,6 +1208,7 @@ export default {
bizId: this.form.id, bizId: this.form.id,
subjectName: this.form.applicationNo || '', subjectName: this.form.applicationNo || '',
approvalStatus: previousApprovalStatus, approvalStatus: previousApprovalStatus,
mkEnabled: await loadMkEnabled().catch(() => false),
}); });
this.$message.success('提交成功'); this.$message.success('提交成功');
this.goBack(); this.goBack();
+71 -6
View File
@@ -63,6 +63,7 @@
v-if="hasPermission('invoice_application_sync')" v-if="hasPermission('invoice_application_sync')"
type="primary" type="primary"
plain plain
:loading="batchSyncing"
@click="handleBatchSync" @click="handleBatchSync"
>批量同步</el-button >批量同步</el-button
> >
@@ -124,6 +125,12 @@
{{ displayValue(row[column.prop]) }} {{ displayValue(row[column.prop]) }}
</el-tag> </el-tag>
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span> <span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
<template v-else-if="column.prop === 'kingdeeLockStatus'">
<el-tooltip v-if="lockTag(row).show" :content="lockTag(row).tip" placement="top">
<el-tag :type="lockTag(row).type" class="status-text">{{ lockTag(row).label }}</el-tag>
</el-tooltip>
<span v-else>-</span>
</template>
<span v-else>{{ displayValue(row[column.prop]) }}</span> <span v-else>{{ displayValue(row[column.prop]) }}</span>
</template> </template>
</el-table-column> </el-table-column>
@@ -192,7 +199,9 @@
> >
<el-link <el-link
v-if=" v-if="
hasPermission('invoice_application_void') && row.approvalStatus === 'approved' hasPermission('invoice_application_void') &&
row.approvalStatus === 'approved' &&
row.kingdeeLockStatus !== 'locked'
" "
type="danger" type="danger"
@click="handleVoid(row)" @click="handleVoid(row)"
@@ -261,6 +270,7 @@ export default {
rows: [], rows: [],
selection: [], selection: [],
loading: false, loading: false,
batchSyncing: false,
page: { current: 1, size: 10, total: 0 }, page: { current: 1, size: 10, total: 0 },
mkEnabled: false, mkEnabled: false,
}; };
@@ -374,6 +384,7 @@ export default {
bizId: row.id, bizId: row.id,
subjectName: row.applicationNo || '', subjectName: row.applicationNo || '',
approvalStatus: previousApprovalStatus, approvalStatus: previousApprovalStatus,
mkEnabled: this.mkEnabled,
}); });
this.$message.success('提交成功'); this.$message.success('提交成功');
this.loadTable(); this.loadTable();
@@ -398,8 +409,13 @@ export default {
}, },
async handleApprove(row) { async handleApprove(row) {
await this.$confirm('确认审批通过该开票申请?', '提示', { type: 'warning' }); await this.$confirm('确认审批通过该开票申请?', '提示', { type: 'warning' });
await api.approve({ id: row.id }); const res = await api.approve({ id: row.id });
this.$message.success('审批通过'); // data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
if (res.data.data === false) {
this.$message.warning(res.data.msg);
} else {
this.$message.success(res.data.msg || '审批通过');
}
this.loadTable(); this.loadTable();
}, },
async handleReturn(row) { async handleReturn(row) {
@@ -415,14 +431,31 @@ export default {
this.loadTable(); this.loadTable();
}, },
async handleBatchSync() { async handleBatchSync() {
if (this.batchSyncing) return;
const rows = this.selection.filter(row => row.approvalStatus === 'approved'); const rows = this.selection.filter(row => row.approvalStatus === 'approved');
if (!rows.length) { if (!rows.length) {
this.$message.warning('请选择至少一条审批通过的开票申请'); this.$message.warning('请选择至少一条审批通过的开票申请');
return; return;
} }
await Promise.all(rows.map(row => api.syncKingdee(row.id))); this.batchSyncing = true;
this.$message.success(`已同步${rows.length}条开票申请`); try {
this.loadTable(); 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() { async handleExport() {
const data = this.unwrapData(await api.getList(1, 100000, this.query)); const data = this.unwrapData(await api.getList(1, 100000, this.query));
@@ -452,6 +485,38 @@ export default {
'' ''
); );
}, },
lockTag(row) {
if (row.kingdeeLockStatus === 'locked') {
return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' };
}
if (row.kingdeeLockStatus === 'failed') {
if ((row.kingdeeTryCount || 0) >= 5) {
return {
show: true,
type: 'danger',
label: '锁定失败·已达上限',
tip: row.kingdeeLockFailReason || '自动补偿次数已用完,请重推「同步金蝶」立即补锁',
};
}
return {
show: true,
type: 'warning',
label: '锁定失败·补偿中',
tip: row.kingdeeLockFailReason || '系统将自动补偿重锁',
};
}
// 金蝶解锁终态标识:删单解锁的源单已作废,此标签用于区分人工作废与金蝶删单
if (row.kingdeeUnlockTime) {
const isDelete = row.kingdeeUnlockOperationName === '金蝶删单';
return {
show: true,
type: isDelete ? 'danger' : 'warning',
label: row.kingdeeUnlockOperationName || '金蝶解锁',
tip: `${row.kingdeeUnlockOperator || '-'} 于 ${this.$dayjs(row.kingdeeUnlockTime).format('YYYY-MM-DD HH:mm:ss')} 解锁`,
};
}
return { show: false };
},
displayValue(value) { displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value; return value === null || value === undefined || value === '' ? '-' : value;
}, },
+34 -241
View File
@@ -47,7 +47,7 @@
<el-col :span="6"> <el-col :span="6">
<el-form-item label="开票金额"> <el-form-item label="开票金额">
<el-input <el-input
:model-value="form.kingdeeInvoicePoolId ? formatMoney(form.invoiceAmount) : ''" :model-value="form.invoiceNo ? formatMoney(form.invoiceAmount) : ''"
disabled disabled
placeholder="从金蝶票据池带出" placeholder="从金蝶票据池带出"
/> />
@@ -56,7 +56,7 @@
<el-col :span="6"> <el-col :span="6">
<el-form-item label="税额"> <el-form-item label="税额">
<el-input <el-input
:model-value="form.kingdeeInvoicePoolId ? formatMoney(form.taxAmount) : ''" :model-value="form.invoiceNo ? formatMoney(form.taxAmount) : ''"
disabled disabled
placeholder="从金蝶票据池带出" placeholder="从金蝶票据池带出"
/> />
@@ -118,7 +118,13 @@
</section-card> </section-card>
<section-card title="附件信息"> <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>
<section-card title="备注"> <section-card title="备注">
@@ -149,62 +155,13 @@
</div> </div>
</el-form> </el-form>
<el-dialog v-model="invoiceDialog.visible" title="查询金蝶票据池" width="86%" append-to-body> <kingdee-invoice-pool-dialog
<div class="receipt-form-page__dialog-search"> v-model="invoiceDialog.visible"
<el-input title="查询金蝶票据池"
v-model="invoiceDialog.keyword" :initial-invoice-no="form.invoiceNo"
clearable :query-api="apiQueryInvoicePool"
placeholder="发票号码、开票单位或受票单位" @confirm="handleInvoicePicked"
@keyup.enter="loadInvoicePool" />
/>
<el-button type="primary" @click="loadInvoicePool">查询</el-button>
</div>
<el-table
v-loading="invoiceDialog.loading"
:data="invoiceDialog.rows"
border
empty-text="暂无票据数据"
highlight-current-row
@current-change="invoiceDialog.current = $event"
@row-dblclick="confirmInvoice"
>
<el-table-column prop="invoiceNo" label="发票号码" min-width="180" />
<el-table-column prop="invoiceDate" label="开票日期" min-width="120" />
<el-table-column prop="invoiceType" label="发票类型" min-width="130" />
<el-table-column prop="issuerName" label="开票单位" min-width="170" />
<el-table-column prop="receiverName" label="受票单位" min-width="170" />
<el-table-column label="开票金额" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
</el-table-column>
<el-table-column label="税率" min-width="100" align="center">
<template #default="{ row }">{{ formatRate(row.taxRate) }}</template>
</el-table-column>
<el-table-column label="税额" min-width="120" align="right">
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
</el-table-column>
<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="150" />
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click.stop="confirmInvoice(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<div class="receipt-form-page__dialog-pagination">
<el-pagination
v-model:current-page="invoiceDialog.page.current"
v-model:page-size="invoiceDialog.page.size"
:total="invoiceDialog.page.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="refreshInvoicePoolPage"
@size-change="handleInvoicePoolSizeChange"
/>
</div>
<template #footer>
<el-button @click="invoiceDialog.visible = false">取消</el-button>
<el-button type="primary" @click="confirmInvoice()">确定</el-button>
</template>
</el-dialog>
<el-dialog <el-dialog
v-model="settlementDialog.visible" v-model="settlementDialog.visible"
@@ -259,82 +216,13 @@
import { Search } from '@element-plus/icons-vue'; import { Search } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import * as api from '@/api/payment/invoiceReceipt'; 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 VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { getMkPublicDetail } from '@/api/mk-process'; import { getMkPublicDetail } from '@/api/mk-process';
import { submitMkApprovalFlow } from '@/utils/mk-approval'; import { loadMkEnabled, 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 = () => ({ const emptyForm = () => ({
id: null, id: null,
kingdeeInvoicePoolId: null,
invoiceNo: '', invoiceNo: '',
invoiceDate: '', invoiceDate: '',
invoiceType: '', invoiceType: '',
@@ -346,8 +234,6 @@ const emptyForm = () => ({
bankName: '', bankName: '',
bankAccount: '', bankAccount: '',
issuingBank: '', issuingBank: '',
kingdeeBillNo: '',
kingdeeStatus: 'unsynced',
phone: '', phone: '',
customerEmails: [], customerEmails: [],
departmentEmails: [], departmentEmails: [],
@@ -363,24 +249,17 @@ const emptyForm = () => ({
export default { export default {
name: 'InvoiceReceiptForm', name: 'InvoiceReceiptForm',
components: { components: {
KingdeeInvoicePoolDialog,
VehicleAttachmentTable, VehicleAttachmentTable,
}, },
data() { data() {
return { return {
Search, Search,
form: emptyForm(), form: emptyForm(),
invoicePoolMockRows: [],
invoicePoolMockLoaded: false,
customerEmailOptions: [], customerEmailOptions: [],
departmentEmailOptions: [], departmentEmailOptions: [],
invoiceDialog: { invoiceDialog: {
visible: false, visible: false,
loading: false,
keyword: '',
sourceRows: [],
rows: [],
current: null,
page: { current: 1, size: 10, total: 0 },
}, },
settlementDialog: { settlementDialog: {
visible: false, visible: false,
@@ -426,6 +305,12 @@ export default {
this.initialize(); this.initialize();
}, },
methods: { methods: {
apiQueryInvoicePool(params) {
return api.queryInvoicePool(params);
},
handleInvoicePicked(rows) {
this.confirmInvoice(rows[0]);
},
hasPermission(code) { hasPermission(code) {
return this.permission?.[code] !== false; return this.permission?.[code] !== false;
}, },
@@ -483,7 +368,7 @@ export default {
}; };
}, },
validateInvoice(rule, value, callback) { validateInvoice(rule, value, callback) {
if (!value || !this.form.kingdeeInvoicePoolId) { if (!value) {
callback(new Error('请选择金蝶票据池发票')); callback(new Error('请选择金蝶票据池发票'));
return; return;
} }
@@ -509,99 +394,17 @@ export default {
} }
callback(); callback();
}, },
async openInvoiceDialog() { openInvoiceDialog() {
this.invoiceDialog.visible = true; 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 = {}) { confirmInvoice(row, options = {}) {
const selected = row?.id ? row : this.invoiceDialog.current; const selected = row;
if (!selected) { if (!selected) {
this.$message.warning('请选择一张金蝶票据池发票'); this.$message.warning('请选择一张金蝶票据池发票');
return; return;
} }
const preserveUserInput = options.preserveUserInput === true; const preserveUserInput = options.preserveUserInput === true;
Object.assign(this.form, { Object.assign(this.form, {
kingdeeInvoicePoolId: selected.id,
invoiceNo: selected.invoiceNo, invoiceNo: selected.invoiceNo,
invoiceDate: selected.invoiceDate, invoiceDate: selected.invoiceDate,
invoiceType: selected.invoiceType, invoiceType: selected.invoiceType,
@@ -613,8 +416,6 @@ export default {
bankName: selected.bankName, bankName: selected.bankName,
bankAccount: selected.bankAccount, bankAccount: selected.bankAccount,
issuingBank: selected.issuingBank, issuingBank: selected.issuingBank,
kingdeeBillNo: selected.kingdeeBillNo || '',
kingdeeStatus: selected.kingdeeStatus || 'unsynced',
phone: preserveUserInput ? this.form.phone : selected.phone || '', phone: preserveUserInput ? this.form.phone : selected.phone || '',
customerEmails: preserveUserInput customerEmails: preserveUserInput
? this.form.customerEmails ? this.form.customerEmails
@@ -725,7 +526,6 @@ export default {
payload() { payload() {
return { return {
id: this.form.id, id: this.form.id,
kingdeeInvoicePoolId: this.form.kingdeeInvoicePoolId,
invoiceNo: this.form.invoiceNo, invoiceNo: this.form.invoiceNo,
invoiceDate: this.form.invoiceDate, invoiceDate: this.form.invoiceDate,
invoiceType: this.form.invoiceType, invoiceType: this.form.invoiceType,
@@ -737,8 +537,6 @@ export default {
bankName: this.form.bankName, bankName: this.form.bankName,
bankAccount: this.form.bankAccount, bankAccount: this.form.bankAccount,
issuingBank: this.form.issuingBank, issuingBank: this.form.issuingBank,
kingdeeBillNo: this.form.kingdeeBillNo,
kingdeeStatus: this.form.kingdeeStatus,
phone: this.form.phone, phone: this.form.phone,
customerEmails: this.form.customerEmails.join(';'), customerEmails: this.form.customerEmails.join(';'),
departmentEmails: this.form.departmentEmails.join(';'), departmentEmails: this.form.departmentEmails.join(';'),
@@ -773,6 +571,7 @@ export default {
bizId: this.form.id, bizId: this.form.id,
subjectName: this.form.invoiceNo || '', subjectName: this.form.invoiceNo || '',
approvalStatus: previousApprovalStatus, approvalStatus: previousApprovalStatus,
mkEnabled: await loadMkEnabled().catch(() => false),
}); });
this.$message.success('提交成功'); this.$message.success('提交成功');
this.goBack(); this.goBack();
@@ -782,13 +581,12 @@ export default {
this.$message.warning('请先选择金蝶票据池发票'); this.$message.warning('请先选择金蝶票据池发票');
return; return;
} }
const data = this.recordId const data = this.unwrapData(
? this.unwrapData(await api.getInvoicePool(this.form.invoiceNo)) await api.queryInvoicePool({ current: 1, size: 1, invoiceNo: this.form.invoiceNo })
: this.filterInvoicePoolMockRows(this.form.invoiceNo); );
const rows = Array.isArray(data) ? data : data?.records || []; const invoice = (data.records || []).find(item => item.invoiceNo === this.form.invoiceNo);
const invoice = rows.find(item => item.invoiceNo === this.form.invoiceNo);
if (!invoice) { if (!invoice) {
this.$message.warning('金蝶票据池中未查询到该发票'); this.$message.warning('金蝶票池未查询到该发票');
return; return;
} }
if (!this.confirmInvoice(invoice, { preserveUserInput: true })) return; if (!this.confirmInvoice(invoice, { preserveUserInput: true })) return;
@@ -851,11 +649,6 @@ export default {
.receipt-form-page__dialog-search .el-input { .receipt-form-page__dialog-search .el-input {
width: 360px; width: 360px;
} }
.receipt-form-page__dialog-pagination {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
.receipt-form-page :deep(.el-table) { .receipt-form-page :deep(.el-table) {
--el-table-border-color: #eff1f7; --el-table-border-color: #eff1f7;
} }
+37 -14
View File
@@ -40,10 +40,10 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item v-if="searchExpanded" label="金蝶单据状态"> <el-form-item v-if="searchExpanded" label="金蝶状态">
<el-select v-model="query.kingdeeStatus" clearable placeholder="请选择"> <el-select v-model="query.kingdeePushStatus" clearable placeholder="请选择">
<el-option <el-option
v-for="item in kingdeeStatusOptions" v-for="item in kingdeePushStatusOptions"
:key="item.value" :key="item.value"
:label="item.label" :label="item.label"
:value="item.value" :value="item.value"
@@ -72,7 +72,8 @@
v-if="hasPermission('invoice_receipt_sync')" v-if="hasPermission('invoice_receipt_sync')"
type="primary" type="primary"
plain plain
@click="handleBatchSync" :loading="pushLoading"
@click="handlePushKingdee"
>批量同步</el-button >批量同步</el-button
> >
<el-button v-if="hasPermission('invoice_receipt_add')" type="primary" @click="openCreate" <el-button v-if="hasPermission('invoice_receipt_add')" type="primary" @click="openCreate"
@@ -229,7 +230,7 @@ const emptyQuery = () => ({
deptName: '', deptName: '',
deptPath: [], deptPath: [],
approvalStatus: '', approvalStatus: '',
kingdeeStatus: '', kingdeePushStatus: '',
}); });
export default { export default {
@@ -248,12 +249,13 @@ export default {
searchVisible: true, searchVisible: true,
tableSize: 'default', tableSize: 'default',
approvalStatusOptions: api.approvalStatusOptions, approvalStatusOptions: api.approvalStatusOptions,
kingdeeStatusOptions: api.kingdeeStatusOptions, kingdeePushStatusOptions: api.kingdeePushStatusOptions,
columns: invoiceReceiptTableColumns, columns: invoiceReceiptTableColumns,
visibleColumnKeys: invoiceReceiptTableColumns.map(column => column.prop), visibleColumnKeys: invoiceReceiptTableColumns.map(column => column.prop),
rows: [], rows: [],
selection: [], selection: [],
loading: false, loading: false,
pushLoading: false,
page: { current: 1, size: 10, total: 0 }, page: { current: 1, size: 10, total: 0 },
mkEnabled: false, mkEnabled: false,
}; };
@@ -339,6 +341,7 @@ export default {
bizId: row.id, bizId: row.id,
subjectName: row.invoiceNo || '', subjectName: row.invoiceNo || '',
approvalStatus: previousApprovalStatus, approvalStatus: previousApprovalStatus,
mkEnabled: this.mkEnabled,
}); });
this.$message.success('提交成功'); this.$message.success('提交成功');
this.loadTable(); this.loadTable();
@@ -363,8 +366,13 @@ export default {
}, },
async handleApprove(row) { async handleApprove(row) {
await this.$confirm('确认审批通过该收票登记?', '提示', { type: 'warning' }); await this.$confirm('确认审批通过该收票登记?', '提示', { type: 'warning' });
await api.approve({ id: row.id }); const res = await api.approve({ id: row.id });
this.$message.success('审批通过'); // data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
if (res.data.data === false) {
this.$message.warning(res.data.msg);
} else {
this.$message.success(res.data.msg || '审批通过');
}
this.loadTable(); this.loadTable();
}, },
async handleReturn(row) { async handleReturn(row) {
@@ -379,15 +387,29 @@ export default {
this.$message.success('作废成功'); this.$message.success('作废成功');
this.loadTable(); this.loadTable();
}, },
async handleBatchSync() { async handlePushKingdee() {
if (this.pushLoading) return;
const rows = this.selection.filter(row => row.approvalStatus === 'approved'); const rows = this.selection.filter(row => row.approvalStatus === 'approved');
if (!rows.length) { if (!rows.length) {
this.$message.warning('请选择至少一条审批通过的收票登记'); this.$message.warning('请选择至少一条审批通过的收票登记');
return; return;
} }
await Promise.all(rows.map(row => api.syncKingdee(row.id))); await this.$confirm(`确认将选中的 ${rows.length} 条收票登记同步至金蝶?`, '提示', {
this.$message.success(`已同步${rows.length}条收票登记`); type: 'warning',
this.loadTable(); });
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() { async handleExport() {
const data = this.unwrapData(await api.getList(1, 100000, this.query)); const data = this.unwrapData(await api.getList(1, 100000, this.query));
@@ -404,8 +426,9 @@ export default {
当前处理人: item.currentProcessor, 当前处理人: item.currentProcessor,
创建人: item.createUserName, 创建人: item.createUserName,
创建时间: item.createTime, 创建时间: item.createTime,
金蝶单据号: item.kingdeeBillNo, 金蝶单据号: item.kingdeeReceivedInvoiceNo,
金蝶单据状态: item.kingdeeStatusName, 金蝶状态: item.kingdeePushStatusName,
失败原因: item.kingdeePushMsg,
})); }));
const workbook = XLSX.utils.book_new(); const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(exportRows), '收票登记'); XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(exportRows), '收票登记');
+130 -46
View File
@@ -36,25 +36,20 @@
<el-col v-if="isSettlementRelatedType" :span="6" <el-col v-if="isSettlementRelatedType" :span="6"
><el-form-item label="结算单号" prop="referenceId"> ><el-form-item label="结算单号" prop="referenceId">
<div class="payment-form-page__settlement-nos"> <div class="payment-form-page__settlement-nos">
<el-select <el-tooltip
ref="settlementNoSelectRef" :content="referenceLabel"
:model-value="referenceSettlementNos" placement="top"
multiple :disabled="!referenceLabel || referenceSettlementCount < 2"
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="1"
:disabled="readonly"
placeholder="请输入"
class="payment-form-page__settlement-select"
@visible-change="handleSettlementSelectVisible"
> >
<el-option <el-input
v-for="item in referenceSettlementLinks" :model-value="referenceDisplayText"
:key="`${item.id || item.settlementNo}`" readonly
:label="item.settlementNo" :disabled="readonly"
:value="item.settlementNo" placeholder="请输入"
class="payment-form-page__settlement-input"
@click="!readonly && openReference()"
/> />
</el-select> </el-tooltip>
<el-button <el-button
v-if="!readonly" v-if="!readonly"
class="payment-form-page__settlement-select-btn" class="payment-form-page__settlement-select-btn"
@@ -283,8 +278,18 @@
<el-table-column label="操作" width="220" fixed="right" align="center"> <el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row, $index }"> <template #default="{ row, $index }">
<div class="payment-form-page__invoice-actions"> <div class="payment-form-page__invoice-actions">
<el-link type="primary" @click="viewInvoiceAttachment(row)">查看</el-link> <el-link
<el-link type="primary" @click="downloadInvoiceAttachment(row)">下载</el-link> type="primary"
:disabled="kingdeeAttachmentBusy"
@click="viewInvoiceAttachment(row)"
>查看</el-link
>
<el-link
type="primary"
:disabled="kingdeeAttachmentBusy"
@click="downloadInvoiceAttachment(row)"
>下载</el-link
>
<el-link <el-link
v-if="!readonly" v-if="!readonly"
type="danger" type="danger"
@@ -668,7 +673,12 @@ import {
import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer'; import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer';
import { formatContractOptionLabel } from '@/utils/contract'; import { formatContractOptionLabel } from '@/utils/contract';
import { downloadFileByUrl } from '@/utils/util'; import { downloadFileByUrl } from '@/utils/util';
import { submitMkApprovalFlow } from '@/utils/mk-approval'; import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import {
downloadKingdeeInvoiceFile,
isKingdeePathAttachment,
viewKingdeeInvoiceFile,
} from '@/utils/kingdee-attachment';
import PdfPreview from '@/components/pdf-preview/main.vue'; import PdfPreview from '@/components/pdf-preview/main.vue';
const attachmentViewerPlugins = [ const attachmentViewerPlugins = [
@@ -678,6 +688,9 @@ const attachmentViewerPlugins = [
fallbackPlugin(), fallbackPlugin(),
]; ];
const paymentApplicationListRoute = '/payment/payment-application';
const paymentApplicationListRefreshKey = 'payment-application-list-refresh';
const ATTACHMENT_MATERIALS = { const ATTACHMENT_MATERIALS = {
weighingSlip: { key: 'weighingSlip', label: '磅单', keywords: ['磅单'] }, weighingSlip: { key: 'weighingSlip', label: '磅单', keywords: ['磅单'] },
entrustOrder: { key: 'entrustOrder', label: '委托单', keywords: ['委托单'] }, entrustOrder: { key: 'entrustOrder', label: '委托单', keywords: ['委托单'] },
@@ -749,6 +762,8 @@ export default {
form: emptyForm(), form: emptyForm(),
amountSyncing: false, amountSyncing: false,
transferPayload: null, transferPayload: null,
// 金蝶侧发票附件查看/下载进行中(行组级 loading 守卫)
kingdeeAttachmentBusy: false,
referenceRows: [], referenceRows: [],
paymentRecords: [], paymentRecords: [],
referenceVisible: false, referenceVisible: false,
@@ -935,6 +950,14 @@ export default {
referenceSettlementNos() { referenceSettlementNos() {
return this.referenceSettlementLinks.map(item => item.settlementNo).filter(Boolean); return this.referenceSettlementLinks.map(item => item.settlementNo).filter(Boolean);
}, },
referenceSettlementCount() {
return this.referenceSettlementNos.length;
},
// 结算单号展示:单张显示单号;多张仅提示数量,明细在下方「结算信息」表,悬停可看全部单号
referenceDisplayText() {
if (this.referenceSettlementCount > 1) return `已选 ${this.referenceSettlementCount} 张结算单`;
return this.referenceLabel;
},
settlementRows() { settlementRows() {
if (this.referenceRows.length) { if (this.referenceRows.length) {
return this.referenceRows.map(item => ({ return this.referenceRows.map(item => ({
@@ -1210,7 +1233,7 @@ export default {
]; ];
} }
const referenceRow = this.createReferenceRow(data); const referenceRow = this.createReferenceRow(data);
this.referenceRows = referenceRow.settlementNo ? [referenceRow] : []; this.referenceRows = this.restoreReferenceRows(data, referenceRow);
this.paymentRecords = data.paymentRecords || []; this.paymentRecords = data.paymentRecords || [];
}, },
async initialize() { async initialize() {
@@ -1233,7 +1256,7 @@ export default {
invoices: data.invoices || [], invoices: data.invoices || [],
}; };
const referenceRow = this.createReferenceRow(data); const referenceRow = this.createReferenceRow(data);
this.referenceRows = referenceRow.settlementNo ? [referenceRow] : []; this.referenceRows = this.restoreReferenceRows(data, referenceRow);
this.paymentRecords = data.paymentRecords || []; this.paymentRecords = data.paymentRecords || [];
if (this.form.paymentType === 'project_advance') { if (this.form.paymentType === 'project_advance') {
await this.loadProjectOptions(); await this.loadProjectOptions();
@@ -1451,6 +1474,33 @@ export default {
this.$refs.formRef?.clearValidate(); this.$refs.formRef?.clearValidate();
}); });
}, },
restoreReferenceRows(data, fallbackRow) {
const relations = Array.isArray(data?.settlements) ? data.settlements : [];
const rows = relations
.map(item => {
const isPre = item.preSettlementId !== undefined && item.preSettlementId !== null && item.preSettlementId !== '';
const settlementNo = item.preSettlementNo || item.formalSettlementNo || '';
if (!settlementNo) return null;
return {
id: isPre ? item.preSettlementId : item.formalSettlementId,
settlementNo,
referenceType: isPre ? 'pre' : 'formal',
settlementAmount: Number(Number(item.settlementAmount || 0).toFixed(2)),
payableAmount: Number(Number(item.payableAmount ?? item.settlementAmount ?? 0).toFixed(2)),
appliedAmount: Number(Number(item.appliedAmount || 0).toFixed(2)),
fixedAppliedAmount: this.readonly,
};
})
.filter(Boolean);
if (rows.length > 1) {
this.form.preSettlementIds = rows.filter(item => item.referenceType === 'pre').map(item => item.id);
this.form.settlementIds = rows.filter(item => item.referenceType === 'formal').map(item => item.id);
if (this.form.preSettlementIds.length) this.form.preSettlementId = this.form.preSettlementIds[0];
if (this.form.settlementIds.length) this.form.settlementId = this.form.settlementIds[0];
return rows;
}
return fallbackRow?.settlementNo ? [fallbackRow] : [];
},
createReferenceRow(row) { createReferenceRow(row) {
const settlementAmount = Number(row.settlementAmount || 0); const settlementAmount = Number(row.settlementAmount || 0);
const appliedAmount = Number(row.advanceAppliedAmount || row.appliedPaymentAmount || 0); const appliedAmount = Number(row.advanceAppliedAmount || row.appliedPaymentAmount || 0);
@@ -1459,11 +1509,13 @@ export default {
id: row.preSettlementId || row.settlementId || row.id, id: row.preSettlementId || row.settlementId || row.id,
settlementNo: row.preSettlementNo || row.formalSettlementNo || row.settlementNo || '', settlementNo: row.preSettlementNo || row.formalSettlementNo || row.settlementNo || '',
referenceType: isPre ? 'pre' : 'formal', referenceType: isPre ? 'pre' : 'formal',
settlementAmount, settlementAmount: Number(Number(settlementAmount || 0).toFixed(2)),
payableAmount: payableAmount: Number(
row.payableAmount === undefined || row.payableAmount === null (row.payableAmount === undefined || row.payableAmount === null
? Math.max(0, settlementAmount - appliedAmount) ? Math.max(0, settlementAmount - appliedAmount)
: Number(row.payableAmount || 0), : Number(row.payableAmount || 0)
).toFixed(2)
),
}; };
}, },
createInvoiceRows(invoices = [], settlementNo = '') { createInvoiceRows(invoices = [], settlementNo = '') {
@@ -1521,9 +1573,24 @@ export default {
} }
const first = rows[0]; const first = rows[0];
this.form.invoices = []; this.form.invoices = [];
this.referenceRows = rows.map(item => const paymentType =
this.createReferenceRow({ ...item, referenceType: 'pre' }) this.form.paymentType === 'settlement_payment' ? 'settlement_payment' : 'progress_advance';
const amountResponses = await Promise.all(
rows.map(item => {
const referenceId = item.preSettlementId || item.id;
if (!this.isValidReferenceId(referenceId)) return Promise.resolve(null);
return api.getReferenceAmount(paymentType, referenceId, this.form.id).catch(() => null);
})
); );
this.referenceRows = rows.map((item, index) => {
const amount = this.unwrapData(amountResponses[index]) || {};
return this.createReferenceRow({
...item,
referenceType: 'pre',
settlementAmount: amount.settlementAmount ?? item.settlementAmount,
payableAmount: amount.payableAmount,
});
});
const settlementAmount = this.referenceRows.reduce( const settlementAmount = this.referenceRows.reduce(
(total, item) => total + Number(item.settlementAmount || 0), (total, item) => total + Number(item.settlementAmount || 0),
0 0
@@ -1534,8 +1601,6 @@ export default {
); );
const paymentRatio = Number(this.form.paymentRatio || 0); const paymentRatio = Number(this.form.paymentRatio || 0);
// 尾款付款场景下选择预结算单时保留尾款付款类型,不强制改为进度预付 // 尾款付款场景下选择预结算单时保留尾款付款类型,不强制改为进度预付
const paymentType =
this.form.paymentType === 'settlement_payment' ? 'settlement_payment' : 'progress_advance';
this.amountSyncing = true; this.amountSyncing = true;
Object.assign(this.form, { Object.assign(this.form, {
paymentType, paymentType,
@@ -1598,8 +1663,19 @@ export default {
this.attachmentFileName(this.invoiceAttachment(row)) || `${row.invoiceNo || '发票'}.pdf` this.attachmentFileName(this.invoiceAttachment(row)) || `${row.invoiceNo || '发票'}.pdf`
); );
}, },
viewInvoiceAttachment(row) { async viewInvoiceAttachment(row) {
const attachment = this.invoiceAttachment(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)) { if (!this.attachmentFileUrl(attachment)) {
this.$message.warning('当前发票暂无可查看附件'); this.$message.warning('当前发票暂无可查看附件');
return; return;
@@ -1609,8 +1685,20 @@ export default {
[attachment] [attachment]
); );
}, },
downloadInvoiceAttachment(row) { async downloadInvoiceAttachment(row) {
const url = this.invoiceAttachmentUrl(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) { if (!url) {
this.$message.warning('当前发票暂无可下载附件'); this.$message.warning('当前发票暂无可下载附件');
return; return;
@@ -2247,13 +2335,6 @@ export default {
} }
return Promise.all(loaders); return Promise.all(loaders);
}, },
handleSettlementSelectVisible(visible) {
// 结算单号仅做展示,通过「选择」按钮维护,避免下拉改值
if (!visible) return;
this.$nextTick(() => {
this.$refs.settlementNoSelectRef?.blur?.();
});
},
handleReferenceSearch() { handleReferenceSearch() {
this.formalPage.current = 1; this.formalPage.current = 1;
this.prePage.current = 1; this.prePage.current = 1;
@@ -2673,14 +2754,17 @@ export default {
bizId: this.form.id, bizId: this.form.id,
subjectName: this.form.paymentNo || '', subjectName: this.form.paymentNo || '',
approvalStatus: previousApprovalStatus, approvalStatus: previousApprovalStatus,
mkEnabled: await loadMkEnabled().catch(() => false),
}); });
this.$message.success('提交成功'); this.$message.success('提交成功');
// 通知列表页(keep-alive)重新拉取数据
sessionStorage.setItem(paymentApplicationListRefreshKey, '1');
this.goBack(); this.goBack();
}, },
goBack() { goBack() {
removeSettlementTransfer(this.$route.query.transferToken); removeSettlementTransfer(this.$route.query.transferToken);
this.$router.$avueRouter?.closeTag?.(); this.$router.$avueRouter?.closeTag?.();
this.$router.push('/payment/payment-application'); this.$router.push(paymentApplicationListRoute);
}, },
formatMoney(value) { formatMoney(value) {
const amount = Number(value); const amount = Number(value);
@@ -2701,15 +2785,15 @@ export default {
width: 100%; width: 100%;
min-height: 32px; min-height: 32px;
} }
.payment-form-page__settlement-select { .payment-form-page__settlement-input {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.payment-form-page__settlement-select :deep(.el-select__wrapper) { .payment-form-page__settlement-input :deep(.el-input__inner) {
width: 100%; cursor: pointer;
} }
.payment-form-page__settlement-select :deep(.el-tag__close) { .payment-form-page__settlement-input.is-disabled :deep(.el-input__inner) {
display: none; cursor: not-allowed;
} }
.payment-form-page__settlement-select-btn { .payment-form-page__settlement-select-btn {
flex-shrink: 0; flex-shrink: 0;
+130 -7
View File
@@ -61,6 +61,13 @@
}}</el-tag> }}</el-tag>
</template> </template>
<template #kingdeeLockStatus="{ row }">
<el-tooltip v-if="lockTag(row).show" :content="lockTag(row).tip" placement="top">
<el-tag :type="lockTag(row).type" class="status-text">{{ lockTag(row).label }}</el-tag>
</el-tooltip>
<span v-else>-</span>
</template>
<template #menu="{ row }"> <template #menu="{ row }">
<el-link <el-link
v-if="hasPermission('payment_application_view')" v-if="hasPermission('payment_application_view')"
@@ -117,7 +124,11 @@
>驳回</el-link >驳回</el-link
> >
<el-link <el-link
v-if="hasPermission('payment_application_void') && row.approvalStatus === 'approved'" v-if="
hasPermission('payment_application_void') &&
row.approvalStatus === 'approved' &&
row.kingdeeLockStatus !== 'locked'
"
type="danger" type="danger"
@click="handleVoid(row)" @click="handleVoid(row)"
>作废</el-link >作废</el-link
@@ -141,6 +152,9 @@ import organizationSearch from '@/mixins/organization-search';
import { getMKApprovalUrl } from '@/api/system/business-process'; import { getMKApprovalUrl } from '@/api/system/business-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval'; import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
const paymentApplicationListRoute = '/payment/payment-application';
const paymentApplicationListRefreshKey = 'payment-application-list-refresh';
export default { export default {
name: 'PaymentApplication', name: 'PaymentApplication',
mixins: [organizationSearch], mixins: [organizationSearch],
@@ -149,6 +163,8 @@ export default {
rows: [], rows: [],
selection: [], selection: [],
loading: false, loading: false,
syncLoading: false,
resultSyncing: false,
query: {}, query: {},
option: {}, option: {},
page: { page: {
@@ -180,7 +196,28 @@ export default {
this.mkEnabled = false; this.mkEnabled = false;
}); });
}, },
activated() {
this.consumeListRefreshFlag();
},
watch: {
$route(to, from) {
if (
to.path === paymentApplicationListRoute &&
from?.path?.includes('/payment-application/form')
) {
this.consumeListRefreshFlag();
}
},
},
methods: { methods: {
/** 表单提交成功后带标记返回时,重新拉取列表 */
consumeListRefreshFlag() {
if (!sessionStorage.getItem(paymentApplicationListRefreshKey)) {
return;
}
sessionStorage.removeItem(paymentApplicationListRefreshKey);
this.onLoad(this.page);
},
hasPermission(code) { hasPermission(code) {
return this.permission?.[code] !== false; return this.permission?.[code] !== false;
}, },
@@ -283,13 +320,19 @@ export default {
bizId: row.id, bizId: row.id,
subjectName: row.paymentNo || '', subjectName: row.paymentNo || '',
approvalStatus: previousApprovalStatus, approvalStatus: previousApprovalStatus,
mkEnabled: this.mkEnabled,
}); });
this.$message.success('提交成功'); this.$message.success('提交成功');
this.refreshChange(); this.refreshChange();
}, },
async handleApprove(row) { async handleApprove(row) {
await api.approve({ id: row.id }); const res = await api.approve({ id: row.id });
this.$message.success('审批通过'); // data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
if (res.data.data === false) {
this.$message.warning(res.data.msg);
} else {
this.$message.success(res.data.msg || '审批通过');
}
this.refreshChange(); this.refreshChange();
}, },
handleMkAudit(row) { handleMkAudit(row) {
@@ -325,18 +368,60 @@ export default {
async handleSyncRow(row) { async handleSyncRow(row) {
const data = this.unwrapData(await api.syncKingdee(row.id)); const data = this.unwrapData(await api.syncKingdee(row.id));
this.$message.success(`已生成金蝶付款单:${data}`); this.$message.success(`已生成金蝶付款单:${data}`);
this.refreshChange(); await this.refreshChange();
this.warnAttachmentNotUploaded([row.id]);
}, },
async handleSync() { async handleSync() {
if (this.syncLoading) return;
const rows = this.selection.filter(row => row.approvalStatus === 'approved'); const rows = this.selection.filter(row => row.approvalStatus === 'approved');
if (!rows.length) { if (!rows.length) {
this.$message.warning('请选择至少一条审批通过的付款申请'); this.$message.warning('请选择至少一条审批通过的付款申请');
return; return;
} }
await api.syncKingdeeBatch(rows.map(row => row.id)); this.syncLoading = true;
this.$message.success(`已同步${rows.length}条付款申请`); try {
this.refreshChange(); 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.refreshChange();
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() { handleExport() {
const columns = this.option.column.filter(column => !column.hide && column.prop); const columns = this.option.column.filter(column => !column.hide && column.prop);
const headers = columns.map(item => item.label); const headers = columns.map(item => item.label);
@@ -361,6 +446,44 @@ export default {
'' ''
); );
}, },
lockTag(row) {
if (row.kingdeeLockStatus === 'locked') {
return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' };
}
if (row.kingdeeLockStatus === 'failed') {
if ((row.kingdeeTryCount || 0) >= 5) {
return {
show: true,
type: 'danger',
label: '锁定失败·已达上限',
tip: row.kingdeeLockFailReason || '自动补偿次数已用完,请重推「同步金蝶」立即补锁',
};
}
return {
show: true,
type: 'warning',
label: '锁定失败·补偿中',
tip: row.kingdeeLockFailReason || '系统将自动补偿重锁',
};
}
// 金蝶解锁终态标识:后端按 unlocked 锁记录回填(付款申请不作废,仅此标识可区分金蝶删单与推送失败)
if (row.kingdeeUnlockTime) {
const isDelete = row.kingdeeUnlockOperationName === '金蝶删单';
return {
show: true,
type: isDelete ? 'danger' : 'warning',
label: row.kingdeeUnlockOperationName || '金蝶解锁',
tip: `${row.kingdeeUnlockOperator || '-'} 于 ${this.$dayjs(row.kingdeeUnlockTime).format('YYYY-MM-DD HH:mm:ss')} 解锁`,
};
}
return { show: false };
},
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
}, },
}; };
</script> </script>
+38 -7
View File
@@ -56,6 +56,16 @@
<el-form-item v-show="searchExpanded" label="所属组织"> <el-form-item v-show="searchExpanded" label="所属组织">
<el-input v-model="query.claimerDeptName" clearable placeholder="请输入" /> <el-input v-model="query.claimerDeptName" clearable placeholder="请输入" />
</el-form-item> </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-form-item v-show="searchExpanded" label="金蝶单据状态">
<el-select v-model="query.kingdeeBillStatus" clearable placeholder="请选择"> <el-select v-model="query.kingdeeBillStatus" clearable placeholder="请选择">
<el-option <el-option
@@ -83,6 +93,9 @@
<section class="claim-record-page__table-panel"> <section class="claim-record-page__table-panel">
<div class="claim-record-page__toolbar"> <div class="claim-record-page__toolbar">
<el-button :loading="refreshing" @click="handleRefreshKingdeeResult">
查询金蝶认领结果
</el-button>
<table-header-tools <table-header-tools
:columns="columns" :columns="columns"
v-model:visible-column-keys="visibleColumnKeys" v-model:visible-column-keys="visibleColumnKeys"
@@ -118,7 +131,11 @@
<el-table-column label="操作" width="120" fixed="right" align="center"> <el-table-column label="操作" width="120" fixed="right" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-link <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" type="danger"
@click="handleVoid(row)" @click="handleVoid(row)"
>作废</el-link >作废</el-link
@@ -164,6 +181,7 @@ const emptyQuery = () => ({
claimDateRange: [], claimDateRange: [],
claimerDeptName: '', claimerDeptName: '',
kingdeeBillStatus: '', kingdeeBillStatus: '',
pushStatus: '',
}); });
export default { export default {
@@ -182,10 +200,12 @@ export default {
tableSize: 'default', tableSize: 'default',
claimStatusOptions: api.claimStatusOptions, claimStatusOptions: api.claimStatusOptions,
kingdeeBillStatusOptions: api.kingdeeBillStatusOptions, kingdeeBillStatusOptions: api.kingdeeBillStatusOptions,
pushStatusOptions: api.pushStatusOptions,
columns: receiptClaimRecordTableColumns, columns: receiptClaimRecordTableColumns,
visibleColumnKeys: receiptClaimRecordTableColumns.map(column => column.prop), visibleColumnKeys: receiptClaimRecordTableColumns.map(column => column.prop),
rows: [], rows: [],
loading: false, loading: false,
refreshing: false,
page: { current: 1, size: 10, total: 0 }, page: { current: 1, size: 10, total: 0 },
}; };
}, },
@@ -206,8 +226,9 @@ export default {
}, },
methods: { methods: {
unwrapData(response) { unwrapData(response) {
const body = response?.data || response || {}; // 数字 0 是合法返回值(如回写/同步 0 条),须用 ?? 判空避免 0 被信封对象顶掉导致 Number() 得 NaN
return body?.data || body; const body = response?.data ?? response ?? {};
return body?.data ?? body;
}, },
queryParams() { queryParams() {
const transactionRange = this.query.transactionDateRange || []; const transactionRange = this.query.transactionDateRange || [];
@@ -251,14 +272,24 @@ export default {
}, },
async handleVoid(row) { async handleVoid(row) {
await this.$confirm( await this.$confirm(
'作废后将回退本次认领金额,并生成审核通过状态的金蝶认领冲单,是否继续?', '作废后将回退本次认领金额,是否继续?',
'作废认领记录', '作废认领记录',
{ type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' } { type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }
); );
const kingdeeBillNo = this.unwrapData(await api.voidClaim(row.id)); await api.voidClaim(row.id);
this.$message.success(`作废成功,金蝶冲单号:${kingdeeBillNo}`); this.$message.success('作废成功');
await this.loadTable(); 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) { hasPermission(code) {
return this.isAdmin || this.validData(this.permission?.[code], false); return this.isAdmin || this.validData(this.permission?.[code], false);
}, },
@@ -305,7 +336,7 @@ export default {
} }
.claim-record-page__toolbar { .claim-record-page__toolbar {
display: flex; display: flex;
justify-content: flex-end; justify-content: space-between;
align-items: center; align-items: center;
padding: 12px 0; padding: 12px 0;
} }
+20 -3
View File
@@ -43,6 +43,12 @@
<el-table-column label="已认领金额" min-width="130" align="right"> <el-table-column label="已认领金额" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.claimedAmount) }}</template> <template #default="{ row }">{{ formatMoney(row.claimedAmount) }}</template>
</el-table-column> </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 label="剩余可认领金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(flowRemaining(row)) }}</template>
</el-table-column>
<el-table-column prop="summary" label="摘要" min-width="180" show-overflow-tooltip /> <el-table-column prop="summary" label="摘要" min-width="180" show-overflow-tooltip />
</el-table> </el-table>
<el-row :gutter="32" class="receipt-claim-batch-form-page__claim-info"> <el-row :gutter="32" class="receipt-claim-batch-form-page__claim-info">
@@ -295,10 +301,21 @@ export default {
0 0
); );
}, },
flowRemaining(flow) {
return Math.max(Number(flow.unclaimedAmount || 0) - Number(flow.claimedAmount || 0), 0);
},
maxAllocation(row) { maxAllocation(row) {
const current = Number(row.settlement.allocations?.[row.flow.id] || 0); const current = Number(row.settlement.allocations?.[row.flow.id] || 0);
const otherTotal = this.settlementAllocatedTotal(row.settlement) - current; const otherTotal = this.settlementAllocatedTotal(row.settlement) - current;
return Math.max(this.settlementRemaining(row.settlement) - otherTotal, 0); // 流水维度:该流水其余分摊已占用部分同样挤占剩余可认领金额
const flowOtherTotal = this.flowAllocatedTotal(row.flow.id) - current;
return Math.max(
Math.min(
this.settlementRemaining(row.settlement) - otherTotal,
this.flowRemaining(row.flow) - flowOtherTotal
),
0
);
}, },
handleAllocationChange(settlement) { handleAllocationChange(settlement) {
const total = this.settlementAllocatedTotal(settlement); const total = this.settlementAllocatedTotal(settlement);
@@ -336,11 +353,11 @@ export default {
for (const flow of this.flows) { for (const flow of this.flows) {
const total = this.flowAllocatedTotal(flow.id); const total = this.flowAllocatedTotal(flow.id);
const remaining = Math.max( const remaining = Math.max(
Number(flow.receiptAmount || 0) - Number(flow.claimedAmount || 0), Number(flow.unclaimedAmount || 0) - Number(flow.claimedAmount || 0),
0 0
); );
if (total > remaining + 0.001) { if (total > remaining + 0.001) {
this.$message.warning(`流水${flow.receiptNoticeNo}的分摊金额不能超过剩余可认领金额`); this.$message.warning(`流水${flow.receiptNoticeNo}的累计认领金额不能超过公告未认领金额`);
return; return;
} }
} }
+17 -2
View File
@@ -47,6 +47,14 @@
><el-form-item label="已认领金额" ><el-form-item label="已认领金额"
><el-input :model-value="formatMoney(form.claimedAmount)" disabled /></el-form-item ><el-input :model-value="formatMoney(form.claimedAmount)" disabled /></el-form-item
></el-col> ></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-col :span="6"
><el-form-item label="摘要"><el-input v-model="form.summary" disabled /></el-form-item ><el-form-item label="摘要"><el-input v-model="form.summary" disabled /></el-form-item
></el-col> ></el-col>
@@ -197,6 +205,7 @@ const emptyForm = () => ({
transactionTime: '', transactionTime: '',
detailSerialNo: '', detailSerialNo: '',
claimedAmount: 0, claimedAmount: 0,
unclaimedAmount: 0,
claimerName: '', claimerName: '',
claimerDeptName: '', claimerDeptName: '',
claimDate: '', claimDate: '',
@@ -242,6 +251,12 @@ export default {
0 0
); );
}, },
claimableAmount() {
return Math.max(
Number(this.form.unclaimedAmount || 0) - Number(this.form.claimedAmount || 0),
0
);
},
}, },
created() { created() {
if (!this.batchMode) this.initialize(); if (!this.batchMode) this.initialize();
@@ -331,8 +346,8 @@ export default {
this.$message.warning('分摊收款金额必须大于0'); this.$message.warning('分摊收款金额必须大于0');
return; return;
} }
if (this.allocatedTotal > amount - Number(this.form.claimedAmount || 0) + 0.001) { if (this.allocatedTotal > this.claimableAmount + 0.001) {
this.$message.warning('本次分摊金额不能超过流水剩余可认领金额'); this.$message.warning('累计认领金额不能超过公告未认领金额(剩余可认领金额)');
return; return;
} }
for (const row of this.form.settlements) for (const row of this.form.settlements)
+148 -76
View File
@@ -15,10 +15,17 @@
<el-form-item label="对方账号"> <el-form-item label="对方账号">
<el-input v-model="query.counterpartyAccount" clearable placeholder="请输入" /> <el-input v-model="query.counterpartyAccount" clearable placeholder="请输入" />
</el-form-item> </el-form-item>
<el-form-item v-show="searchExpanded" label="摘要"> <el-form-item label="推送状态">
<el-input v-model="query.summary" clearable placeholder="请输入" /> <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>
<el-form-item v-show="searchExpanded" label="认领状态"> <el-form-item label="认领状态">
<el-select v-model="query.claimStatus" clearable placeholder="请选择"> <el-select v-model="query.claimStatus" clearable placeholder="请选择">
<el-option <el-option
v-for="item in claimStatusOptions" v-for="item in claimStatusOptions"
@@ -28,7 +35,7 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item v-show="searchExpanded" label="交易时间"> <el-form-item label="交易时间">
<el-date-picker <el-date-picker
v-model="query.transactionTimeRange" v-model="query.transactionTimeRange"
type="datetimerange" type="datetimerange"
@@ -39,6 +46,45 @@
clearable clearable
/> />
</el-form-item> </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>
<div class="receipt-flow-page__search-actions"> <div class="receipt-flow-page__search-actions">
<el-button type="primary" @click="handleSearch">查询</el-button> <el-button type="primary" @click="handleSearch">查询</el-button>
@@ -104,7 +150,7 @@
</el-link> </el-link>
<el-tag <el-tag
v-else-if="column.status" v-else-if="column.status"
:type="statusType(row.claimStatus)" :type="statusType(row, column)"
class="status-text" class="status-text"
> >
{{ displayValue(row[column.prop]) }} {{ displayValue(row[column.prop]) }}
@@ -113,10 +159,23 @@
<span v-else>{{ displayValue(row[column.prop]) }}</span> <span v-else>{{ displayValue(row[column.prop]) }}</span>
</template> </template>
</el-table-column> </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 }"> <template #default="{ row }">
<el-link v-if="canClaim(row)" type="primary" @click="openClaim(row)">认领</el-link> <el-link
<span v-else>-</span> 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> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -167,7 +226,7 @@
{{ displayValue(detailDialog.data.detailSerialNo) }} {{ displayValue(detailDialog.data.detailSerialNo) }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="认领状态"> <el-descriptions-item label="认领状态">
<el-tag :type="statusType(detailDialog.data.claimStatus)"> <el-tag :type="statusType(detailDialog.data, { prop: 'claimStatusName' })">
{{ displayValue(detailDialog.data.claimStatusName) }} {{ displayValue(detailDialog.data.claimStatusName) }}
</el-tag> </el-tag>
</el-descriptions-item> </el-descriptions-item>
@@ -177,6 +236,23 @@
<el-descriptions-item label="剩余可认领金额"> <el-descriptions-item label="剩余可认领金额">
{{ formatMoney(detailDialog.data.remainingAmount) }} {{ formatMoney(detailDialog.data.remainingAmount) }}
</el-descriptions-item> </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="摘要"> <el-descriptions-item label="摘要">
{{ displayValue(detailDialog.data.summary) }} {{ displayValue(detailDialog.data.summary) }}
</el-descriptions-item> </el-descriptions-item>
@@ -191,7 +267,9 @@
</el-table-column> </el-table-column>
<el-table-column prop="operatorName" label="认领人" min-width="120" align="center" /> <el-table-column prop="operatorName" label="认领人" min-width="120" align="center" />
<el-table-column label="状态变化" min-width="170" 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>
<el-table-column prop="content" label="认领说明" min-width="220" /> <el-table-column prop="content" label="认领说明" min-width="220" />
</el-table> </el-table>
@@ -211,71 +289,19 @@ import { receiptFlowTableColumns } from '@/option/payment/receiptFlow';
import TableHeaderTools from '@/components/table-header-tools/main.vue'; import TableHeaderTools from '@/components/table-header-tools/main.vue';
import { isAdministrator } from '@/utils/auth'; 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 = () => ({ const emptyQuery = () => ({
receiptNoticeNo: '', receiptNoticeNo: '',
counterpartyName: '', counterpartyName: '',
counterpartyBank: '', counterpartyBank: '',
counterpartyAccount: '', counterpartyAccount: '',
summary: '', summary: '',
detailSerialNo: '',
claimStatus: '', claimStatus: '',
pushStatus: '',
kingdeeBillStatus: '',
receiptAmountStart: undefined,
receiptAmountEnd: undefined,
claimerName: '',
transactionTimeRange: [], transactionTimeRange: [],
}); });
@@ -288,6 +314,8 @@ export default {
ArrowUp, ArrowUp,
query: emptyQuery(), query: emptyQuery(),
claimStatusOptions: api.claimStatusOptions, claimStatusOptions: api.claimStatusOptions,
pushStatusOptions: api.pushStatusOptions,
kingdeeBillStatusOptions: api.kingdeeBillStatusOptions,
columns: receiptFlowTableColumns, columns: receiptFlowTableColumns,
visibleColumnKeys: receiptFlowTableColumns.map(column => column.prop), visibleColumnKeys: receiptFlowTableColumns.map(column => column.prop),
searchVisible: true, searchVisible: true,
@@ -296,6 +324,7 @@ export default {
selectedRows: [], selectedRows: [],
loading: false, loading: false,
syncing: false, syncing: false,
pushingId: null,
searchExpanded: false, searchExpanded: false,
// 与 avue-crud 一致:搜索项数量超过 searchIndex(默认一行 4 项)时才显示“展开/收起” // 与 avue-crud 一致:搜索项数量超过 searchIndex(默认一行 4 项)时才显示“展开/收起”
searchIndex: 4, searchIndex: 4,
@@ -321,8 +350,9 @@ export default {
}, },
methods: { methods: {
unwrapData(response) { unwrapData(response) {
const body = response?.data || response || {}; // 数字 0 是合法返回值(如回写/同步 0 条),须用 ?? 判空避免 0 被信封对象顶掉导致 Number() 得 NaN
return body?.data || body; const body = response?.data ?? response ?? {};
return body?.data ?? body;
}, },
async loadTable() { async loadTable() {
this.loading = true; this.loading = true;
@@ -358,9 +388,8 @@ export default {
async handleSync() { async handleSync() {
this.syncing = true; this.syncing = true;
try { try {
const mockRows = createReceiptFlowMockRows(); const syncedCount = Number(this.unwrapData(await api.sync()) || 0);
const syncedCount = Number(this.unwrapData(await api.sync({ flows: mockRows })) || 0); this.$message.success(`流水同步完成,共同步${syncedCount}条认领公告`);
this.$message.success(`流水同步完成,共同步${syncedCount}条`);
await this.loadTable(); await this.loadTable();
} finally { } finally {
this.syncing = false; this.syncing = false;
@@ -419,8 +448,41 @@ export default {
canClaim(row) { canClaim(row) {
return this.hasPermission('receipt_flow_claim') && Number(row.remainingAmount || 0) > 0; return this.hasPermission('receipt_flow_claim') && Number(row.remainingAmount || 0) > 0;
}, },
statusType(status) { canPush(row) {
return { claimed: 'success', partial: 'warning', unclaimed: 'info' }[status] || ''; 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) { claimStatusChange(row) {
const label = status => const label = status =>
@@ -453,7 +515,14 @@ export default {
} }
.receipt-flow-page__search :deep(.el-input), .receipt-flow-page__search :deep(.el-input),
.receipt-flow-page__search :deep(.el-select), .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%; width: 100%;
} }
.receipt-flow-page__search-actions { .receipt-flow-page__search-actions {
@@ -487,6 +556,9 @@ export default {
.receipt-flow-page :deep(.el-table) { .receipt-flow-page :deep(.el-table) {
--el-table-border-color: #eff1f7; --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) { .receipt-flow-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
background: #fafafa; background: #fafafa;
} }
@@ -14,7 +14,7 @@
label-width="auto" label-width="auto"
> >
<el-row :gutter="24"> <el-row :gutter="24">
<el-col v-for="field in fields" :key="field.prop" :span="6"> <el-col v-for="field in visibleFields" :key="field.prop" :span="6">
<el-form-item :label="field.label" :prop="field.prop"> <el-form-item :label="field.label" :prop="field.prop">
<el-date-picker <el-date-picker
v-if="field.type === 'date' && editable" v-if="field.type === 'date' && editable"
@@ -22,6 +22,7 @@
type="date" type="date"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="请选择" placeholder="请选择"
@change="fetchBfiExchangeRate"
/> />
<el-select <el-select
v-else-if="field.type === 'project' && editable" v-else-if="field.type === 'project' && editable"
@@ -63,6 +64,12 @@
:precision="2" :precision="2"
:controls="false" :controls="false"
/> />
<span
v-else-if="field.prop === 'formalSettlementNo'"
:class="{ 'form-readonly--code': form.formalSettlementNo }"
>
{{ form.formalSettlementNo || (form.id ? '-' : '系统自动生成') }}
</span>
<span v-else-if="field.money">{{ formatMoney(form[field.prop]) }}</span> <span v-else-if="field.money">{{ formatMoney(form[field.prop]) }}</span>
<span v-else-if="field.type === 'contract'">{{ <span v-else-if="field.type === 'contract'">{{
displayValue(form.contractName) displayValue(form.contractName)
@@ -245,24 +252,36 @@
:index="detailIndexMethod" :index="detailIndexMethod"
/> />
<el-table-column <el-table-column
v-for="column in detailTableColumns" v-for="item in detailDisplayColumns"
:key="column.prop" :key="item.key"
v-bind="column" :label="item.kind === 'fee' ? item.name : item.column.label"
:prop="item.kind === 'fee' ? undefined : item.column.prop"
:min-width="item.kind === 'fee' ? 130 : item.column.minWidth"
align="center" align="center"
show-overflow-tooltip show-overflow-tooltip
> >
<template #default="{ row }"> <template #default="{ row }">
<span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span> <span v-if="item.kind === 'fee'">
<span v-else-if="column.prop === 'transportQuantity'"> {{ formatMoney(parseFeeItems(row.feeItemsJson || row.feeItems)[item.name]) }}
{{ formatDetailTransportQuantity(row[column.prop]) }}
</span> </span>
<span v-else-if="column.prop === 'mileage'"> <el-link
{{ formatDetailMileage(row[column.prop]) }} v-else-if="item.column.link && row[item.column.prop] && !publicMode"
type="primary"
@click.stop="handleDetailLink(row, item.column)"
>
{{ row[item.column.prop] }}
</el-link>
<span v-else-if="item.column.money">{{ formatMoney(row[item.column.prop]) }}</span>
<span v-else-if="item.column.prop === 'transportQuantity'">
{{ formatDetailTransportQuantity(row[item.column.prop]) }}
</span> </span>
<span v-else-if="column.prop === 'transportType'"> <span v-else-if="item.column.prop === 'mileage'">
{{ transportTypeName(row[column.prop]) }} {{ formatDetailMileage(row[item.column.prop]) }}
</span> </span>
<span v-else>{{ displayValue(row[column.prop]) }}</span> <span v-else-if="item.column.prop === 'transportType'">
{{ transportTypeName(row[item.column.prop]) }}
</span>
<span v-else>{{ displayValue(row[item.column.prop]) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column v-if="editable" label="操作" width="150" fixed="right" align="center"> <el-table-column v-if="editable" label="操作" width="150" fixed="right" align="center">
@@ -377,6 +396,16 @@
</div> </div>
</section-card> </section-card>
<project-advance-write-off
ref="projectAdvanceWriteOffRef"
v-model="projectAdvanceWriteOffs"
:editable="editable"
:contract-id="form.contractId"
:settlement-amount="form.settlementAmount"
settlement-type="formal"
:settlement-id="form.id"
/>
<section-card title="发票信息"> <section-card title="发票信息">
<div class="formal-editor__invoice-claim"> <div class="formal-editor__invoice-claim">
<el-form inline label-position="right" label-width="auto" @submit.prevent> <el-form inline label-position="right" label-width="auto" @submit.prevent>
@@ -448,78 +477,61 @@
<span v-else>{{ formatMoney(row.matchedAmount) }}</span> <span v-else>{{ formatMoney(row.matchedAmount) }}</span>
</template> </template>
</el-table-column> </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 }"> <template #default="{ row, $index }">
<div class="formal-editor__links"> <div class="formal-editor__links">
<el-link type="primary" @click="viewInvoiceAttachment(row)">查看</el-link> <el-link
<el-link type="primary" @click="downloadInvoiceAttachment(row)">下载</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> <el-link v-if="editable" type="danger" @click="removeInvoice($index)">删除</el-link>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </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> </section-card>
<el-dialog <kingdee-invoice-pool-dialog
v-model="invoiceClaimDialog.visible" v-model="invoiceClaimDialog.visible"
title="查询可认领发票" title="查询可认领发票"
width="92%" confirm-text="确认认领"
append-to-body multiple
destroy-on-close :initial-invoice-no="String(invoiceClaimNo || '').trim()"
> :query-api="invoicePoolQueryApi"
<el-table @confirm="handleInvoiceClaimConfirm"
ref="invoiceClaimTable" />
v-loading="invoiceClaimDialog.loading"
:data="invoiceClaimPagedRows"
row-key="invoiceNo"
border
height="520"
@selection-change="invoiceClaimDialog.selected = $event"
>
<el-table-column
type="selection"
width="52"
fixed="left"
align="center"
reserve-selection
/>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="invoiceNo" label="发票号" min-width="180" align="center" />
<el-table-column prop="invoiceDate" label="开票日期" min-width="130" align="center" />
<el-table-column prop="invoiceType" label="发票类型" min-width="150" align="center" />
<el-table-column prop="issuerName" label="开票单位" min-width="180" align="center" />
<el-table-column prop="receiverName" label="受票单位" min-width="180" align="center" />
<el-table-column
prop="invoiceAmount"
label="发票金额(含税)"
min-width="160"
align="right"
>
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
</el-table-column>
<el-table-column prop="matchedAmount" label="认领金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.matchedAmount) }}</template>
</el-table-column>
</el-table>
<div class="formal-editor__pagination">
<el-pagination
v-model:current-page="invoiceClaimDialog.page.current"
v-model:page-size="invoiceClaimDialog.page.size"
:total="invoiceClaimDialog.page.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
/>
</div>
<template #footer>
<el-button @click="invoiceClaimDialog.visible = false">取消</el-button>
<el-button
type="primary"
:loading="invoiceClaimDialog.confirming"
@click="confirmInvoiceClaims"
>确认认领</el-button
>
</template>
</el-dialog>
<section-card <section-card
v-if="readonly" v-if="readonly"
@@ -995,6 +1007,7 @@ import {
getNextNo, getNextNo,
getReceiptClaims, getReceiptClaims,
claimInvoices as claimInvoicesApi, claimInvoices as claimInvoicesApi,
queryInvoicePool,
save, save,
} from '@/api/settlement/formalSettlement'; } from '@/api/settlement/formalSettlement';
import { import {
@@ -1002,6 +1015,12 @@ import {
getDetailFees as getPreSettlementDetailFees, getDetailFees as getPreSettlementDetailFees,
} from '@/api/settlement/preSettlement'; } from '@/api/settlement/preSettlement';
import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail'; import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail';
import {
resolveReceivablePayableDetailId,
resolveWaybillId,
settlementDetailPath,
} from '@/utils/settlement-detail-link';
import { getBfiExchangeRate } from '@/api/base/currency';
import { import {
createFormalSettlementForm, createFormalSettlementForm,
formalSettlementFormFields, formalSettlementFormFields,
@@ -1018,13 +1037,24 @@ import { h } from 'vue';
import { InfoFilled } from '@element-plus/icons-vue'; import { InfoFilled } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { formatContractOptionLabel } from '@/utils/contract'; import { formatContractOptionLabel } from '@/utils/contract';
import { normalizeCurrencyCode } from '@/utils/currency';
import { downloadFileByUrl } from '@/utils/util'; import { downloadFileByUrl } from '@/utils/util';
import {
downloadKingdeeInvoiceFile,
isKingdeePathAttachment,
viewKingdeeInvoiceFile,
} from '@/utils/kingdee-attachment';
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
import ProjectAdvanceWriteOff from './project-advance-write-off.vue';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { isAdministrator } from '@/utils/auth'; import { isAdministrator } from '@/utils/auth';
// 结算币种为本位币(人民币)时汇率恒为1,汇率日期、结算汇率与按汇率换算的本位币合计都不展示
const LOCAL_CURRENCY_HIDDEN_FIELDS = ['exchangeRateDate', 'exchangeRate', 'localSettlementAmount'];
export default { export default {
name: 'FormalSettlementEditor', name: 'FormalSettlementEditor',
components: { InfoFilled }, components: { InfoFilled, KingdeeInvoicePoolDialog, ProjectAdvanceWriteOff },
props: { props: {
modelValue: Boolean, modelValue: Boolean,
recordId: [String, Number], recordId: [String, Number],
@@ -1053,18 +1083,20 @@ export default {
receiptClaims: [], receiptClaims: [],
adjustments: [], adjustments: [],
changeRecords: [], changeRecords: [],
projectAdvanceWriteOffs: [],
attachments: [], attachments: [],
selectedAttachmentRows: [], selectedAttachmentRows: [],
invoices: [], invoices: [],
// 开票申请经金蝶回写的发票结果(只读展示,含板式文件/数电PDF下载地址)
issuedInvoices: [],
invoiceClaimNo: '', invoiceClaimNo: '',
invoiceClaimLoading: false, invoiceClaimLoading: false,
// 金蝶侧发票附件查看/下载进行中(行组级 loading 守卫)
kingdeeAttachmentBusy: false,
invoiceClaimDialog: { invoiceClaimDialog: {
visible: false, visible: false,
loading: false,
confirming: false,
rows: [],
selected: [], selected: [],
page: { current: 1, size: 10, total: 0 }, confirming: false,
}, },
attachmentUploadFiles: [], attachmentUploadFiles: [],
attachmentTypeOptions: [], attachmentTypeOptions: [],
@@ -1190,6 +1222,34 @@ export default {
title() { title() {
return this.readonly ? '查看正式结算单' : this.recordId ? '编辑正式结算单' : '新增正式结算单'; return this.readonly ? '查看正式结算单' : this.recordId ? '编辑正式结算单' : '新增正式结算单';
}, },
detailFeeItemNames() {
const names = new Set();
this.details.forEach(row => {
Object.keys(this.parseFeeItems(row.feeItemsJson || row.feeItems)).forEach(name =>
names.add(name)
);
});
return Array.from(names);
},
detailDisplayColumns() {
const columns = this.detailTableColumns;
const unitPriceIndex = columns.findIndex(column => column.prop === 'unitPrice');
const beforeColumns = unitPriceIndex < 0 ? columns : columns.slice(0, unitPriceIndex + 1);
const afterColumns = unitPriceIndex < 0 ? [] : columns.slice(unitPriceIndex + 1);
return [
...beforeColumns.map(column => ({ kind: 'field', key: column.prop, column })),
...this.detailFeeItemNames.map(name => ({ kind: 'fee', key: `fee-${name}`, name })),
...afterColumns.map(column => ({ kind: 'field', key: column.prop, column })),
];
},
// 币种为空按人民币处理,与后端预结算/正式结算对空币种的默认取值一致
isLocalCurrencySettlement() {
return normalizeCurrencyCode(this.form.currency) === 'RMB';
},
visibleFields() {
if (!this.isLocalCurrencySettlement) return this.fields;
return this.fields.filter(field => !LOCAL_CURRENCY_HIDDEN_FIELDS.includes(field.prop));
},
summaryTotal() { summaryTotal() {
return this.summaryFees.reduce((total, row) => total + Number(row.settlementAmount || 0), 0); return this.summaryFees.reduce((total, row) => total + Number(row.settlementAmount || 0), 0);
}, },
@@ -1221,11 +1281,6 @@ export default {
}); });
return Array.from(names); 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: { watch: {
modelValue: { modelValue: {
@@ -1287,9 +1342,17 @@ export default {
} }
this.adjustments = data.adjustments || []; this.adjustments = data.adjustments || [];
this.changeRecords = data.changeRecords || []; this.changeRecords = data.changeRecords || [];
this.projectAdvanceWriteOffs = (data.projectAdvanceWriteOffs || []).map(row => ({
...row,
writeOffAmount: Number(row.writeOffAmount || 0),
projectAdvanceAmount: Number(row.projectAdvanceAmount || 0),
availableWriteOffAmount: Number(row.availableWriteOffAmount || 0),
createTime: row.paymentCreateTime || row.createTime,
}));
this.attachments = this.parseAttachments(data.attachmentsJson); this.attachments = this.parseAttachments(data.attachmentsJson);
this.selectedAttachmentRows = []; this.selectedAttachmentRows = [];
this.invoices = data.invoices || []; this.invoices = data.invoices || [];
this.issuedInvoices = data.issuedInvoices || [];
this.sortAttachments(); this.sortAttachments();
this.contracts = this.filterContracts(this.allContracts, this.form.projectId); this.contracts = this.filterContracts(this.allContracts, this.form.projectId);
}, },
@@ -1316,12 +1379,13 @@ export default {
this.receiptClaims = []; this.receiptClaims = [];
this.adjustments = []; this.adjustments = [];
this.changeRecords = []; this.changeRecords = [];
this.projectAdvanceWriteOffs = [];
this.attachments = []; this.attachments = [];
this.selectedAttachmentRows = []; this.selectedAttachmentRows = [];
this.invoices = []; this.invoices = [];
this.issuedInvoices = [];
this.invoiceClaimNo = ''; this.invoiceClaimNo = '';
this.invoiceClaimDialog.visible = false; this.invoiceClaimDialog.visible = false;
this.invoiceClaimDialog.rows = [];
this.invoiceClaimDialog.selected = []; this.invoiceClaimDialog.selected = [];
this.attachmentUploadFiles = []; this.attachmentUploadFiles = [];
this.billingRuleDialog = { this.billingRuleDialog = {
@@ -1345,6 +1409,7 @@ export default {
if (this.initialData) await this.applyInitialData(); if (this.initialData) await this.applyInitialData();
Object.assign(this.form, newRecordAudit); Object.assign(this.form, newRecordAudit);
await this.refreshFormalSettlementNo(); await this.refreshFormalSettlementNo();
await this.fetchBfiExchangeRate();
return; return;
} }
this.loading = true; this.loading = true;
@@ -1579,6 +1644,7 @@ export default {
(settlementType === 'receivable' ? contract.partyA : contract.partyB), (settlementType === 'receivable' ? contract.partyA : contract.partyB),
settlementType, settlementType,
settlementTypeName: settlementType === 'receivable' ? '应收' : '应付', settlementTypeName: settlementType === 'receivable' ? '应收' : '应付',
currency: contract.settlementCurrency || 'RMB',
}); });
this.sources = []; this.sources = [];
this.details = []; this.details = [];
@@ -1586,6 +1652,23 @@ export default {
this.form.sourcePreSettlementIds = []; this.form.sourcePreSettlementIds = [];
this.form.sourceDetailIds = []; this.form.sourceDetailIds = [];
if (refreshSettlementNo) this.refreshFormalSettlementNo(); 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() { openCandidateDialog() {
this.candidate.visible = true; this.candidate.visible = true;
@@ -2023,6 +2106,21 @@ export default {
this.appliedDetailQuery = { ...this.detailQuery }; this.appliedDetailQuery = { ...this.detailQuery };
this.detailPage.current = 1; this.detailPage.current = 1;
}, },
// 结算明细的「单据号」跳到该应收应付单据在应收明细/应付明细页的详情,「运单号」跳到运单详情
async handleDetailLink(row, column) {
if (column.prop === 'documentNo') {
const settlementType = row.settlementType || this.form.settlementType || 'payable';
const detailId = await resolveReceivablePayableDetailId(row, settlementType);
if (!detailId) return this.$message.info('当前记录未找到对应的应收/应付明细');
this.$router.push({ path: settlementDetailPath(settlementType), query: { detailId } });
return;
}
if (column.prop === 'waybillNo') {
const waybillId = await resolveWaybillId(row);
if (!waybillId) return this.$message.info('当前记录未找到对应的运单详情');
this.$router.push({ path: '/business/waybill-manage/detail', query: { id: waybillId } });
}
},
resetDetailQuery(apply = true) { resetDetailQuery(apply = true) {
this.detailQuery = { documentNo: '', waybillNo: '', batchNo: '', cargoName: '' }; this.detailQuery = { documentNo: '', waybillNo: '', batchNo: '', cargoName: '' };
if (apply) this.handleDetailQuery(); if (apply) this.handleDetailQuery();
@@ -2310,79 +2408,19 @@ export default {
this.adjust.saving = false; 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() { 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.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() { async confirmInvoiceClaims() {
if (!this.invoiceClaimDialog.selected.length) { if (!this.invoiceClaimDialog.selected.length) {
@@ -2408,7 +2446,11 @@ export default {
if (Number(this.form.settlementAmount || 0) > 0) { if (Number(this.form.settlementAmount || 0) > 0) {
remaining = Math.max(0, remaining - matchedAmount); remaining = Math.max(0, remaining - matchedAmount);
} }
return { ...row, matchedAmount }; return {
...row,
invoiceType: row.invoiceTypeName || row.invoiceType || '',
matchedAmount,
};
}) })
.filter(row => row.matchedAmount > 0); .filter(row => row.matchedAmount > 0);
if (!invoices.length) { if (!invoices.length) {
@@ -2494,16 +2536,64 @@ export default {
const attachment = this.invoiceAttachment(row); const attachment = this.invoiceAttachment(row);
return attachment.originalName || attachment.name || `${row.invoiceNo || '发票'}.pdf`; 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); const url = this.invoiceAttachmentUrl(row);
if (!url) return this.$message.warning('当前Mock发票暂无可查看附件'); if (!url) return this.$message.warning('当前发票暂无可查看附件');
window.open(url, '_blank'); 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); const url = this.invoiceAttachmentUrl(row);
if (!url) return this.$message.warning('当前Mock发票暂无可下载附件'); if (!url) return this.$message.warning('当前发票暂无可下载附件');
downloadFileByUrl(url, this.invoiceAttachmentName(row)); 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() { buildSavePayload() {
return { return {
id: this.form.id, id: this.form.id,
@@ -2549,6 +2639,7 @@ export default {
matchedAmount: Number(row.matchedAmount || 0), matchedAmount: Number(row.matchedAmount || 0),
attachmentJson: row.attachmentJson || '', attachmentJson: row.attachmentJson || '',
})), })),
projectAdvanceWriteOffs: this.$refs.projectAdvanceWriteOffRef?.toSavePayload?.() || [],
remark: this.form.remark, remark: this.form.remark,
}; };
}, },
@@ -2557,6 +2648,9 @@ export default {
}, },
async handleDraftSave() { async handleDraftSave() {
// 保存草稿:不做表单校验,直接落草稿并返回列表刷新 // 保存草稿:不做表单校验,直接落草稿并返回列表刷新
if (this.$refs.projectAdvanceWriteOffRef?.validateBeforeSave?.() === false) {
return;
}
this.draftSaving = true; this.draftSaving = true;
try { try {
await save({ await save({
@@ -2584,6 +2678,9 @@ export default {
return this.$message.warning('请完善手工费用的费用类型和费用项'); return this.$message.warning('请完善手工费用的费用类型和费用项');
} }
if (!this.validateInvoices()) return; if (!this.validateInvoices()) return;
if (this.$refs.projectAdvanceWriteOffRef?.validateBeforeSave?.() === false) {
return;
}
// 附件类型仅做提示,不阻断正式结算单新增/保存提交。 // 附件类型仅做提示,不阻断正式结算单新增/保存提交。
// this.validateAttachments(); // this.validateAttachments();
this.saving = true; this.saving = true;
@@ -2864,6 +2961,9 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.form-readonly--code {
color: var(--el-color-primary);
}
.formal-editor--page { .formal-editor--page {
padding-bottom: 72px; padding-bottom: 72px;
} }
@@ -2993,6 +3093,14 @@ export default {
.formal-editor__invoice-claim :deep(.el-input) { .formal-editor__invoice-claim :deep(.el-input) {
width: 360px; 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 { .formal-editor__negative-amount {
color: var(--el-color-danger); color: var(--el-color-danger);
} }
@@ -21,6 +21,7 @@
type="primary" type="primary"
plain plain
:disabled="selection.length !== 1" :disabled="selection.length !== 1"
:loading="syncLoading"
@click="$emit('sync')" @click="$emit('sync')"
>同步金蝶</el-button >同步金蝶</el-button
> >
@@ -91,6 +92,12 @@
<span v-else-if="column.prop === 'kingdeeSyncStatus'">{{ <span v-else-if="column.prop === 'kingdeeSyncStatus'">{{
kingdeeSyncName(row.kingdeeSyncStatus) kingdeeSyncName(row.kingdeeSyncStatus)
}}</span> }}</span>
<template v-else-if="column.prop === 'kingdeeLockStatus'">
<el-tooltip v-if="lockTag(row).show" :content="lockTag(row).tip" placement="top">
<el-tag :type="lockTag(row).type" class="status-text">{{ lockTag(row).label }}</el-tag>
</el-tooltip>
<span v-else>-</span>
</template>
<span v-else>{{ displayValue(row[column.prop]) }}</span> <span v-else>{{ displayValue(row[column.prop]) }}</span>
</template> </template>
</el-table-column> </el-table-column>
@@ -137,6 +144,7 @@ export default {
rows: { type: Array, default: () => [] }, rows: { type: Array, default: () => [] },
columns: { type: Array, default: () => [] }, columns: { type: Array, default: () => [] },
loading: { type: Boolean, default: false }, loading: { type: Boolean, default: false },
syncLoading: { type: Boolean, default: false },
page: { type: Object, required: true }, page: { type: Object, required: true },
hasPermission: { type: Function, required: true }, hasPermission: { type: Function, required: true },
mkEnabled: { type: Boolean, default: false }, mkEnabled: { type: Boolean, default: false },
@@ -267,6 +275,38 @@ export default {
kingdeeSyncName(value) { kingdeeSyncName(value) {
return kingdeeSyncStatusOptions.find(item => item.value === value)?.label || value || '-'; return kingdeeSyncStatusOptions.find(item => item.value === value)?.label || value || '-';
}, },
lockTag(row) {
if (row.kingdeeLockStatus === 'locked') {
return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' };
}
if (row.kingdeeLockStatus === 'failed') {
if ((row.kingdeeTryCount || 0) >= 5) {
return {
show: true,
type: 'danger',
label: '锁定失败·已达上限',
tip: row.kingdeeLockFailReason || '自动补偿次数已用完,请重推「同步金蝶」立即补锁',
};
}
return {
show: true,
type: 'warning',
label: '锁定失败·补偿中',
tip: row.kingdeeLockFailReason || '系统将自动补偿重锁',
};
}
// 金蝶解锁终态标识:删单解锁的源单已作废,此标签用于区分人工作废与金蝶删单
if (row.kingdeeUnlockTime) {
const isDelete = row.kingdeeUnlockOperationName === '金蝶删单';
return {
show: true,
type: isDelete ? 'danger' : 'warning',
label: row.kingdeeUnlockOperationName || '金蝶解锁',
tip: `${row.kingdeeUnlockOperator || '-'} 于 ${this.$dayjs(row.kingdeeUnlockTime).format('YYYY-MM-DD HH:mm:ss')} 解锁`,
};
}
return { show: false };
},
displayValue(value) { displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value; return value === null || value === undefined || value === '' ? '-' : value;
}, },
@@ -16,7 +16,7 @@
class="pre-settlement-editor__form" class="pre-settlement-editor__form"
> >
<el-row :gutter="24"> <el-row :gutter="24">
<el-col v-for="field in formFields" :key="field.prop" :span="field.span || 6"> <el-col v-for="field in visibleFormFields" :key="field.prop" :span="field.span || 6">
<el-form-item :label="field.label" :prop="field.prop"> <el-form-item :label="field.label" :prop="field.prop">
<el-input <el-input
v-if="field.type === 'contract'" v-if="field.type === 'contract'"
@@ -40,9 +40,9 @@
type="date" type="date"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
format="YYYY-MM-DD" format="YYYY-MM-DD"
:disabled="!editable || form.currency === 'RMB'" :disabled="!editable"
placeholder="请选择" placeholder="请选择"
@change="recalculateLocalAmount" @change="handleExchangeRateDateChange"
/> />
<el-input-number <el-input-number
v-else-if="field.type === 'number'" v-else-if="field.type === 'number'"
@@ -50,7 +50,7 @@
:min="0" :min="0"
:precision="2" :precision="2"
:controls="false" :controls="false"
:disabled="!editable || form.currency === 'RMB'" :disabled="!editable"
@change="recalculateLocalAmount" @change="recalculateLocalAmount"
/> />
<el-input <el-input
@@ -66,7 +66,11 @@
<span v-else-if="field.prop === 'settlementType'" class="form-readonly"> <span v-else-if="field.prop === 'settlementType'" class="form-readonly">
{{ settlementTypeName }} {{ settlementTypeName }}
</span> </span>
<span v-else-if="field.prop === 'preSettlementNo'" class="form-readonly"> <span
v-else-if="field.prop === 'preSettlementNo'"
class="form-readonly"
:class="{ 'form-readonly--code': form.preSettlementNo }"
>
{{ form.preSettlementNo || (!form.id ? '系统自动生成' : '-') }} {{ form.preSettlementNo || (!form.id ? '系统自动生成' : '-') }}
</span> </span>
<span v-else-if="field.prop === 'createTime'" class="form-readonly"> <span v-else-if="field.prop === 'createTime'" class="form-readonly">
@@ -243,60 +247,52 @@
:index="detailIndexMethod" :index="detailIndexMethod"
/> />
<el-table-column <el-table-column
v-for="column in visibleDetailColumns" v-for="item in detailDisplayColumns"
:key="column.prop" :key="item.key"
:label="column.label" :label="item.kind === 'fee' ? item.name : item.column.label"
:prop="column.prop" :prop="item.kind === 'fee' ? undefined : item.column.prop"
:min-width="column.minWidth" :min-width="item.kind === 'fee' ? 130 : item.column.minWidth"
align="center" align="center"
show-overflow-tooltip show-overflow-tooltip
> >
<template #default="{ row }"> <template #default="{ row }">
<span v-if="item.kind === 'fee'">
{{
formatMoney(
parseFeeItems(row.feeItemsJson)[item.name],
row.currency || form.currency
)
}}
</span>
<el-link <el-link
v-if="column.link && row[column.prop]" v-else-if="item.column.link && row[item.column.prop] && !publicMode"
type="primary" type="primary"
@click="openAdjustDialog(row, true)" @click.stop="handleDetailLink(row, item.column)"
> >
{{ row[column.prop] }} {{ row[item.column.prop] }}
</el-link> </el-link>
<span v-else-if="column.money && column.prop === 'unitPrice'"> <span v-else-if="item.column.money && item.column.prop === 'unitPrice'">
{{ formatUnitPrice(row) }} {{ formatUnitPrice(row) }}
</span> </span>
<span v-else-if="column.money"> <span v-else-if="item.column.money">
{{ formatMoney(row[column.prop], row.currency || form.currency) }} {{ formatMoney(row[item.column.prop], row.currency || form.currency) }}
</span> </span>
<span v-else-if="column.prop === 'transportQuantityText'"> <span v-else-if="item.column.prop === 'transportQuantityText'">
{{ formatQuantity(row) }} {{ formatQuantity(row) }}
</span> </span>
<span v-else-if="pageMode && column.prop === 'mileage'"> <span v-else-if="item.column.prop === 'mileage'">
{{ formatDetailMileage(row[column.prop]) }} {{ formatDetailMileage(row[item.column.prop]) }}
</span> </span>
<span v-else-if="column.prop === 'transportType'"> <span v-else-if="item.column.prop === 'transportType'">
{{ transportTypeName(row[column.prop]) }} {{ transportTypeName(row[item.column.prop]) }}
</span> </span>
<span v-else-if="column.prop === 'cargoName'"> <span v-else-if="item.column.prop === 'cargoName'">
{{ formatCargoNames(row) }} {{ formatCargoNames(row) }}
</span> </span>
<span v-else-if="column.prop === 'cargoType'"> <span v-else-if="item.column.prop === 'cargoType'">
{{ formatCargoTypes(row) }} {{ formatCargoTypes(row) }}
</span> </span>
<span v-else>{{ displayValue(row[column.prop]) }}</span> <span v-else>{{ displayValue(row[item.column.prop]) }}</span>
</template>
</el-table-column>
<el-table-column
v-for="feeItem in detailFeeItemNames"
:key="`fee-${feeItem}`"
:label="feeItem"
min-width="130"
align="center"
>
<template #default="{ row }">
{{
formatMoney(
parseFeeItems(row.feeItemsJson)[feeItem],
row.currency || form.currency
)
}}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" :width="editable ? 150 : 90" fixed="right" align="center"> <el-table-column label="操作" :width="editable ? 150 : 90" fixed="right" align="center">
@@ -421,6 +417,16 @@
</div> </div>
</section-card> </section-card>
<project-advance-write-off
ref="projectAdvanceWriteOffRef"
v-model="projectAdvanceWriteOffs"
:editable="editable"
:contract-id="form.contractId"
:settlement-amount="form.settlementAmount"
settlement-type="pre"
:settlement-id="form.id"
/>
<section-card v-if="form.id && readonly" title="预付信息"> <section-card v-if="form.id && readonly" title="预付信息">
<el-table :data="advances" border> <el-table :data="advances" border>
<el-table-column type="index" label="序号" width="64" align="center" /> <el-table-column type="index" label="序号" width="64" align="center" />
@@ -776,12 +782,13 @@
</el-table-column> </el-table-column>
<el-table-column label="里程(KM)" min-width="140" align="center"> <el-table-column label="里程(KM)" min-width="140" align="center">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="adjustDialog.readonly">{{ formatDetailMileage(row.mileage) }}</span>
<el-input-number <el-input-number
v-else
v-model="row.mileage" v-model="row.mileage"
:min="0" :min="0"
:precision="2" :precision="2"
:controls="false" :controls="false"
:disabled="adjustDialog.readonly"
@change="recalculateAdjustRow(row, 'mileage')" @change="recalculateAdjustRow(row, 'mileage')"
/> />
</template> </template>
@@ -939,19 +946,28 @@ import { h } from 'vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { InfoFilled } from '@element-plus/icons-vue'; import { InfoFilled } from '@element-plus/icons-vue';
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue'; import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
import ProjectAdvanceWriteOff from './project-advance-write-off.vue';
import { getMkPublicDetail } from '@/api/mk-process'; import { getMkPublicDetail } from '@/api/mk-process';
import { import {
adjustDetail, adjustDetail,
getCandidateDetails, getCandidateDetails,
getContractList, getContractList,
getContractOptions,
getDetail, getDetail,
getDetailFees, getDetailFees,
getFeeOptions, getFeeOptions,
getNextNo,
removeDetail, removeDetail,
save, save,
submit, submit,
} from '@/api/settlement/preSettlement'; } from '@/api/settlement/preSettlement';
import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail'; import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail';
import {
resolveReceivablePayableDetailId,
resolveWaybillId,
settlementDetailPath,
} from '@/utils/settlement-detail-link';
import { getBfiExchangeRate } from '@/api/base/currency';
import { getDictionary } from '@/api/system/dictbiz'; import { getDictionary } from '@/api/system/dictbiz';
import { import {
emptyPreSettlementForm, emptyPreSettlementForm,
@@ -973,14 +989,18 @@ import {
settlementSummaryColumns, settlementSummaryColumns,
} from '@/option/settlement/preSettlementTable'; } from '@/option/settlement/preSettlementTable';
import { formatContractOptionLabel } from '@/utils/contract'; import { formatContractOptionLabel } from '@/utils/contract';
import { normalizeCurrencyCode } from '@/utils/currency';
import { downloadFileByUrl } from '@/utils/util'; import { downloadFileByUrl } from '@/utils/util';
import { submitMkApprovalFlow } from '@/utils/mk-approval'; import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { isAdministrator } from '@/utils/auth'; import { isAdministrator } from '@/utils/auth';
// 结算币种为本位币(人民币)时汇率恒为1,汇率日期、结算汇率与按汇率换算的本位币合计都不展示
const LOCAL_CURRENCY_HIDDEN_FIELDS = ['exchangeRateDate', 'exchangeRate', 'localSettlementAmount'];
export default { export default {
name: 'PreSettlementEditor', name: 'PreSettlementEditor',
components: { InfoFilled, ChangeRecordDetailDialog }, components: { InfoFilled, ChangeRecordDetailDialog, ProjectAdvanceWriteOff },
props: { props: {
modelValue: { modelValue: {
type: Boolean, type: Boolean,
@@ -1083,6 +1103,7 @@ export default {
size: 10, size: 10,
}, },
advances: [], advances: [],
projectAdvanceWriteOffs: [],
changeRecords: [], changeRecords: [],
attachments: [], attachments: [],
attachmentTypeOptions: [], attachmentTypeOptions: [],
@@ -1187,6 +1208,14 @@ export default {
settlementTypeName() { settlementTypeName() {
return this.form.settlementType === 'receivable' ? '应收' : '应付'; return this.form.settlementType === 'receivable' ? '应收' : '应付';
}, },
// 币种为空按人民币处理,与后端 normalizeRate 的取默认值一致
isLocalCurrencySettlement() {
return normalizeCurrencyCode(this.form.currency) === 'RMB';
},
visibleFormFields() {
if (!this.isLocalCurrencySettlement) return this.formFields;
return this.formFields.filter(field => !LOCAL_CURRENCY_HIDDEN_FIELDS.includes(field.prop));
},
contractDialogCategory() { contractDialogCategory() {
return '承运商合同'; return '承运商合同';
}, },
@@ -1208,6 +1237,17 @@ export default {
}); });
return Array.from(names); return Array.from(names);
}, },
detailDisplayColumns() {
const columns = this.visibleDetailColumns;
const unitPriceIndex = columns.findIndex(column => column.prop === 'unitPrice');
const beforeColumns = unitPriceIndex < 0 ? columns : columns.slice(0, unitPriceIndex + 1);
const afterColumns = unitPriceIndex < 0 ? [] : columns.slice(unitPriceIndex + 1);
return [
...beforeColumns.map(column => ({ kind: 'field', key: column.prop, column })),
...this.detailFeeItemNames.map(name => ({ kind: 'fee', key: `fee-${name}`, name })),
...afterColumns.map(column => ({ kind: 'field', key: column.prop, column })),
];
},
// 按 preSettlementDetailId 分组的明细数据(用于拼接货物名称和类型) // 按 preSettlementDetailId 分组的明细数据(用于拼接货物名称和类型)
groupedDetails() { groupedDetails() {
const groups = {}; const groups = {};
@@ -1282,6 +1322,11 @@ export default {
}, },
async initialize() { async initialize() {
this.resetEditor(); this.resetEditor();
// 新增页在保存前没有创建人/创建日期,先按当前登录用户与当前时间展示将要落库的值
if (!this.recordId) {
this.form.createUserName = this.userInfo?.realName || this.userInfo?.userName || '';
this.form.createTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
}
if (!this.publicMode) { if (!this.publicMode) {
await this.loadFeeOptions(); await this.loadFeeOptions();
await this.loadFeeCategoryOptions(); await this.loadFeeCategoryOptions();
@@ -1289,7 +1334,19 @@ export default {
await this.loadAttachmentTypeOptions(); await this.loadAttachmentTypeOptions();
} }
if (this.recordId) await this.loadDetail(); if (this.recordId) await this.loadDetail();
else if (this.initialData) await this.applyInitialData(); else if (this.initialData) {
await this.applyInitialData();
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
await this.refreshPreSettlementNo();
await this.fetchBfiExchangeRate();
} else if (!this.publicMode) {
await this.refreshPreSettlementNo();
}
},
async refreshPreSettlementNo() {
if (this.form.id) return;
const response = await getNextNo();
this.form.preSettlementNo = response.data?.data || '';
}, },
async applyInitialData() { async applyInitialData() {
const rows = Array.isArray(this.initialData?.rows) ? this.initialData.rows : []; const rows = Array.isArray(this.initialData?.rows) ? this.initialData.rows : [];
@@ -1297,6 +1354,19 @@ export default {
const first = rows[0]; const first = rows[0];
const settlementType = this.initialData.settlementType || first.settlementType || 'payable'; const settlementType = this.initialData.settlementType || first.settlementType || 'payable';
const contractId = first.contractId || ''; const contractId = first.contractId || '';
let contractPartyA = first.payerName || '';
let contractPartyB = first.payeeName || '';
if (contractId && (!contractPartyA || !contractPartyB)) {
try {
const { data } = await getContractOptions(first.contractNo || '');
const matched = (data?.data || []).find(item => String(item.id) === String(contractId));
contractPartyA = contractPartyA || matched?.partyA || '';
contractPartyB = contractPartyB || matched?.partyB || '';
} catch (error) {
contractPartyA = '';
contractPartyB = '';
}
}
if (!this.contractOptions.some(item => String(item.id) === String(contractId))) { if (!this.contractOptions.some(item => String(item.id) === String(contractId))) {
this.contractOptions.push({ this.contractOptions.push({
id: contractId, id: contractId,
@@ -1307,8 +1377,8 @@ export default {
deptId: first.deptId, deptId: first.deptId,
deptName: first.deptName, deptName: first.deptName,
settlementType, settlementType,
partyA: first.payerName, partyA: contractPartyA,
partyB: first.payeeName, partyB: contractPartyB,
}); });
} }
this.handleContractChange(contractId); this.handleContractChange(contractId);
@@ -1320,8 +1390,8 @@ export default {
projectName: first.projectName || this.form.projectName, projectName: first.projectName || this.form.projectName,
deptId: first.deptId || this.form.deptId, deptId: first.deptId || this.form.deptId,
deptName: first.deptName || this.form.deptName, deptName: first.deptName || this.form.deptName,
payerName: first.payerName || this.form.payerName, payerName: this.form.payerName || contractPartyA,
payeeName: first.payeeName || this.form.payeeName, payeeName: this.form.payeeName || contractPartyB,
settlementType, settlementType,
currency: first.currency || 'RMB', currency: first.currency || 'RMB',
localCurrency: first.localCurrency || 'RMB', localCurrency: first.localCurrency || 'RMB',
@@ -1391,6 +1461,7 @@ export default {
this.appliedDetailQuery = { ...this.detailQuery }; this.appliedDetailQuery = { ...this.detailQuery };
this.detailPage = { current: 1, size: 10 }; this.detailPage = { current: 1, size: 10 };
this.advances = []; this.advances = [];
this.projectAdvanceWriteOffs = [];
this.changeRecords = []; this.changeRecords = [];
this.attachments = []; this.attachments = [];
this.selectedAttachmentRows = []; this.selectedAttachmentRows = [];
@@ -1430,6 +1501,13 @@ export default {
createUserName: row.createUserName || detail.createUserName, createUserName: row.createUserName || detail.createUserName,
createTime: row.createTime || detail.createTime, createTime: row.createTime || detail.createTime,
})); }));
this.projectAdvanceWriteOffs = (detail.projectAdvanceWriteOffs || []).map(row => ({
...row,
writeOffAmount: Number(row.writeOffAmount || 0),
projectAdvanceAmount: Number(row.projectAdvanceAmount || 0),
availableWriteOffAmount: Number(row.availableWriteOffAmount || 0),
createTime: row.paymentCreateTime || row.createTime,
}));
this.changeRecords = detail.changeRecords || []; this.changeRecords = detail.changeRecords || [];
this.attachments = this.parseAttachments(detail.attachmentsJson); this.attachments = this.parseAttachments(detail.attachmentsJson);
this.sortAttachments(); this.sortAttachments();
@@ -1574,6 +1652,7 @@ export default {
this.form.payerName = ''; this.form.payerName = '';
this.form.payeeName = ''; this.form.payeeName = '';
this.summaryFees = []; this.summaryFees = [];
this.projectAdvanceWriteOffs = [];
return; return;
} }
this.form.contractId = contract.id; this.form.contractId = contract.id;
@@ -1584,9 +1663,16 @@ export default {
this.form.deptId = contract.deptId; this.form.deptId = contract.deptId;
this.form.deptName = contract.deptName; this.form.deptName = contract.deptName;
this.form.settlementType = contract.settlementType || 'payable'; this.form.settlementType = contract.settlementType || 'payable';
this.form.payerName = contract.partyA; // 部分候选合同只带 id/名称,缺甲方乙方;此时保留已带出的收付方,不要清成 undefined
this.form.payeeName = contract.partyB; if (contract.partyA) this.form.payerName = contract.partyA;
if (contract.partyB) this.form.payeeName = contract.partyB;
this.form.currency = contract.settlementCurrency || 'RMB';
this.summaryFees = []; this.summaryFees = [];
this.projectAdvanceWriteOffs = [];
if (this.form.currency !== 'RMB') {
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
this.fetchBfiExchangeRate();
}
}, },
async saveDraft(shouldSubmit) { async saveDraft(shouldSubmit) {
await this.$refs.formRef?.validate(); await this.$refs.formRef?.validate();
@@ -1603,6 +1689,9 @@ export default {
this.$message.warning(`请完善结算合计第${invalidManualFeeIndex + 1}行的费用类型和费用项`); this.$message.warning(`请完善结算合计第${invalidManualFeeIndex + 1}行的费用类型和费用项`);
return; return;
} }
if (this.$refs.projectAdvanceWriteOffRef?.validateBeforeSave?.() === false) {
return;
}
const stateKey = shouldSubmit ? 'submitting' : 'saving'; const stateKey = shouldSubmit ? 'submitting' : 'saving';
this[stateKey] = true; this[stateKey] = true;
try { try {
@@ -1613,13 +1702,14 @@ export default {
if (shouldSubmit) { if (shouldSubmit) {
const previousApprovalStatus = this.form.approvalStatus || ''; const previousApprovalStatus = this.form.approvalStatus || '';
await submit({ id: this.form.id }); await submit({ id: this.form.id });
await submitMkApprovalFlow({ const mkStarted = await submitMkApprovalFlow({
bizType: 'pre-settlement', bizType: 'pre-settlement',
bizId: this.form.id, bizId: this.form.id,
subjectName: this.form.preSettlementNo || this.form.contractName || '', subjectName: this.form.preSettlementNo || this.form.contractName || '',
approvalStatus: previousApprovalStatus, approvalStatus: previousApprovalStatus,
mkEnabled: await loadMkEnabled().catch(() => false),
}); });
this.$message.success('审批流程已发起'); this.$message.success(mkStarted ? '审批流程已发起' : '提交成功');
this.visible = false; this.visible = false;
this.$emit('success', this.form.id); this.$emit('success', this.form.id);
return; return;
@@ -1649,6 +1739,7 @@ export default {
remark: row.remark, remark: row.remark,
manualFlag: row.manualFlag || 0, manualFlag: row.manualFlag || 0,
})), })),
projectAdvanceWriteOffs: this.$refs.projectAdvanceWriteOffRef?.toSavePayload?.() || [],
}; };
}, },
addSummaryFee() { addSummaryFee() {
@@ -1705,7 +1796,7 @@ export default {
return option?.dictValue || value; return option?.dictValue || value;
}, },
formatDetailMileage(value) { formatDetailMileage(value) {
if (value === undefined || value === null || value === '' || Number(value) === -1) return ''; if (value === undefined || value === null || value === '' || Number(value) === -1) return '-';
return value; return value;
}, },
removeSummaryFee(index) { removeSummaryFee(index) {
@@ -1861,6 +1952,26 @@ export default {
.toFixed(2) .toFixed(2)
); );
}, },
handleExchangeRateDateChange() {
this.fetchBfiExchangeRate();
this.recalculateLocalAmount();
},
async fetchBfiExchangeRate() {
if (!this.form.currency || this.form.currency === 'RMB') return;
if (!this.form.exchangeRateDate) return;
try {
const { data } = await getBfiExchangeRate(this.form.currency, this.form.exchangeRateDate);
const rateData = data?.data;
if (rateData?.excval != null) {
this.form.exchangeRate = Number(rateData.excval);
this.recalculateLocalAmount();
} else {
this.$message.warning(`未查询到 ${this.form.currency} 的BFI汇率数据`);
}
} catch (e) {
this.$message.warning('查询BFI汇率失败,请手动输入结算汇率');
}
},
recalculateLocalAmount() { recalculateLocalAmount() {
const rate = this.form.currency === 'RMB' ? 1 : Number(this.form.exchangeRate || 0); const rate = this.form.currency === 'RMB' ? 1 : Number(this.form.exchangeRate || 0);
this.form.localSettlementAmount = (Number(this.form.settlementAmount || 0) * rate).toFixed(2); this.form.localSettlementAmount = (Number(this.form.settlementAmount || 0) * rate).toFixed(2);
@@ -1957,6 +2068,21 @@ export default {
this.appliedDetailQuery = { ...this.detailQuery }; this.appliedDetailQuery = { ...this.detailQuery };
this.detailPage.current = 1; this.detailPage.current = 1;
}, },
// 结算明细的「单据号」跳到该应收应付单据在应收明细/应付明细页的详情,「运单号」跳到运单详情
async handleDetailLink(row, column) {
if (column.prop === 'documentNo') {
const settlementType = row.settlementType || this.form.settlementType || 'payable';
const detailId = await resolveReceivablePayableDetailId(row, settlementType);
if (!detailId) return this.$message.info('当前记录未找到对应的应收/应付明细');
this.$router.push({ path: settlementDetailPath(settlementType), query: { detailId } });
return;
}
if (column.prop === 'waybillNo') {
const waybillId = await resolveWaybillId(row);
if (!waybillId) return this.$message.info('当前记录未找到对应的运单详情');
this.$router.push({ path: '/business/waybill-manage/detail', query: { id: waybillId } });
}
},
resetDetailQuery() { resetDetailQuery() {
this.detailQuery = { this.detailQuery = {
documentNo: '', documentNo: '',
@@ -2962,6 +3088,10 @@ export default {
line-height: 32px; line-height: 32px;
color: #303133; color: #303133;
} }
.form-readonly--code {
color: var(--el-color-primary);
}
} }
.dialog-section-title { .dialog-section-title {
@@ -0,0 +1,300 @@
<template>
<section-card title="项目预付核销">
<template v-if="editable" #extra>
<el-button type="primary" :disabled="!contractId" @click="openSelectDialog">选择</el-button>
</template>
<el-table :data="rows" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column
prop="paymentNo"
label="预付单号"
min-width="160"
align="center"
show-overflow-tooltip
/>
<el-table-column label="项目预付金额" min-width="140" align="center">
<template #default="{ row }">{{ formatMoney(row.projectAdvanceAmount) }}</template>
</el-table-column>
<el-table-column label="可核销金额" min-width="140" align="center">
<template #default="{ row }">{{ formatMoney(row.availableWriteOffAmount) }}</template>
</el-table-column>
<el-table-column label="本次核销金额" min-width="160" align="center">
<template #default="{ row }">
<el-input-number
v-if="editable"
v-model="row.writeOffAmount"
:min="0"
:precision="2"
:controls="false"
class="project-advance-write-off__amount"
@change="handleAmountChange(row)"
/>
<span v-else>{{ formatMoney(row.writeOffAmount) }}</span>
</template>
</el-table-column>
<el-table-column
prop="kingdeeBillNo"
label="金蝶预付单号"
min-width="160"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">{{ row.kingdeeBillNo || '-' }}</template>
</el-table-column>
<el-table-column
prop="createUserName"
label="创建人"
min-width="120"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">{{ row.createUserName || '-' }}</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center">
<template #default="{ row }">{{ formatDateTime(row.createTime || row.paymentCreateTime) }}</template>
</el-table-column>
<el-table-column v-if="editable" label="操作" width="90" align="center" fixed="right">
<template #default="{ $index }">
<el-link type="danger" @click="removeRow($index)">删除</el-link>
</template>
</el-table-column>
</el-table>
<el-dialog
v-model="dialog.visible"
title="选择项目预付单"
width="960px"
append-to-body
destroy-on-close
class="dialog-form-title"
@closed="handleDialogClosed"
>
<el-table
v-loading="dialog.loading"
:data="dialog.rows"
border
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" :selectable="rowSelectable" />
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column
prop="paymentNo"
label="预付单号"
min-width="160"
align="center"
show-overflow-tooltip
/>
<el-table-column label="项目预付金额" min-width="140" align="center">
<template #default="{ row }">{{ formatMoney(row.projectAdvanceAmount) }}</template>
</el-table-column>
<el-table-column label="可核销金额" min-width="140" align="center">
<template #default="{ row }">{{ formatMoney(row.availableWriteOffAmount) }}</template>
</el-table-column>
<el-table-column
prop="kingdeeBillNo"
label="金蝶预付单号"
min-width="160"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">{{ row.kingdeeBillNo || '-' }}</template>
</el-table-column>
<el-table-column
prop="createUserName"
label="创建人"
min-width="120"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">{{ row.createUserName || '-' }}</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center">
<template #default="{ row }">{{ formatDateTime(row.createTime) }}</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="dialog.visible = false">取消</el-button>
<el-button type="primary" :disabled="!dialog.selection.length" @click="confirmSelect">
确定
</el-button>
</template>
</el-dialog>
</section-card>
</template>
<script>
import { getProjectAdvanceWriteOffOptions } from '@/api/payment/paymentApplication';
export default {
name: 'ProjectAdvanceWriteOff',
props: {
modelValue: {
type: Array,
default: () => [],
},
editable: {
type: Boolean,
default: true,
},
contractId: {
type: [String, Number],
default: '',
},
settlementAmount: {
type: [String, Number],
default: 0,
},
settlementType: {
type: String,
required: true,
},
settlementId: {
type: [String, Number],
default: '',
},
},
emits: ['update:modelValue'],
data() {
return {
dialog: {
visible: false,
loading: false,
rows: [],
selection: [],
},
};
},
computed: {
rows: {
get() {
return Array.isArray(this.modelValue) ? this.modelValue : [];
},
set(value) {
this.$emit('update:modelValue', value);
},
},
},
methods: {
formatMoney(value) {
const amount = Number(value);
return Number.isFinite(amount) ? amount.toFixed(2) : '0.00';
},
formatDateTime(value) {
if (!value) return '-';
return this.$dayjs(value).format('YYYY-MM-DD HH:mm:ss');
},
money(value) {
const amount = Number(value);
return Number.isFinite(amount) ? Number(amount.toFixed(2)) : 0;
},
openSelectDialog() {
if (!this.contractId) {
this.$message.warning('请先选择合同');
return;
}
this.dialog.visible = true;
this.loadOptions();
},
async loadOptions() {
this.dialog.loading = true;
try {
const res = await getProjectAdvanceWriteOffOptions({
contractId: this.contractId,
excludeSettlementType: this.settlementType,
excludeSettlementId: this.settlementId || undefined,
});
this.dialog.rows = res?.data?.data || [];
} finally {
this.dialog.loading = false;
}
},
rowSelectable(row) {
const selectedIds = new Set(this.rows.map(item => String(item.paymentApplicationId)));
return !selectedIds.has(String(row.paymentApplicationId));
},
handleSelectionChange(list) {
this.dialog.selection = list || [];
},
confirmSelect() {
const nextRows = [...this.rows];
const existed = new Set(nextRows.map(item => String(item.paymentApplicationId)));
this.dialog.selection.forEach(item => {
const id = String(item.paymentApplicationId);
if (existed.has(id)) return;
nextRows.push({
paymentApplicationId: item.paymentApplicationId,
paymentNo: item.paymentNo,
projectAdvanceAmount: this.money(item.projectAdvanceAmount),
availableWriteOffAmount: this.money(item.availableWriteOffAmount),
writeOffAmount: this.money(item.availableWriteOffAmount),
kingdeeBillNo: item.kingdeeBillNo || '',
createUserName: item.createUserName || '',
createTime: item.createTime,
paymentCreateTime: item.createTime,
});
});
this.rows = nextRows;
this.dialog.visible = false;
this.validateTotal();
},
handleDialogClosed() {
this.dialog.rows = [];
this.dialog.selection = [];
},
handleAmountChange(row) {
const available = this.money(row.availableWriteOffAmount);
let amount = this.money(row.writeOffAmount);
if (amount < 0) amount = 0;
if (amount > available) {
amount = available;
this.$message.warning('本次核销金额不能超过可核销金额');
}
row.writeOffAmount = amount;
this.validateTotal();
},
validateTotal() {
const total = this.rows.reduce((sum, row) => sum + this.money(row.writeOffAmount), 0);
const settlementAmount = this.money(this.settlementAmount);
if (settlementAmount > 0 && total > settlementAmount) {
this.$message.warning('项目预付核销合计不能超过结算单金额');
return false;
}
return true;
},
validateBeforeSave() {
for (const row of this.rows) {
const amount = this.money(row.writeOffAmount);
if (amount <= 0) {
this.$message.warning(`预付单${row.paymentNo || ''}本次核销金额必须大于0`);
return false;
}
if (amount > this.money(row.availableWriteOffAmount)) {
this.$message.warning(`预付单${row.paymentNo || ''}本次核销金额不能超过可核销金额`);
return false;
}
}
return this.validateTotal();
},
removeRow(index) {
const nextRows = [...this.rows];
nextRows.splice(index, 1);
this.rows = nextRows;
},
toSavePayload() {
return this.rows.map(row => ({
paymentApplicationId: row.paymentApplicationId,
writeOffAmount: this.money(row.writeOffAmount),
}));
},
},
};
</script>
<style scoped lang="scss">
.project-advance-write-off__amount {
width: 100%;
}
:deep(.project-advance-write-off__amount .el-input__inner) {
text-align: right;
}
</style>
File diff suppressed because it is too large Load Diff
+31 -6
View File
@@ -64,6 +64,7 @@
:loading="loading" :loading="loading"
:page="page" :page="page"
:has-permission="hasPermissionFn" :has-permission="hasPermissionFn"
:sync-loading="syncLoading"
:mk-enabled="mkEnabled" :mk-enabled="mkEnabled"
v-model:search-visible="searchVisible" v-model:search-visible="searchVisible"
@create="openCreate" @create="openCreate"
@@ -84,6 +85,7 @@
:loading="loading" :loading="loading"
:page="page" :page="page"
:has-permission="hasPermissionFn" :has-permission="hasPermissionFn"
:sync-loading="syncLoading"
:mk-enabled="mkEnabled" :mk-enabled="mkEnabled"
v-model:search-visible="searchVisible" v-model:search-visible="searchVisible"
@create="openCreate" @create="openCreate"
@@ -221,6 +223,7 @@ export default {
rows: [], rows: [],
selection: [], selection: [],
loading: false, loading: false,
syncLoading: false,
page: { current: 1, size: 10, total: 0 }, page: { current: 1, size: 10, total: 0 },
editor: { visible: false, id: null, readonly: false }, editor: { visible: false, id: null, readonly: false },
paymentDialog: { paymentDialog: {
@@ -365,6 +368,7 @@ export default {
bizId: row.id, bizId: row.id,
subjectName: row.formalSettlementNo || '', subjectName: row.formalSettlementNo || '',
approvalStatus: previousApprovalStatus, approvalStatus: previousApprovalStatus,
mkEnabled: this.mkEnabled,
}); });
this.$message.success('提交成功'); this.$message.success('提交成功');
this.loadTable(); this.loadTable();
@@ -393,8 +397,13 @@ export default {
'提示', '提示',
{ type: 'warning' } { type: 'warning' }
); );
await api.approve({ id: row.id }); const res = await api.approve({ id: row.id });
this.$message.success('审批通过'); // data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
if (res.data.data === false) {
this.$message.warning(res.data.msg);
} else {
this.$message.success(res.data.msg || '审批通过');
}
this.loadTable(); this.loadTable();
}, },
async handleReturn(row) { async handleReturn(row) {
@@ -421,12 +430,28 @@ export default {
return this.selection[0]; return this.selection[0];
}, },
async handleSync() { async handleSync() {
if (this.syncLoading) return;
const row = this.selectedOne('同步金蝶'); const row = this.selectedOne('同步金蝶');
if (!row) return; if (!row) return;
const response = await api.syncKingdee(row.id);
const data = this.unwrapData(response); this.syncLoading = true;
this.$message.success(`同步成功,金蝶单据号:${data}`); try {
this.loadTable(); const response = await api.syncKingdee(row.id);
const data = this.unwrapData(response);
this.$message.success(`同步成功,金蝶单据号:${data}`);
await this.loadTable();
// 附件联带同步提示:同步成功但附件未全部上传时追加 warning,重推即自动补传
const freshRow = (this.rows || []).find(item => String(item.id) === String(row.id));
if (freshRow && ['pending', 'partial'].includes(freshRow.attachmentSyncStatus)) {
this.$message({
type: 'warning',
message: '该单附件未全部上传,可重推补传',
duration: 8000,
});
}
} finally {
this.syncLoading = false;
}
}, },
async openPaymentDialog() { async openPaymentDialog() {
if (!this.selection.length) return this.$message.warning('请至少选择一条正式结算单'); if (!this.selection.length) return this.$message.warning('请至少选择一条正式结算单');
+1
View File
@@ -661,6 +661,7 @@ export default {
bizId: row.id, bizId: row.id,
subjectName: row.preSettlementNo || '', subjectName: row.preSettlementNo || '',
approvalStatus: previousApprovalStatus, approvalStatus: previousApprovalStatus,
mkEnabled: this.mkEnabled,
}); });
this.$message.success('提交成功'); this.$message.success('提交成功');
this.loadTable(); this.loadTable();
@@ -520,7 +520,7 @@
:index="transferIndexMethod" :index="transferIndexMethod"
/> />
<el-table-column <el-table-column
v-for="column in tableColumns.slice(0, 13)" v-for="column in transferTableColumns"
:key="column.prop" :key="column.prop"
:prop="column.prop" :prop="column.prop"
:label="column.label" :label="column.label"
@@ -1174,6 +1174,13 @@ export default {
columns.splice(totalIndex, 0, ...dynamicColumns); columns.splice(totalIndex, 0, ...dynamicColumns);
return columns; return columns;
}, },
// 批量转结算弹窗只展示前 13 列。预结算单号、正式结算单号正是本操作要生成的单号,
// 候选行上必为空,故不占列
transferTableColumns() {
return this.tableColumns
.filter(column => !['preSettlementNo', 'formalSettlementNo'].includes(column.prop))
.slice(0, 13);
},
columnSettingOptions() { columnSettingOptions() {
return this.displayTableColumns.map(column => ({ return this.displayTableColumns.map(column => ({
prop: column.prop, prop: column.prop,
@@ -1712,11 +1719,12 @@ export default {
this.$router.push({ path, query: { detailNo } }); this.$router.push({ path, query: { detailNo } });
}, },
async openRouteDetail(detailId = this.$route.query.detailId) { async openRouteDetail(detailId = this.$route.query.detailId) {
if ( // 结算明细点「单据号」会带 detailId 跳到本页;只有当前停在应收/应付明细页时才自动展开详情
!detailId || const routePath =
this.settlementType !== 'payable' || this.settlementType === 'payable'
this.$route.path !== '/settlement/payable-detail' ? '/settlement/payable-detail'
) { : '/settlement/receivable-detail';
if (!detailId || this.$route.path !== routePath) {
return; return;
} }
let row = this.rows.find(item => String(item.id) === String(detailId)); let row = this.rows.find(item => String(item.id) === String(detailId));
@@ -1729,7 +1737,9 @@ export default {
if (data.records?.[0]) row = this.decorateRow(data.records[0]); if (data.records?.[0]) row = this.decorateRow(data.records[0]);
} }
if (!row) { if (!row) {
this.$message.info('当前记录未找到对应的应付明细'); this.$message.info(
`当前记录未找到对应的${this.settlementType === 'payable' ? '应付' : '应收'}明细`
);
return; return;
} }
await this.openDetailDialog(row); await this.openDetailDialog(row);
@@ -2681,12 +2691,12 @@ export default {
return `${Number(value).toFixed(2)} ${currency}`; return `${Number(value).toFixed(2)} ${currency}`;
}, },
formatDetailCell(row, prop) { formatDetailCell(row, prop) {
if (prop === 'mileage' && Number(row?.[prop]) === -1) {
return '-';
}
if ( if (
prop === 'mileage' && prop === 'mileage' &&
(row?.[prop] === null || (row?.[prop] === null || row?.[prop] === undefined || row?.[prop] === '')
row?.[prop] === undefined ||
row?.[prop] === '' ||
Number(row[prop]) === -1)
) { ) {
return ''; return '';
} }
@@ -0,0 +1,49 @@
<template>
<basic-container class="settlement-import-form-page">
<div class="settlement-import-form-page__title">{{ pageTitle }}</div>
<settlement-import-page />
</basic-container>
</template>
<script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import SettlementImportPage from './components/settlement-import-page.vue';
const route = useRoute();
const pageTitle = computed(() => (route.query.id ? '编辑导入结算单' : '新增导入结算单'));
</script>
<style scoped lang="scss">
.settlement-import-form-page {
:deep(.basic-container__card) {
background: #fff;
}
:deep(.basic-container__card > .el-card__body) {
padding: 0;
background: #fff;
}
}
.settlement-import-form-page__title {
position: relative;
padding: 16px 24px 12px 36px;
border-bottom: 1px solid #eff1f7;
background: #fff;
color: #303133;
font-size: 18px;
font-weight: 600;
&::before {
position: absolute;
top: 16px;
left: 24px;
width: 4px;
height: 22px;
border-radius: 2px;
background: #409eff;
content: '';
}
}
</style>
+531
View File
@@ -0,0 +1,531 @@
<template>
<basic-container class="settlement-import-page">
<section v-show="searchVisible" class="settlement-import-page__search">
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
<div class="settlement-import-page__search-grid">
<el-form-item v-for="field in visibleSearchFields" :key="field.prop" :label="field.label">
<el-select
v-if="field.type === 'select'"
v-model="query[field.prop]"
clearable
filterable
allow-create
default-first-option
placeholder="请选择"
>
<el-option
v-for="item in field.options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
</el-form-item>
<div class="settlement-import-page__search-actions">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
{{ searchExpanded ? '收起' : '展开' }}
</el-button>
</div>
</div>
</el-form>
</section>
<section class="settlement-import-page__table-panel">
<div class="settlement-import-page__toolbar">
<div class="settlement-import-page__toolbar-left">
<el-button
v-if="hasPermission('settlement_import_add')"
type="primary"
@click="openCreate"
>
新增
</el-button>
</div>
</div>
<el-table v-loading="loading" :data="rows" border class="settlement-import-page__table">
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="batchNo" label="导入批次号" min-width="160">
<template #default="{ row }">
<el-link type="primary" @click="openDetail(row)">{{ row.batchNo || '-' }}</el-link>
</template>
</el-table-column>
<el-table-column prop="settlementTypeName" label="导入类型" min-width="100">
<template #default="{ row }">
{{ row.settlementTypeName || settlementTypeLabel(row.settlementType) }}
</template>
</el-table-column>
<el-table-column prop="settlementBillTypeName" label="结算单" min-width="110">
<template #default="{ row }">
{{ row.settlementBillTypeName || settlementBillTypeLabel(row.settlementBillType) }}
</template>
</el-table-column>
<el-table-column prop="projectName" label="项目" min-width="140" show-overflow-tooltip />
<el-table-column prop="customerName" label="客户" min-width="140" show-overflow-tooltip />
<el-table-column prop="carrierName" label="承运商" min-width="140" show-overflow-tooltip />
<el-table-column label="明细行数" width="100" align="center">
<template #default="{ row }">
{{ row.waybillCount ?? row.detailCount ?? 0 }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="170" />
<el-table-column prop="createUserName" label="创建人" min-width="120">
<template #default="{ row }">{{ row.createUserName || '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<div class="settlement-import-page__row-actions">
<el-link type="primary" @click="openDetail(row)">详情</el-link>
<el-link
v-if="row.importStatus !== 'voided' && hasPermission('settlement_import_void')"
type="danger"
@click="openVoid(row)"
>
作废
</el-link>
</div>
</template>
</el-table-column>
</el-table>
<div class="settlement-import-page__pagination">
<el-pagination
background
v-model:current-page="page.current"
v-model:page-size="page.size"
layout="total, sizes, prev, pager, next, jumper"
:page-sizes="[10, 20, 50, 100]"
:total="page.total"
@current-change="loadTable"
@size-change="handleSizeChange"
/>
</div>
</section>
<el-dialog v-model="detailVisible" title="导入结算单详情" width="640px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="导入批次号">{{ detail.batchNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="导入类型">
{{ detail.settlementTypeName || settlementTypeLabel(detail.settlementType) }}
</el-descriptions-item>
<el-descriptions-item label="结算单">
{{ detail.settlementBillTypeName || settlementBillTypeLabel(detail.settlementBillType) }}
</el-descriptions-item>
<el-descriptions-item label="运单类型">
{{ detail.waybillModeName || waybillModeLabel(detail.waybillMode) }}
</el-descriptions-item>
<el-descriptions-item label="项目">{{ detail.projectName || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户">{{ detail.customerName || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户合同">
{{ detail.customerContractName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="承运商">{{ detail.carrierName || '-' }}</el-descriptions-item>
<el-descriptions-item label="承运商合同">
{{ detail.carrierContractName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="明细行数">
{{ detail.waybillCount ?? detail.detailCount ?? 0 }}
</el-descriptions-item>
<el-descriptions-item label="导入状态">
{{ detail.importStatusName || detail.importStatus || '-' }}
</el-descriptions-item>
<el-descriptions-item label="创建人">{{ detail.createUserName || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detail.createTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detail.remark || '-' }}</el-descriptions-item>
<el-descriptions-item label="关联结算单" :span="2">
<el-link
v-if="detail.formalSettlementId"
type="primary"
@click="goFormalSettlement(detail)"
>
{{ detail.formalSettlementNo || detail.formalSettlementId }}
</el-link>
<el-link
v-else-if="detail.preSettlementId"
type="primary"
@click="goPreSettlement(detail)"
>
{{ detail.preSettlementNo || detail.preSettlementId }}
</el-link>
<span v-else>-</span>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
<el-button
v-if="detail.importStatus === 'draft'"
type="primary"
@click="editDraft(detail)"
>
编辑草稿
</el-button>
</template>
</el-dialog>
<el-dialog v-model="voidVisible" title="作废导入结算单" width="480px" append-to-body>
<el-form
ref="voidFormRef"
:model="voidForm"
:rules="voidRules"
label-position="right"
label-width="auto"
>
<el-form-item label="作废原因" prop="reason">
<el-input
v-model="voidForm.reason"
type="textarea"
:rows="4"
maxlength="200"
show-word-limit
placeholder="请输入作废原因"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="voidVisible = false">取消</el-button>
<el-button type="danger" :loading="voidLoading" @click="submitVoid">确认作废</el-button>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import * as api from '@/api/settlement/settlementImport';
const emptyQuery = () => ({
batchNo: '',
settlementType: '',
projectName: '',
customerName: '',
carrierName: '',
});
const SETTLEMENT_TYPE_OPTIONS = [
{ label: '全部', value: '' },
{ label: '应收正式结算单', value: 'receivable' },
{ label: '应付正式结算单', value: 'payable' },
];
const SETTLEMENT_IMPORT_LIST_REFRESH_KEY = 'settlement-import-list-refresh';
export default {
name: 'SettlementImport',
data() {
return {
ArrowDown,
ArrowUp,
searchVisible: true,
searchExpanded: false,
loading: false,
query: emptyQuery(),
rows: [],
page: { current: 1, size: 10, total: 0 },
batchNoOptions: [],
detailVisible: false,
detail: {},
voidVisible: false,
voidLoading: false,
voidForm: { id: null, reason: '' },
voidRules: {
reason: [
{ required: true, message: '请输入作废原因', trigger: 'blur' },
{ max: 200, message: '作废原因不能超过200个字', trigger: 'blur' },
],
},
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
const authority = this.userInfo?.authority || '';
return String(authority).includes('admin');
},
searchFields() {
return [
{
prop: 'batchNo',
label: '导入批次号',
type: 'select',
options: this.batchNoOptions,
},
{
prop: 'settlementType',
label: '导入类型',
type: 'select',
options: SETTLEMENT_TYPE_OPTIONS,
},
{ prop: 'projectName', label: '项目', type: 'input' },
{ prop: 'customerName', label: '客户', type: 'input' },
{ prop: 'carrierName', label: '承运商', type: 'input' },
];
},
visibleSearchFields() {
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
},
},
created() {
this.loadOptions();
this.loadTable();
},
activated() {
this.consumeListRefresh();
},
watch: {
$route(to, from) {
if (
to.path === '/settlement/settlement-import' &&
from?.path === '/settlement/settlement-import/form'
) {
this.consumeListRefresh();
}
},
},
methods: {
consumeListRefresh() {
if (sessionStorage.getItem(SETTLEMENT_IMPORT_LIST_REFRESH_KEY) !== '1') {
return;
}
sessionStorage.removeItem(SETTLEMENT_IMPORT_LIST_REFRESH_KEY);
this.page.current = 1;
this.loadOptions();
this.loadTable();
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission?.[code], false);
},
settlementTypeLabel(value) {
return (
SETTLEMENT_TYPE_OPTIONS.find(item => item.value === value)?.label || value || '-'
);
},
settlementBillTypeLabel(value) {
if (value === 'pre') return '预结算单';
return '正式结算单';
},
waybillModeLabel(value) {
if (value === 'history') return '历史运单';
return '新运单';
},
unwrapData(response) {
const body = response?.data || response || {};
return body?.data !== undefined ? body.data : body;
},
async loadOptions() {
try {
const response = await api.getOptions();
const options = this.unwrapData(response) || {};
const batchNos = Array.isArray(options.batchNos) ? options.batchNos : [];
this.batchNoOptions = [
{ label: '全部', value: '' },
...batchNos.map(item => {
if (typeof item === 'string') {
return { label: item, value: item };
}
const value = item.id ?? item.value ?? item.name ?? item.batchNo ?? '';
const label = item.name ?? item.label ?? item.batchNo ?? value;
return { label: String(label || ''), value: String(value || '') };
}),
];
} catch (error) {
this.batchNoOptions = [{ label: '全部', value: '' }];
}
},
async loadTable() {
this.loading = true;
try {
const response = await api.getList(this.page.current, this.page.size, {
batchNo: this.query.batchNo || undefined,
settlementType: this.query.settlementType || undefined,
projectName: this.query.projectName || undefined,
customerName: this.query.customerName || undefined,
carrierName: this.query.carrierName || undefined,
});
const data = this.unwrapData(response) || {};
this.rows = data.records || [];
this.page.total = Number(data.total || 0);
} finally {
this.loading = false;
}
},
handleSearch() {
this.page.current = 1;
this.loadTable();
},
resetSearch() {
this.query = emptyQuery();
this.handleSearch();
},
toggleSearch() {
this.searchExpanded = !this.searchExpanded;
},
handleSizeChange() {
this.page.current = 1;
this.loadTable();
},
openCreate() {
this.$router.push({ path: '/settlement/settlement-import/form' });
},
async openDetail(row) {
if (!row?.id) return;
try {
const response = await api.getDetail(row.id);
this.detail = this.unwrapData(response) || { ...row };
} catch (error) {
this.detail = { ...row };
}
this.detailVisible = true;
},
editDraft(row) {
this.detailVisible = false;
this.$router.push({
path: '/settlement/settlement-import/form',
query: { id: row.id },
});
},
goFormalSettlement(row) {
if (!row?.formalSettlementId) return;
this.detailVisible = false;
this.$router.push({
path: '/settlement/formal-settlement/form',
query: {
mode: 'view',
id: row.formalSettlementId,
name: '正式结算详情',
},
});
},
goPreSettlement(row) {
if (!row?.preSettlementId) return;
this.detailVisible = false;
this.$router.push({
path: '/settlement/pre-settlement/form',
query: {
mode: 'view',
id: row.preSettlementId,
name: '预结算详情',
},
});
},
openVoid(row) {
this.voidForm = { id: row.id, reason: '' };
this.voidVisible = true;
this.$nextTick(() => this.$refs.voidFormRef?.clearValidate());
},
async submitVoid() {
await this.$refs.voidFormRef?.validate();
this.voidLoading = true;
try {
await api.voidBatch({
id: this.voidForm.id,
reason: String(this.voidForm.reason || '').trim(),
});
this.$message.success('作废成功');
this.voidVisible = false;
this.loadTable();
this.loadOptions();
} finally {
this.voidLoading = false;
}
},
},
};
</script>
<style scoped lang="scss">
.settlement-import-page__search {
padding: 12px 12px 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.settlement-import-page__search-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px 24px;
align-items: start;
}
.settlement-import-page__search :deep(.el-form-item) {
margin-bottom: 8px;
}
.settlement-import-page__search :deep(.el-form-item__label) {
white-space: nowrap;
}
.settlement-import-page__search :deep(.el-input),
.settlement-import-page__search :deep(.el-select) {
width: 100%;
}
.settlement-import-page__search-actions {
grid-column: 1 / -1;
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.settlement-import-page__table-panel {
margin-top: 8px;
background: #fff;
}
.settlement-import-page__toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0 0;
}
.settlement-import-page__toolbar-left {
display: flex;
gap: 8px;
}
.settlement-import-page__table {
--el-table-border-color: #eff1f7;
}
.settlement-import-page__table :deep(.el-table__body tr:nth-child(even) > td.el-table__cell),
.settlement-import-page__table
:deep(.el-table__body tr:nth-child(even) > td.el-table-fixed-column--left),
.settlement-import-page__table
:deep(.el-table__body tr:nth-child(even) > td.el-table-fixed-column--right) {
background: #fafafa;
}
.settlement-import-page__row-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.settlement-import-page__pagination {
display: flex;
justify-content: flex-end;
padding: 12px 0;
}
.settlement-import-page :deep(.basic-container__card > .el-card__body) {
padding: 0;
}
@media (max-width: 1200px) {
.settlement-import-page__search-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 760px) {
.settlement-import-page__search-grid {
grid-template-columns: 1fr;
}
}
</style>
+9 -1
View File
@@ -165,6 +165,9 @@
<span class="oa-sync-stat__value oa-sync-stat__value--skip">{{ oaSyncProgress.skipped }}</span> <span class="oa-sync-stat__value oa-sync-stat__value--skip">{{ oaSyncProgress.skipped }}</span>
</div> </div>
</div> </div>
<div v-if="oaSyncProgress.dumpFilePath" class="oa-sync-meta">
原始数据 JSON:{{ oaSyncProgress.dumpFilePath }}
</div>
</div> </div>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
@@ -253,6 +256,7 @@ export default {
total: 0, total: 0,
synced: 0, synced: 0,
skipped: 0, skipped: 0,
dumpFilePath: '',
}, },
parentId: 0, parentId: 0,
page: { page: {
@@ -658,7 +662,7 @@ export default {
methods: { methods: {
handleOaOrgFullSync() { handleOaOrgFullSync() {
this.$confirm( this.$confirm(
'全量同步将拉取 OA 全部公司/部门;已存在且 OA 更新时间一致的记录会跳过。是否清除后同步?选择“是”将保留顶级「外部组织」与集团锚点(id=135/GWWL),删除其余组织后再同步;选择“否”则直接全量同步。', '全量同步将拉取 OA 公司/部门(仅保留二级公司 oaId=2、135 及其子公司与部门);已存在且 OA 更新时间一致的记录会跳过。是否清除后同步?选择“是”将保留顶级「外部组织」与集团锚点(id=1/GWWL),删除其余组织后再同步;选择“否”则直接全量同步。',
'全量同步', '全量同步',
{ {
confirmButtonText: '是', confirmButtonText: '是',
@@ -709,6 +713,7 @@ export default {
total: 0, total: 0,
synced: 0, synced: 0,
skipped: 0, skipped: 0,
dumpFilePath: '',
}; };
this.runOaOrgSyncPages(); this.runOaOrgSyncPages();
}, },
@@ -742,6 +747,9 @@ export default {
this.oaSyncProgress.total = page.total || 0; this.oaSyncProgress.total = page.total || 0;
this.oaSyncProgress.synced += page.syncedCount || 0; this.oaSyncProgress.synced += page.syncedCount || 0;
this.oaSyncProgress.skipped += page.skippedCount || 0; this.oaSyncProgress.skipped += page.skippedCount || 0;
if (page.dumpFilePath) {
this.oaSyncProgress.dumpFilePath = page.dumpFilePath;
}
if (page.finished) { if (page.finished) {
break; break;
} }
+8
View File
@@ -364,6 +364,9 @@
<span class="oa-sync-stat__value oa-sync-stat__value--skip">{{ oaSyncProgress.skipped }}</span> <span class="oa-sync-stat__value oa-sync-stat__value--skip">{{ oaSyncProgress.skipped }}</span>
</div> </div>
</div> </div>
<div v-if="oaSyncProgress.dumpFilePath" class="oa-sync-meta">
原始数据 JSON:{{ oaSyncProgress.dumpFilePath }}
</div>
</div> </div>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
@@ -450,6 +453,7 @@ export default {
total: 0, total: 0,
synced: 0, synced: 0,
skipped: 0, skipped: 0,
dumpFilePath: '',
}, },
page: { page: {
pageSize: 10, pageSize: 10,
@@ -712,6 +716,7 @@ export default {
total: 0, total: 0,
synced: 0, synced: 0,
skipped: 0, skipped: 0,
dumpFilePath: '',
}; };
this.runOaSyncPages(); this.runOaSyncPages();
}, },
@@ -746,6 +751,9 @@ export default {
this.oaSyncProgress.total = page.total || 0; this.oaSyncProgress.total = page.total || 0;
this.oaSyncProgress.synced += page.syncedCount || 0; this.oaSyncProgress.synced += page.syncedCount || 0;
this.oaSyncProgress.skipped += page.skippedCount || 0; this.oaSyncProgress.skipped += page.skippedCount || 0;
if (page.dumpFilePath) {
this.oaSyncProgress.dumpFilePath = page.dumpFilePath;
}
if (page.finished) { if (page.finished) {
this.oaSyncStatus = 'done'; this.oaSyncStatus = 'done';
break; break;
+2 -2
View File
@@ -158,7 +158,7 @@ import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util'; import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel'; import { openImportDialog } from '@/utils/import-excel';
import { getToken, isAdministrator } from '@/utils/auth'; import { getToken, isAdministrator } from '@/utils/auth';
import { normalizeSearchRangeParams } from '@/utils/search-range'; import { applyIncomingSearch, normalizeSearchRangeParams } from '@/utils/search-range';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue'; import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { excelOption, exportColumns, option } from '@/option/vehicle/accident-record'; import { excelOption, exportColumns, option } from '@/option/vehicle/accident-record';
@@ -550,7 +550,7 @@ export default {
}; };
}, },
buildQuery(params = {}) { buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap); return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
}, },
handleTemplate() { handleTemplate() {
exportBlob( exportBlob(
@@ -468,7 +468,11 @@ import {
publish, publish,
changeStatus, changeStatus,
} from '@/api/vehicle/credit-score-quantification'; } from '@/api/vehicle/credit-score-quantification';
import { normalizeSearchRangeParams } from '@/utils/search-range'; import {
applyIncomingSearch,
normalizeSearchRangeParams,
omitInvalidListStatus,
} from '@/utils/search-range';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { isAdministrator } from '@/utils/auth'; import { isAdministrator } from '@/utils/auth';
@@ -1324,7 +1328,9 @@ export default {
}); });
}, },
buildQuery(params = {}) { buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap); return omitInvalidListStatus(
normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap)
);
}, },
}, },
}; };
+285 -46
View File
@@ -109,6 +109,12 @@
{{ row.status === 1 ? '启用' : '停用' }} {{ row.status === 1 ? '启用' : '停用' }}
</el-tag> </el-tag>
</template> </template>
<template #kingdeeStatus="{ row }">
<el-tag v-if="row.kingdeeStatus === 'synced'" type="success">已推送</el-tag>
<el-tag v-else-if="row.kingdeeStatus === 'exist'" type="primary">金蝶已有</el-tag>
<el-tag v-else-if="row.kingdeeStatus === 'failed'" type="danger">推送失败</el-tag>
<el-tag v-else type="info">未推送</el-tag>
</template>
<template #menu="{ row }"> <template #menu="{ row }">
<el-link <el-link
type="primary" type="primary"
@@ -196,6 +202,18 @@
> >
{{ row.status === 1 ? '停用' : '启用' }} {{ row.status === 1 ? '停用' : '启用' }}
</el-link> </el-link>
<el-link
type="primary"
:disabled="kingdeeSyncingId === row.id"
v-if="
hasPermission('customer_archive_sync') &&
row.approvalStatus === 'approved' &&
!['synced', 'exist'].includes(row.kingdeeStatus)
"
@click="handleSyncKingdee(row)"
>
{{ kingdeeSyncingId === row.id ? '推送中…' : '同步金蝶' }}
</el-link>
<el-link <el-link
type="danger" type="danger"
v-if="hasPermission('customer_archive_delete') && row.approvalStatus === 'draft'" v-if="hasPermission('customer_archive_delete') && row.approvalStatus === 'draft'"
@@ -297,6 +315,7 @@
placeholder="请输入" placeholder="请输入"
maxlength="18" maxlength="18"
show-word-limit show-word-limit
:disabled="kingdeeLocked && Boolean(archiveForm.id)"
@input=" @input="
archiveForm.unifiedCreditCode = String( archiveForm.unifiedCreditCode = String(
archiveForm.unifiedCreditCode || '' archiveForm.unifiedCreditCode || ''
@@ -312,6 +331,7 @@
placeholder="请输入" placeholder="请输入"
maxlength="100" maxlength="100"
show-word-limit show-word-limit
:disabled="kingdeeLocked"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -335,7 +355,12 @@
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="企业法人" prop="legalPerson"> <el-form-item label="企业法人" prop="legalPerson">
<el-input v-model="archiveForm.legalPerson" maxlength="10" show-word-limit /> <el-input
v-model="archiveForm.legalPerson"
maxlength="10"
show-word-limit
:disabled="kingdeeLocked"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
@@ -461,7 +486,7 @@
:options="deptTree" :options="deptTree"
:props="deptCascaderProps" :props="deptCascaderProps"
disabled disabled
placeholder="桂物物流集团" placeholder="广西现代物流集团有限公司"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -476,6 +501,7 @@
<el-input <el-input
v-model="archiveForm.registeredCapital" v-model="archiveForm.registeredCapital"
maxlength="18" maxlength="18"
:disabled="kingdeeLocked"
@input="value => handleDecimalInput('registeredCapital', value)" @input="value => handleDecimalInput('registeredCapital', value)"
/> />
</el-form-item> </el-form-item>
@@ -1528,6 +1554,8 @@ import {
remove, remove,
getScoreTemplate, getScoreTemplate,
recognizeBusinessLicenseOcr, recognizeBusinessLicenseOcr,
queryKingdeeCustomer,
syncKingdee,
} from '@/api/vehicle/customer-archive'; } from '@/api/vehicle/customer-archive';
import { import {
getList as getCreditScoreQuantificationList, getList as getCreditScoreQuantificationList,
@@ -1560,7 +1588,7 @@ const customerArchiveListRoute = '/vehicle/customer-archive';
const customerArchiveFormRoute = '/vehicle/customer-archive/form'; const customerArchiveFormRoute = '/vehicle/customer-archive/form';
const customerArchiveListRefreshKey = 'customer-archive-list-refresh'; const customerArchiveListRefreshKey = 'customer-archive-list-refresh';
/** 客商所属组织固定为顶层组织 */ /** 客商所属组织固定为顶层组织 */
const FIXED_OWNER_DEPT_NAME = '桂物物流集团'; const FIXED_OWNER_DEPT_NAME = '广西现代物流集团有限公司';
const createTimeRangeMap = { const createTimeRangeMap = {
createTimeRange: ['createTimeStart', 'createTimeEnd'], createTimeRange: ['createTimeStart', 'createTimeEnd'],
@@ -1617,23 +1645,13 @@ export default {
const code = String(value || '') const code = String(value || '')
.trim() .trim()
.toUpperCase(); .toUpperCase();
const charset = '0123456789ABCDEFGHJKLMNPQRTUWXY';
const weights = [1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28];
if (!code) { if (!code) {
callback(); callback();
return; return;
} }
if (code.length !== 18 || ![...code].every(char => charset.includes(char))) { const error = this.unifiedCreditCodeError(code);
callback(new Error('统一社会信用代码必须为18位有效字符')); if (error) {
return; callback(new Error(error));
}
const sum = [...code.slice(0, 17)].reduce(
(total, char, index) => total + charset.indexOf(char) * weights[index],
0
);
const checkCode = charset[(31 - (sum % 31)) % 31];
if (code[17] !== checkCode) {
callback(new Error('统一社会信用代码校验码错误'));
return; return;
} }
callback(); callback();
@@ -1664,6 +1682,9 @@ export default {
originalAccessType: '', originalAccessType: '',
activeTab: 'scores', activeTab: 'scores',
archiveForm: this.emptyArchive(), archiveForm: this.emptyArchive(),
kingdeeBlocked: false,
kingdeeQueryTimer: null,
kingdeeSyncingId: null,
currentScore: { details: [] }, currentScore: { details: [] },
scoreRecordIndex: -1, scoreRecordIndex: -1,
scoreAttachmentFiles: [], scoreAttachmentFiles: [],
@@ -1770,6 +1791,8 @@ export default {
}, },
/** 业务字典 mk_template「开启MK」=1 */ /** 业务字典 mk_template「开启MK」=1 */
mkEnabled: false, mkEnabled: false,
/** 是否已完成 MK 开关加载,避免提交时误判 */
_mkEnabledResolved: false,
formRules: { formRules: {
customerCode: [{ max: 20, message: '客商编号最多20个字符', trigger: 'blur' }], customerCode: [{ max: 20, message: '客商编号最多20个字符', trigger: 'blur' }],
fullName: [{ required: true, message: '请输入客商名称', trigger: 'blur' }], fullName: [{ required: true, message: '请输入客商名称', trigger: 'blur' }],
@@ -1850,7 +1873,7 @@ export default {
editBtn: false, editBtn: false,
selection: true, selection: true,
dialogClickModal: false, dialogClickModal: false,
menuWidth: 320, menuWidth: 400,
column: [ column: [
{ {
label: '客商编号', label: '客商编号',
@@ -2006,6 +2029,12 @@ export default {
valueFormat: 'YYYY-MM-DD HH:mm:ss', valueFormat: 'YYYY-MM-DD HH:mm:ss',
minWidth: 170, minWidth: 170,
}, },
{
label: '金蝶同步状态',
prop: 'kingdeeStatus',
slot: true,
minWidth: 110,
},
{ {
label: '状态', label: '状态',
prop: 'status', prop: 'status',
@@ -2014,7 +2043,6 @@ export default {
search: true, search: true,
searchOrder: 6, searchOrder: 6,
searchValue: '', searchValue: '',
dataType: 'number',
minWidth: 100, minWidth: 100,
dicData: [ dicData: [
{ label: '全部', value: '' }, { label: '全部', value: '' },
@@ -2090,6 +2118,9 @@ export default {
const authority = this.userInfo.authority || ''; const authority = this.userInfo.authority || '';
return isAdministrator(this.userInfo); return isAdministrator(this.userInfo);
}, },
kingdeeLocked() {
return this.archiveForm.kingdeeStatus === 'exist';
},
permissionList() { permissionList() {
return { return {
addBtn: false, addBtn: false,
@@ -2295,6 +2326,9 @@ export default {
this.onLoad(this.page, this.query); this.onLoad(this.page, this.query);
} }
}, },
'archiveForm.unifiedCreditCode'(val) {
this.handleKingdeeCreditCodeChange(val);
},
'archiveForm.registeredDetailAddress'() { 'archiveForm.registeredDetailAddress'() {
this.$nextTick(() => this.checkOverflow('registeredDetailInput', 'registeredDetailOverflow')); this.$nextTick(() => this.checkOverflow('registeredDetailInput', 'registeredDetailOverflow'));
}, },
@@ -2307,7 +2341,13 @@ export default {
sessionStorage.setItem(customerArchiveListRefreshKey, '1'); sessionStorage.setItem(customerArchiveListRefreshKey, '1');
}, },
hasPermission(code) { hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false); if (this.isAdmin) {
return true;
}
const permission = this.permission || {};
const value = permission[code];
// 不依赖 Avue validData,避免权限值为 true 以外的真值时被误判
return value === true || value === 'true' || value === 1 || value === '1';
}, },
mergeSelectOptions(target, values) { mergeSelectOptions(target, values) {
const list = Array.isArray(values) ? values : values ? [values] : []; const list = Array.isArray(values) ? values : values ? [values] : [];
@@ -2348,6 +2388,119 @@ export default {
} }
}); });
}, },
unifiedCreditCodeError(value) {
const code = String(value || '')
.trim()
.toUpperCase();
const charset = '0123456789ABCDEFGHJKLMNPQRTUWXY';
const weights = [1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28];
if (code.length !== 18 || ![...code].every(char => charset.includes(char))) {
return '统一社会信用代码必须为18位有效字符';
}
const sum = [...code.slice(0, 17)].reduce(
(total, char, index) => total + charset.indexOf(char) * weights[index],
0
);
const checkCode = charset[(31 - (sum % 31)) % 31];
if (code[17] !== checkCode) {
return '统一社会信用代码校验码错误';
}
return '';
},
resetKingdeeQueryState() {
this.kingdeeBlocked = false;
clearTimeout(this.kingdeeQueryTimer);
this.kingdeeQueryTimer = null;
},
// 新增态下重新输入统一社会信用代码后,此前金蝶回填的数据即失效,重置回填状态并允许对新代码重新查询
resetKingdeeBackfill() {
const form = this.archiveForm;
form.kingdeeStatus = 'none';
form.fullName = '';
form.legalPerson = '';
form.registeredCapital = '';
form.registeredDetailAddress = '';
form.businessScope = [];
form.businessTermType = '固定期限';
form.businessEndDate = '';
},
// 新增态下统一社会信用代码录入完成后自动查询金蝶(A902-1),防抖 500ms;
// 代码一经变化即重新查询(含改回历史代码),不做同码去重
handleKingdeeCreditCodeChange(val) {
this.kingdeeBlocked = false;
if (this.readonly || this.archiveForm.id) return;
if (this.archiveForm.kingdeeStatus === 'exist') {
this.resetKingdeeBackfill();
}
const code = String(val || '')
.trim()
.toUpperCase();
if (code.length !== 18 || this.unifiedCreditCodeError(code)) return;
clearTimeout(this.kingdeeQueryTimer);
this.kingdeeQueryTimer = setTimeout(() => this.fetchKingdeeCustomer(code), 500);
},
async fetchKingdeeCustomer(code) {
let customer = null;
try {
const res = await queryKingdeeCustomer(code);
customer = res?.data?.data || null;
} catch (error) {
this.$message.warning('金蝶查询失败,可继续填写,审批通过后系统将自动复查');
return;
}
// 防止响应返回时用户已修改代码
if (String(this.archiveForm.unifiedCreditCode || '').trim().toUpperCase() !== code) return;
// 金蝶不存在(rows 为空,data 为 null 或空对象):静默返回,正常填写,不做提示
if (!customer || !customer.status) return;
if (customer.status === 'C') {
this.applyKingdeeCustomer(customer);
this.$alert(
'金蝶已创建该客户,将获取金蝶的工商信息,相关字段已自动回填并锁定。如信用代码有误,可直接修改,系统将重新查询。',
'提示',
{
confirmButtonText: '确定',
}
);
} else {
this.kingdeeBlocked = true;
this.$alert('金蝶审核中,请稍后重新添加。', '提示', { confirmButtonText: '确定' });
}
},
applyKingdeeCustomer(customer) {
const form = this.archiveForm;
form.kingdeeStatus = 'exist';
// 工商信息以金蝶为准,直接覆盖(后续字段锁定)
if (customer.name) form.fullName = customer.name;
if (customer.artificialperson) form.legalPerson = customer.artificialperson;
if (customer.bizpartnerAddress) form.registeredDetailAddress = customer.bizpartnerAddress;
if (customer.regcapital) {
const capital = String(customer.regcapital).replace(/,/g, '').match(/\d+(?:\.\d+)?/);
if (capital) form.registeredCapital = capital[0];
}
if (customer.businessterm) this.applyBusinessTermRecognition(customer.businessterm);
if (customer.businessscope) {
const matchedScopes = this.businessScopeOptions
.filter(
item =>
customer.businessscope.includes(item.label) ||
customer.businessscope.includes(item.value)
)
.map(item => item.value);
if (matchedScopes.length) form.businessScope = matchedScopes;
}
if (!form.shortName && customer.simplename) form.shortName = customer.simplename;
this.$nextTick(() => {
[
'unifiedCreditCode',
'fullName',
'legalPerson',
'registeredCapital',
'businessEndDate',
'businessScope',
'registeredDetailAddress',
].forEach(prop => this.$refs.archiveForm?.validateField(prop));
});
},
// 检测输入框文本是否溢出,仅溢出时才允许 tooltip 显示完整地址 // 检测输入框文本是否溢出,仅溢出时才允许 tooltip 显示完整地址
checkOverflow(refName, flag) { checkOverflow(refName, flag) {
const inst = this.$refs[refName]; const inst = this.$refs[refName];
@@ -2377,6 +2530,9 @@ export default {
invoices: [], invoices: [],
scores: [], scores: [],
changeRecords: [], changeRecords: [],
kingdeeStatus: 'none',
kingdeeFailReason: '',
kingdeeSyncTime: null,
}; };
}, },
getDetailList(type) { getDetailList(type) {
@@ -2951,12 +3107,15 @@ export default {
}); });
}, },
initMkEnabled() { initMkEnabled() {
this._mkEnabledResolved = false;
loadMkEnabled() loadMkEnabled()
.then(enabled => { .then(enabled => {
this.mkEnabled = !!enabled; this.mkEnabled = !!enabled;
this._mkEnabledResolved = true;
}) })
.catch(() => { .catch(() => {
this.mkEnabled = false; this.mkEnabled = false;
this._mkEnabledResolved = true;
}); });
}, },
initScoreQuantificationOptions() { initScoreQuantificationOptions() {
@@ -3056,7 +3215,7 @@ export default {
); );
}, },
/** /**
* 客商所属组织固定为顶层「桂物物流集团」,表单不可编辑。 * 客商所属组织固定为顶层「广西现代物流集团有限公司」,表单不可编辑。
*/ */
applyFixedOwnerDept() { applyFixedOwnerDept() {
if (!this.isArchivePage || this.readonly) { if (!this.isArchivePage || this.readonly) {
@@ -4134,6 +4293,7 @@ export default {
} }
this.archiveForm = this.emptyArchive(); this.archiveForm = this.emptyArchive();
this.originalAccessType = ''; this.originalAccessType = '';
this.resetKingdeeQueryState();
this.qualificationFiles = []; this.qualificationFiles = [];
this.qualificationUploadFiles = []; this.qualificationUploadFiles = [];
this.ocrQualificationUploadFiles = []; this.ocrQualificationUploadFiles = [];
@@ -4143,6 +4303,7 @@ export default {
}, },
resetArchive() { resetArchive() {
this.readonly = false; this.readonly = false;
this.resetKingdeeQueryState();
this.changeRecordDetailVisible = false; this.changeRecordDetailVisible = false;
this.changeRecordDetail = null; this.changeRecordDetail = null;
this.changeRecordDetailRows = []; this.changeRecordDetailRows = [];
@@ -4266,6 +4427,10 @@ export default {
return archive; return archive;
}, },
saveArchive() { saveArchive() {
if (this.kingdeeBlocked) {
this.$message.warning('金蝶审核中,请稍后重新添加');
return;
}
this.$refs.archiveForm.validate(valid => { this.$refs.archiveForm.validate(valid => {
if (!valid) { if (!valid) {
this.$message.warning('请先完整填写必填项'); this.$message.warning('请先完整填写必填项');
@@ -4279,6 +4444,10 @@ export default {
}); });
}, },
saveAndSubmitArchive() { saveAndSubmitArchive() {
if (this.kingdeeBlocked) {
this.$message.warning('金蝶审核中,请稍后重新添加');
return;
}
this.$refs.archiveForm.validate(valid => { this.$refs.archiveForm.validate(valid => {
if (!valid) { if (!valid) {
this.$message.warning('请先完整填写必填项'); this.$message.warning('请先完整填写必填项');
@@ -4322,15 +4491,34 @@ export default {
}); });
}, },
/** /**
* 列表 / 新增 / 编辑提交共用:开启 MK 时提交审核流,并更新客商审批状态 * 列表 / 新增 / 编辑提交共用:
* 未开启 MK 时仅走本地提交审批(与新增一致,不请求 MK 流程);
* 开启 MK 时先提交审核流,再更新客商审批状态。
*/ */
submitCustomerApproval(id, fullName = '', approvalStatus = '') { submitCustomerApproval(id, fullName = '', approvalStatus = '') {
return submitMkApprovalFlow({ const submitLocal = () => submitApproval(id);
bizType: 'customer-archive', const run = enabled => {
bizId: id, this.mkEnabled = !!enabled;
subjectName: fullName, this._mkEnabledResolved = true;
approvalStatus, if (!this.mkEnabled) {
}).then(() => submitApproval(id)); return submitLocal();
}
return submitMkApprovalFlow({
bizType: 'customer-archive',
bizId: id,
subjectName: fullName,
approvalStatus,
mkEnabled: true,
}).then(submitLocal);
};
// 与列表按钮一致:以页面 mkEnabled 为准,未开启时不请求 MK
if (this._mkEnabledResolved) {
return run(this.mkEnabled);
}
return loadMkEnabled()
.then(enabled => run(enabled))
.catch(() => run(false));
}, },
openReviewScore() { openReviewScore() {
const scores = this.archiveForm.scores || []; const scores = this.archiveForm.scores || [];
@@ -5035,17 +5223,47 @@ export default {
this.$message.error('获取审核跳转地址失败'); this.$message.error('获取审核跳转地址失败');
}); });
}, },
handleApprove(row) { async handleApprove(row) {
this.$confirm('是否确认审核通过?', '提示', { await this.$confirm('是否确认审核通过?', '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
}) });
.then(() => approve(row.id)) const res = await approve(row.id);
.then(() => { // data=false 表示审核已通过但推送金蝶失败,msg 带具体原因
this.onLoad(this.page); if (res.data.data === false) {
this.$message({ type: 'success', message: '审核通过!' }); this.$message.warning(res.data.msg);
} else {
this.$message.success(res.data.msg || '审核通过!');
}
this.onLoad(this.page);
},
async handleSyncKingdee(row) {
if (this.kingdeeSyncingId) return;
try {
await this.$confirm('是否确认推送金蝶?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}); });
} catch (exception) {
return;
}
this.kingdeeSyncingId = row.id;
try {
const res = await syncKingdee(row.id);
const archive = res?.data?.data || {};
if (archive.kingdeeStatus === 'synced') {
this.$message.success('推送成功,金蝶客户编码与客商编号一致');
} else if (archive.kingdeeStatus === 'exist') {
this.$message.info('金蝶已有该客商档案,无需推送');
} else {
this.$message.warning(`推送失败:${archive.kingdeeFailReason || '未知原因'}`);
}
this.onLoad(this.page);
} finally {
this.kingdeeSyncingId = null;
}
}, },
handleReject(row) { handleReject(row) {
this.$confirm('是否确认审核不通过?', '提示', { this.$confirm('是否确认审核不通过?', '提示', {
@@ -5078,19 +5296,9 @@ export default {
this.onLoad(this.page); this.onLoad(this.page);
}, },
searchChange(params, done) { searchChange(params, done) {
// 搜索区所属组织级联返回的是 ID 路径数组,取末级 ID 转成部门名称传给后端(与列表接口 deptName 字段一致)
if (Array.isArray(params.deptName)) {
const path = params.deptName.filter(
item => item !== undefined && item !== null && item !== ''
);
const deptId = path.length ? path[path.length - 1] : '';
params.deptName = deptId
? this.deptOptions.find(item => String(item.value) === String(deptId))?.rawLabel || ''
: '';
}
this.query = this.buildQuery(params); this.query = this.buildQuery(params);
this.page.currentPage = 1; this.page.currentPage = 1;
this.onLoad(this.page); this.onLoad(this.page, this.query);
done(); done();
}, },
selectionChange(list) { selectionChange(list) {
@@ -5151,7 +5359,38 @@ export default {
}; };
}, },
buildQuery(params = {}) { buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap); // 新条件覆盖旧条件;本次显式清空的字段必须从查询里去掉,避免第二次搜索仍带着上一次的客商名称。
const merged = { ...this.query, ...params };
Object.keys(params).forEach(key => {
const value = params[key];
if (
value === '' ||
value === null ||
value === undefined ||
(Array.isArray(value) && value.length === 0)
) {
delete merged[key];
}
});
if (Array.isArray(merged.deptName)) {
const path = merged.deptName.filter(
item => item !== undefined && item !== null && item !== ''
);
const deptId = path.length ? path[path.length - 1] : '';
merged.deptName = deptId
? this.deptOptions.find(item => String(item.value) === String(deptId))?.rawLabel || ''
: '';
if (!merged.deptName) {
delete merged.deptName;
}
}
const values = normalizeSearchRangeParams(merged, createTimeRangeMap);
// 状态下拉「全部」会被数字类型转成 0/-1,二次搜索会把列表滤空。
const status = values.status;
if (status === 0 || status === '0' || status === -1 || status === '-1') {
delete values.status;
}
return values;
}, },
}, },
}; };
+2 -2
View File
@@ -220,7 +220,7 @@ import { downloadXls } from '@/utils/util';
import { handleImportExcel } from '@/utils/import-excel'; import { handleImportExcel } from '@/utils/import-excel';
import { getToken, isAdministrator } from '@/utils/auth'; import { getToken, isAdministrator } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload'; import { getUploadHeaders } from '@/utils/upload';
import { normalizeSearchRangeParams } from '@/utils/search-range'; import { applyIncomingSearch, normalizeSearchRangeParams } from '@/utils/search-range';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { ElLoading } from 'element-plus'; import { ElLoading } from 'element-plus';
import { excelOption, exportColumns, option } from '@/option/vehicle/insurance-record'; import { excelOption, exportColumns, option } from '@/option/vehicle/insurance-record';
@@ -409,7 +409,7 @@ export default {
: integerPart; : integerPart;
}, },
buildQuery(params = {}) { buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap); return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
}, },
validateRow(row) { validateRow(row) {
if (row.startDate && row.endDate && row.endDate < row.startDate) { if (row.startDate && row.endDate && row.endDate < row.startDate) {
+2 -2
View File
@@ -195,7 +195,7 @@ import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel'; import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit'; import { formatUpdateUserName } from '@/utils/audit';
import { getToken, isAdministrator } from '@/utils/auth'; import { getToken, isAdministrator } from '@/utils/auth';
import { normalizeSearchRangeParams } from '@/utils/search-range'; import { applyIncomingSearch, normalizeSearchRangeParams } from '@/utils/search-range';
import AddressMapPicker from '@/components/address-map-picker/main.vue'; import AddressMapPicker from '@/components/address-map-picker/main.vue';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue'; import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
@@ -903,7 +903,7 @@ export default {
}; };
}, },
buildQuery(params = {}) { buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap); return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
}, },
handleTemplate() { handleTemplate() {
exportBlob( exportBlob(
+4 -6
View File
@@ -168,7 +168,7 @@ import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel'; import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit'; import { formatUpdateUserName } from '@/utils/audit';
import { getToken, isAdministrator } from '@/utils/auth'; import { getToken, isAdministrator } from '@/utils/auth';
import { normalizeSearchRangeParams } from '@/utils/search-range'; import { applyIncomingSearch, normalizeSearchRangeParams } from '@/utils/search-range';
import AddressMapPicker from '@/components/address-map-picker/main.vue'; import AddressMapPicker from '@/components/address-map-picker/main.vue';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue'; import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
@@ -371,9 +371,8 @@ export default {
placeholder: '日期时间选择器', placeholder: '日期时间选择器',
}, },
{ {
label: '里程/航程数(公里)', label: '里程/航程数',
prop: 'mileage', prop: 'mileage',
renderHeader: () => '里程/航程数',
type: 'input', type: 'input',
minWidth: 130, minWidth: 130,
slot: true, slot: true,
@@ -391,6 +390,7 @@ export default {
], ],
value: '公里', value: '公里',
hide: true, hide: true,
display: false,
}, },
{ {
label: '备注', label: '备注',
@@ -565,12 +565,10 @@ export default {
const mileageUnit = vehicleType === '船舶' ? '海里' : '公里'; const mileageUnit = vehicleType === '船舶' ? '海里' : '公里';
const vehicleTypeColumn = this.findColumn(this.option.column, 'vehicleType'); const vehicleTypeColumn = this.findColumn(this.option.column, 'vehicleType');
const vehicleNoColumn = this.findColumn(this.option.column, 'vehicleNo'); const vehicleNoColumn = this.findColumn(this.option.column, 'vehicleNo');
const mileageColumn = this.findColumn(this.option.column, 'mileage');
const mileageUnitColumn = this.findColumn(this.option.column, 'mileageUnit'); const mileageUnitColumn = this.findColumn(this.option.column, 'mileageUnit');
vehicleNoColumn.placeholder = vehicleNoColumn.placeholder =
vehicleType === '船舶' ? '请选择船号' : '请选择车牌号'; vehicleType === '船舶' ? '请选择船号' : '请选择车牌号';
vehicleTypeColumn.disabled = Boolean(this.form.id); vehicleTypeColumn.disabled = Boolean(this.form.id);
mileageColumn.label = `里程/航程数(${mileageUnit})`;
mileageUnitColumn.value = mileageUnit; mileageUnitColumn.value = mileageUnit;
this.form.mileageUnit = mileageUnit; this.form.mileageUnit = mileageUnit;
if (!this.isDetailLoading && oldValue && vehicleType !== oldValue) { if (!this.isDetailLoading && oldValue && vehicleType !== oldValue) {
@@ -833,7 +831,7 @@ export default {
}; };
}, },
buildQuery(params = {}) { buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap); return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
}, },
handleTemplate() { handleTemplate() {
exportBlob( exportBlob(
+2 -2
View File
@@ -210,7 +210,7 @@ import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util'; import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel'; import { openImportDialog } from '@/utils/import-excel';
import { getToken, isAdministrator } from '@/utils/auth'; import { getToken, isAdministrator } from '@/utils/auth';
import { normalizeSearchRangeParams } from '@/utils/search-range'; import { applyIncomingSearch, normalizeSearchRangeParams } from '@/utils/search-range';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue'; import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { excelOption, exportColumns, option } from '@/option/vehicle/mileage-record'; import { excelOption, exportColumns, option } from '@/option/vehicle/mileage-record';
@@ -607,7 +607,7 @@ export default {
}, },
buildQuery(params = {}) { buildQuery(params = {}) {
return normalizeSearchRangeParams( return normalizeSearchRangeParams(
{ ...params, ...this.query, vehicleType: '车辆' }, { ...applyIncomingSearch(this.query, params), vehicleType: '车辆' },
createTimeRangeMap createTimeRangeMap
); );
}, },
@@ -155,7 +155,7 @@ import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util'; import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel'; import { openImportDialog } from '@/utils/import-excel';
import { getToken, isAdministrator } from '@/utils/auth'; import { getToken, isAdministrator } from '@/utils/auth';
import { normalizeSearchRangeParams } from '@/utils/search-range'; import { applyIncomingSearch, normalizeSearchRangeParams } from '@/utils/search-range';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue'; import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { excelOption, exportColumns, option } from '@/option/vehicle/tire-replacement-record'; import { excelOption, exportColumns, option } from '@/option/vehicle/tire-replacement-record';
@@ -500,7 +500,7 @@ export default {
}; };
}, },
buildQuery(params = {}) { buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap); return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
}, },
handleTemplate() { handleTemplate() {
exportBlob( exportBlob(
@@ -130,7 +130,7 @@ import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util'; import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel'; import { openImportDialog } from '@/utils/import-excel';
import { getToken, isAdministrator } from '@/utils/auth'; import { getToken, isAdministrator } from '@/utils/auth';
import { normalizeSearchRangeParams } from '@/utils/search-range'; import { applyIncomingSearch, normalizeSearchRangeParams } from '@/utils/search-range';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue'; import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { excelOption, exportColumns, option } from '@/option/vehicle/transport-change-record'; import { excelOption, exportColumns, option } from '@/option/vehicle/transport-change-record';
@@ -478,7 +478,7 @@ export default {
}; };
}, },
buildQuery(params = {}) { buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap); return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
}, },
handleTemplate() { handleTemplate() {
exportBlob( exportBlob(
+2 -2
View File
@@ -197,7 +197,7 @@ import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util'; import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel'; import { openImportDialog } from '@/utils/import-excel';
import { getToken, isAdministrator } from '@/utils/auth'; import { getToken, isAdministrator } from '@/utils/auth';
import { normalizeSearchRangeParams } from '@/utils/search-range'; import { applyIncomingSearch, normalizeSearchRangeParams } from '@/utils/search-range';
import AddressMapPicker from '@/components/address-map-picker/main.vue'; import AddressMapPicker from '@/components/address-map-picker/main.vue';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue'; import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
@@ -673,7 +673,7 @@ export default {
}; };
}, },
buildQuery(params = {}) { buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap); return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
}, },
handleTemplate() { handleTemplate() {
exportBlob( exportBlob(
+17 -15
View File
@@ -18,6 +18,8 @@ export default ({ mode, command }) => {
target: 'esnext', target: 'esnext',
minify: isProd ? 'terser' : 'esbuild', // 根据环境选择压缩工具 minify: isProd ? 'terser' : 'esbuild', // 根据环境选择压缩工具
rollupOptions: { rollupOptions: {
// @napi-rs/canvas 是 Node 原生模块(@open-file-viewer 传递依赖),浏览器 bundle 打不了,跳过
external: [/^@napi-rs\//],
output: { output: {
assetFileNames, assetFileNames,
}, },
@@ -47,28 +49,28 @@ export default ({ mode, command }) => {
__VUE_I18N_LEGACY_API__: true, __VUE_I18N_LEGACY_API__: true,
__INTLIFY_PROD_DEVTOOLS__: false, __INTLIFY_PROD_DEVTOOLS__: false,
}, },
// server: {
// port: 2888,
// proxy: {
// '/api': {
// // 本地网关默认 80 端口(blade-gateway bootstrap.yml);开发时需先启动网关及 blade-auth/blade-system 等服务
// // 注意:本机 8080 一般是 Nacos,不是业务网关
// target: 'http://localhost',
// // target: 'http://172.16.203.228:8000', // 仅联调远程网关时启用(且不要 rewrite)
// changeOrigin: true,
// rewrite: path => path.replace(/^\/api/, ''),
// },
// },
// },
server: { server: {
port: 2889, port: 2888,
proxy: { proxy: {
'/api': { '/api': {
target: 'http://172.16.203.221:8000', // 本地网关默认 80 端口(blade-gateway bootstrap.yml);开发时需先启动网关及 blade-auth/blade-system 等服务
// 注意:本机 8080 一般是 Nacos,不是业务网关
target: 'http://localhost',
// target: 'http://172.16.203.228:8000', // 仅联调远程网关时启用(且不要 rewrite)
changeOrigin: true, changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
}, },
}, },
}, },
// server: {
// port: 2889,
// proxy: {
// '/api': {
// target: 'http://172.16.203.228:8000',
// changeOrigin: true,
// },
// },
// },
resolve: { resolve: {
alias: { alias: {
'~': resolve(__dirname, './'), '~': resolve(__dirname, './'),