Compare commits

...

30 Commits

Author SHA1 Message Date
b2894lxlx 96b675b3be Merge remote-tracking branch 'websoft/master' 2026-10-16 16:05:55 +08:00
b2894lxlx 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
b2894lxlx 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
b2894lxlx 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
b2894lxlx 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
gxwebsoft 9919fb9c89 style(settlement): 优化预结算单选择弹窗及支付申请表样式
- 在预结算单选择弹窗中添加el-card包裹form表单,提升视觉层次
- 修改支付申请表结算单选择的placeholder文本为“请输入”
- 使用el-card包裹支付申请表中的参考搜索区块,增强样式一致性
- 调整表单验证提示文本,将“请选择结算单”改为“请输入”
2026-10-14 16:53:20 +08:00
gxwebsoft 6eab96b620 style(settlement): 优化预结算单选择弹窗及支付申请表样式
- 在预结算单选择弹窗中添加el-card包裹form表单,提升视觉层次
- 修改支付申请表结算单选择的placeholder文本为“请输入”
- 使用el-card包裹支付申请表中的参考搜索区块,增强样式一致性
- 调整表单验证提示文本,将“请选择结算单”改为“请输入”
2026-10-14 16:53:13 +08:00
gxwebsoft b39501291c refactor(payment): 统一搜索栏“展开/收起”按钮实现
- 取消 el-tooltip 包裹,改用纯 el-button 实现带图标的展开收起按钮
- 按 avue-crud 规范控制按钮显示,新增 searchIndex (默认 4)和 searchItemCount 控制逻辑
- 搜索项数量未超出 searchIndex 时隐藏展开收起按钮
- 相关组件支付模块多处应用此统一方案(bill-ledger-search、bill-payment-search、invoice-application、invoice-receipt、receipt-claim-record、receipt-flow)
- 维护展开状态对应的箭头图标和文案为“收起”与“展开”

refactor(business): 优化路线变更内容渲染与样式

- 替换旧的 formatRouteChangeContent 为更详细的 parseRouteChangeContent,支持多分组与字段显示
- 路线变更内容支持多层次分组、字段标签、改变前后值对比及未变更标识展示
- 取消全局列溢出提示以避免多行内容被截断,针对路线变更内容列显式禁用溢出提示并设置白色换行样式
- 在 business-crud-page、waybill-manage-page、loading-manage 相关页面和组件中统一应用该解析与渲染方案

refactor(option): 调整 measurement-unit 配置以提升表格列展示

- 移除 menuWidth 配置,采用 menuFixed 固定右侧模式
- 为创建人和更新人列添加 minWidth 110,时间列添加 width 180,避免内容被截断
- 表格列数组顺序调整,确保搜索与展示保持一致

refactor(payment & receipt-flow): 统一支付及回执模块工具栏和表格行为

- 按照统一规范和用户反馈,调整多处页面的搜索栏操作区和表格列配置
- 引入 TableHeaderTools 组件和相关状态传递,实现搜索显隐与表格尺寸切换统一
- 优化 avue-crud 表格组件的交互一致性和视觉表现

refactor(utils): 重构路线变更内容解析函数

- 新增 parseRouteChangeContent 以支持复杂路线变更文本的解析与分组
- 识别运单号、字段标签、变更前后值,支持多种箭头符号及调整描述
- 用法推广至相关业务页面,支持更加直观的路线变更展示界面
2026-10-14 16:21:50 +08:00
67 changed files with 4072 additions and 435 deletions
+142
View File
@@ -0,0 +1,142 @@
# 2026-10-14
## 运单管理:草稿行「查看」改为与其它状态一致的「详情」
- 现象:`/business/waybill-manage` 列表操作列,草稿行显示「查看」→ 走 avue 自带 `$refs.crud.rowView()` 表单弹窗;其它状态显示「详情」→ `openDetail()` 跳独立详情页 `/business/waybill-manage/detail?id=x`(新标签「运单管理详情」)。同一列两种弹窗形态。
- 根因:`waybill-manage-page.vue` 的 `showDetailButton(row)` 显式 `return status !== 'draft'`,把草稿排除在详情按钮外,于是落到 `#menu` 里 `!showDetailButton(row)` 的「查看」分支。
- 修法:`showDetailButton()` 去掉草稿判断,仅保留 `config.detailButton` + `_view` 权限判断;「查看」分支保留,仅在 `detailButton:false` 时兜底。
- 备注:`business-crud-page.vue:7043`、`transport-plan-page.vue:4111` 有同款 `status !== 'draft'` 逻辑(运输计划另有 `showViewButton`);两个 option 文件里的 `detailButtonIgnoreDraftStatus` 目前是死配置,组件未读取。
## 收付款管理模块:列表页右上角统一为 4 键工具组(最终方案 = 改 avue-crud)
- 现象:付款管理页右上角只有 1 个刷新按钮,与其它模块不一致;参考模块的 4 键工具组其实是 **avue-crud 内置头部**,不是可引用的公共组件。
- 方案演进:
1. 过渡方案(已落地 7 页):手写页引入标准件 `src/components/table-header-tools/main.vue`,改法为搜索区 `v-if="searchVisible"`、el-table 加 `:size="tableSize"`、列 `v-for="column in visibleColumns"`、data 加 `searchVisible/tableSize/visibleColumnKeys`;汇票两页表格在子组件,子组件透传 `update:searchVisible` + `refresh`。已接入 `invoice-application.vue`、`invoice-receipt.vue`、`receipt-flow.vue`、`receipt-claim-record.vue`、`bill-ledger.vue`+`bill-ledger-table.vue`、`bill-payment.vue`+`bill-payment-table.vue`。
但用户实测反馈:第 2 键(列设置)与第 4 键(表格尺寸)效果仍与参考模块不同 —— 参考模块第 2 键是 `el-drawer` 列设置抽屉(列名/隐藏/冻结/过滤/排序),第 4 键是「表格 ↔ 卡片」视图切换,两者都嵌在 avue 内部,**无法脱离 avue-crud 复用**。
2. 最终方案(用户选定,先做样板):列表页改写成 `<avue-crud>`,4 键 = 刷新 / 列显隐抽屉 / 搜索显隐 / 网格切换,与其它模块 100% 一致。
- 样板:`src/views/payment/payment-application.vue` + `src/option/payment/paymentApplication.js`(`paymentApplicationOption()` 工厂)。
- 搜索项与表格列 label 不一致的(表格列「收款方」vs 搜索项「客户名称」)用 `searchProp` 映射;属搜索专用列用 `searchOnlyColumn()` 统一生成(`hide + showColumn:false + add/edit/viewDisplay:false`)。
- ⚠️ **avue 的 searchOrder 是降序**(`header-search2.js`:`(b.searchOrder||0)-(a.searchOrder||0)`),值越大越靠前 —— 首次做反了,9 个搜索项 score 全部反转。
- ⚠️ **column 数组顺序即表格列顺序**,搜索项夹在中间会把该列挤到表格前面(`deptName` 踩过),搜索专用列必须紧跟在同位置或用 `hide` 隔离。
- `on-load` 在 created / currentChange / sizeChange 都会触发(`table-page2.js`),因此 `currentChange`/`sizeChange` 只改 page、**不要再调 onLoad**;`onLoad(page)` 也不能把 page 里的 total/pageSizes spread 进查询参数。
- 顺带加固:`table-header-tools` 加 `deactivated()` 关闭列设置弹窗(keep-alive 切页防 Teleport 弹窗残留);清理 invoice-application / invoice-receipt 里失效的 `__toolbar-right` 样式规则。
- 验证:项目根建临时 html 挂真组件 + `app.use(Avue)`,`agent-browser` 实测 —— 4 个按钮均渲染图标、刷新触发、搜索区切换、表格尺寸 default→large、列设置弹窗勾选双向生效;avue-crud 样板另测:4 键数量=4、默认 4 项搜索 + 展开 9 项、18 列表格顺序与原页一致、列显隐抽屉表头 5 列 18 行、网格按钮切出卡片视图;临时文件用完即删。
- 踩坑:`el-icon-*` 字符串图标靠 Avue 的全局映射,验证页不装 Avue 时 4 个按钮是空圆(差点误判)。
- 待办:用户确认样板效果后,把其余 6 页(invoice-application / invoice-receipt / receipt-flow / receipt-claim-record / bill-ledger / bill-payment)也改成 avue-crud,并回退其 table-header-tools 改动。
## 正式结算单表单:结算明细区按钮/筛选行对齐预结算单
- 现象:`formal-settlement-editor.vue` 的「结算明细」区把 导出/选择结算明细 塞在筛选表单的 actions 里(`grid-column:1/-1` 换行右对齐,出现两个蓝实心),与 `pre-settlement-editor.vue` 不一致(后者把这两个按钮放 `<section-card #extra>`,筛选项与查询/重置同行)。
- 改法(只动 formal 一个文件,72+/17-):
1. 模板 `section-card#extra` 加 导出(`v-if="hasPermission('formal_settlement_export')" type=primary plain`)+ 选择结算明细(`v-if="editable" type=primary plain`),与「收起明细」并列;actions 里只留 查询/重置。
2. el-form 去掉 `inline`、`label-width` 88px→auto,4 个输入补 `placeholder="请输入"`。
3. 样式 `grid-template-columns: repeat(4, minmax(0,1fr)) auto` + `align-items:center`;actions 去掉 `grid-column:1/-1` 改 `width:auto;margin-left:0;white-space:nowrap;justify-self:end`;媒体查询 ≤1200px 同步补 `auto` 列。
4. 补 `hasPermission`:`...mapGetters(['permission','userInfo'])` + `isAdmin`(`isAdministrator` from `@/utils/auth`)+ methods `hasPermission(code){ return this.isAdmin || this.validData(this.permission?.[code], false) }`(`validData` 是项目全局属性,main.js 未显式挂载但全站通用)。
- ⚠️ 关键坑:文件末尾 `.formal-editor :deep(.el-form-item){margin-bottom:24px}` 与筛选区 `:deep(.el-form-item){margin-bottom:0}` **特异性相同**,靠源序决定胜负 → 覆盖规则必须写在文件尾部(`@media` 之前)。pre 用的也是同索引结构。
- 验证:`agent-browser` + mock 注入实测 —— grid 列 `188.25×4 + 132`、5 个 form-item 同一行 top 相同、form-item margin-bottom=0;正式单与预结算单**同视口截图逐像素一致**。
## 「变更运输路线」弹窗 · 变更记录列表:变更前/后地址分行展示
- 需求:变更内容原为单行 `YD-xxx:发货地 A → B;到货地 C → D`(多条以 `;` 拼接),地址太长挤成一坨,要求「变更前地址 / 变更后地址」换行。
- 落地:新增 `src/utils/route-change.js` 导出 `formatRouteChangeContent(content, emptyText='-')` —— 仅**展示层**格式化(不动 taskInfoJson 里已存的 content,历史数据同样生效):`\r\n` 归一 → `;` 转 `\n` → `→` 转 ` →\n`(箭头留上一行行尾)→ 压缩空行;空值返回 `-`。
- 接入 3 处同一弹窗(都在 methods 里以 ES6 简写挂载 `formatRouteChangeContent`,与 `formatContractOptionLabel` 同款):
- `views/business/loading-manage.vue`(表格列 `record-content`)
- `views/business/components/waybill-manage-page.vue`
- `views/business/components/business-crud-page.vue`
- ⚠️ 关键坑:`white-space: pre-wrap` 下模板里插值前后的**换行+缩进空白会被渲染出来**(内容前面凭空多一行缩进)。所以要么把 `>{{ ... }}</div>` 写成紧贴一行(loading-manage),要么改用 `v-text="formatRouteChangeContent(row.content)"` 多行写法(另两处)——文本节点不存在才安全。CSS 里 `white-space:normal` 需改 `pre-wrap`(loading-manage 原为 normal),另两处补 `overflow-wrap:anywhere; word-break:break-word; line-height:22px`。
- 效果:`YD-20260904-0022:发货地 广西…公司 →` / 换行 / `广东省…西瓜洲` / `到货地 山东…公司 →` / `山东省…公司`。
- 验证:node 直跑函数(多条 / 单条 / 路线顺序 / 空值)输出正确;dev `localhost:2889` 三个 .vue 与 scoped style 块均 200,编译产物各含 3 处 `formatRouteChangeContent`。
- 未做(如需再说):存储格式与导出 Excel 仍是单行文本;`vehicle/transport-change-record.vue` 的 `changeContent` 是人工填写的自由文本,未改。
## 发货模板"选择常用地址"改图标(对齐运输计划)
- 需求:`/business/shipping-template/form` 收发货信息行,把文字「选择」按钮改成运输计划同款图标方式。
- 改动(`src/views/business/components/shipping-template-page.vue`):
1. `import { Location, OfficeBuilding } from '@element-plus/icons-vue'`,并把 `OfficeBuilding` 加进 `data()`;
2. departure/arrival 两个 form 插槽里,把 `<el-tooltip><el-button ...>选择</el-button></el-tooltip>` 从行尾移到第二个地址输入框之后,改成 `:icon="OfficeBuilding"` + `link` 图标按钮(class `shipping-template-page__address-pick-btn`);
3. suffix 的 `<el-icon>` 补 class `shipping-template-page__map-suffix`(cursor/18px/#909399/hover #409eff),与运输计划同名同款;
4. `.shipping-template-page__route-row` grid 第 3 列插入 `40px` 图标列(原末尾的 `auto`),并把 `__address-pick-btn .el-icon { font-size: 22px }` 写进该行容器内。
- 关键坑(差点误判成 bug):Element Plus 图标两种用法机制不同 ——
- 标签形式 `<Location />` 靠 `main.js` 里 `app.component(key, ...)` **全局注册**解析,与 `data()` 无关;
- 表达式形式 `:icon="OfficeBuilding"` 编译成 `_ctx.OfficeBuilding`,**必须把组件对象放进 data()**(放 `components` 注册里模板取不到,会静默渲染不出图标)。
验证页漏了全局注册,曾误判 `<Location />` 不渲染。
- 验证:临时验证页(项目根 html + 真 .vue,走 Vite + 真 element-plus + 全局图标注册)实测:地址框内 Location 18px、按钮 40px 内 svg 22px 居中、7 列 184/368/40/48/184/64/184 铺满 1120px;截图视觉与运输计划一致。用完已删临时文件。
- dev 自检:`shipping-template-page.vue` 与 scoped style 均 200。
## 多联总单:调度页顶部路线节点 / 详情页路线图 —— 省市区按「市 区县」分段展示
- 现象:`/business/master-order?mode=dispatch&id=xxx` 顶部 `route-overview` 的起/终节点直接绑定 `route.departureName/arrivalName` 原始值 → 显示成「上海市浦东新区」「广西壮族自治区南宁市邕宁区」挤成一坨(同页段1~段3 的表头早已是「上海市 浦东新区 → 上海市 静安区」,用户截图圈出顶部这行说"怎么还没改")。
- 根因:顶部节点是唯一没走格式化方法的绑定(段头走 `dispatchLocationName`,详情页走 `segmentLocationName`),不是格式化逻辑有问题。
- 落地:新增 `src/utils/region-address.js` 导出 `formatRegionAddress(value)` + `splitRegionTokens(value)`(按行政区划后缀切分,`州`+`市`、`县`+`区` 连写特例合并;**整串都是行政区划**才重排,站点名「南宁站」「广州南站」和含街道门牌的详细地址原样返回)→「市 区县」的空格展示,省/自治区不重复(与全站既有 `formatRoadLocation`/`formatRouteCityDistrict` 规则一致,`广西壮族自治区南宁市邕宁区` → `南宁市 邕宁区`)。
- 接入 4 处:调度页 `route-overview` 起/终节点的强文本改用 `dispatchLocationName(route, 'departure'|'arrival')`、第二行地址改用 `formatRegionAddress(...)`,`addressText()` 名称+地址都格式化并去重(`南宁市 邕宁区` 不再重复两遍);详情页 `route-map` 两处 `<small>` 地址改用 `formatRegionAddress(...)`。
- 验证:项目根临时验证页(真组件 + `axios.defaults.adapter` 全量 mock 后端,复刻截图数据 `routes=[上海市静安区, 南宁站, 广西壮族自治区南宁市邕宁区]`)+ agent-browser 读 DOM —— 调度页顶部 4 节点 =「上海市 浦东新区 / 上海市 静安区 / 南宁站 / 南宁市 邕宁区」,与段1~段3 表头、详情页路线图**逐字一致**;终节点宽 101px 未被省略号截断(改前原始串超宽被截);两页 innerText 再扫 `省|自治区` 已无未加空格的残留。用完已删临时文件。
## 同事拉代码编译报错(漏提交新文件)→ 已修复
- 现象:同事 pull 后 `[vite:load-fallback] Could not load .../src/utils/route-change (imported by src/views/business/loading-manage.vue): ENOENT`。
- 根因:`src/utils/route-change.js`、`src/utils/region-address.js` 是**新增文件且一直 untracked**,本地能跑、远端没有。
提交时用了只带已跟踪文件的写法(`git commit -am` 之类),.vue 的引用被提交了、新 .js 没进去(12:44 的 6de87ed 就是这种情况)。region-address 是第二个雷:route-change 修好后会接着报它。
- 修复:`git add src/utils/region-address.js src/utils/route-change.js` → commit `749b314 :bug: 补充提交遗漏的 region-address 与 route-change 工具文件` → push(期间远端有同事新提交 74780b8,先 `git pull --rebase` 无冲突)。三处 tree(本地 HEAD / origin/master / 远端真实 SHA)核对一致。
- 复查:写脚本扫 `src/**` 所有 `@/`、相对路径引用(含 scss @use/@import),比对 `git ls-files`,**无其它"文件存在但未跟踪"的引用目标**,暂时不会再踩同类坑。
- 教训:本仓库提交前建议 `git status` 扫一眼 `??`,新增文件必须显式 `git add`。
## payment 模块 6 个手写列表页:搜索栏「展开/收起」统一(对齐 customer-archive / settlement)
- 起因:`/payment/invoice-application`、`/payment/bill-payment` 的展开收起控件与别的模块不一致。排查发现 payment 模块 6 个手写搜索栏各写各的:5 个是 `el-tooltip`+`el-button text`+纯箭头图标(文案「折叠」,tooltip 才显示),`receipt-flow.vue` 更离谱 —— 裸 `el-button`(带边框)且排在**查询/重置左边**。
- 统一标准(两处参照物写法完全一致):
- `views/settlement/{formal-settlement,pre-settlement,transport-reconciliation,receivable-payable-detail,settlement-adjustment}.vue` 已有写法:
```html
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
{{ searchExpanded ? '收起' : '展开' }}
</el-button>
```
放在「查询 / 重置」之后(最右)。
- `views/vehicle/customer-archive.vue` 是 avue-crud 内建,`searchIcon: true` + `searchIndex: 4`,渲染出的也是同一形态。
- avue 显示规则(`node_modules/@smallwei/avue/lib/packages/element-plus/crud/menu/header-search3.js` 的 `isSearchIcon`):
`searchIcon === true && columnLen > searchIndex` → **搜索项不超过 4 项时根本不渲染展开/收起按钮**。故各页 data 加 `searchIndex: 4` + `searchItemCount: N`,computed `showSearchToggle = searchItemCount > searchIndex`。
- 逐页落地:invoice-application(5项) / invoice-receipt(6项) / receipt-claim-record(11项) / receipt-flow(7项) / components/bill-ledger-search(6项) 均显示;**components/bill-payment-search 仅 4 项(后端 `BillPaymentServiceImpl.selectPage` 也只支持 paymentNo/deptName/paymentDate 区间/approvalStatus 这 4 个条件)→ showSearchToggle=false,不渲染按钮**,避免留一个"点了没反应"的假按钮。
- `receipt-flow.vue`:补 `import { ArrowDown, ArrowUp }`、按钮从最左移到最右、文案「折叠」→「收起」。
- gap 不动:`business/`、`settlement/` 手写页的 `__search-actions` 都写了 `gap: 8px`(与本次统一无关,别顺手删)。
- 校验:6 个文件 `curl localhost:2889/src/...vue` 与 `?vue&type=style&index=0&scoped=true&lang.scss` 全 200;`grep 折叠|el-tooltip src/views/payment/` 已无残留。
## 15:55 路线变更「变更内容」列排布问题(待用户确认方案,未动代码)
- 位置:`变更运输路线` 弹窗 → `变更记录` tab → 表格 `变更内容` 列,共 **3 处同款**:`views/business/loading-manage.vue:1688`(`__record-content`)、`views/business/components/waybill-manage-page.vue:3027`、`views/business/components/business-crud-page.vue:2998`。
- 存储格式为**单行文本**:`YD-xxx:发货地 A → B;到货地 C → D;YD-yyy:…`,展示层由 `src/utils/route-change.js` 的 `formatRouteChangeContent()` 切行(`;`→换行、`→`→` →\n`),再以 `white-space: pre-wrap` 平铺。
- 病因:`:` 不带换行 → 运单号与「发货地 原地址」挤同一行;前/后无标签;多条记录无分隔。一个单元格可堆 3 条运单 × 2 字段 × 2 行 ≈ 12+ 行。
- 已给 3 个方案(A 单元格内分组对齐:运单号独占行主色 + 字段标签列 + 前值/后值缩进对齐,改 formatter 返回结构 + 模板渲染;B 列只留「运单号 · N 项」摘要 + 查看详情弹窗,复用同类组件 `src/components/change-record-detail-dialog/main.vue`(变更字段/变更前/变更后三列 + 分页);C 按「运单 × 字段」拆行 + `span-method` 合并时间/人)。**推荐 A,B 备选**。等用户确认后再改。
## 16:20 已按方案 A 落地(变更内容列分组渲染)
- **`src/utils/route-change.js` 重写**:删掉 `formatRouteChangeContent`,改为导出 `parseRouteChangeContent(content)`,返回分组数组 `[{ type, title, text, fields:[{label,before,after,single,changed,unchanged}] }]`。
- ⚠️ **关键坑:存储格式里「到货地」前面只有 `;`、没有冒号**(真实格式 `${waybillNo}:发货地 A → B;到货地 C → D`)。第一版按「冒号切 head/tail」写,导致「到货地」整段落进纯文本兜底 → 必须"先判断 segment 是否以字段名开头",再把无标题字段行合并进上一条 waybill 分组。
- 字段名正则的**长标签必须排在短标签前**(`发货地址` 早于 `发货地`),否则被前缀吃掉。字段白名单:发货地址/到货地址/发货地/到货地/装货地/卸货地/收货地。
- 三种分组类型:`waybill`(`运单号:发货地…`)、`field`(`;到货地…`,合并进上一个非 text 分组)、`group`(`路线顺序:X 调整为 Y`,按「调整为」拆前后,不走箭头切分,因为路线文本本身含 `→`)、`text`(兜底原文)。
- 箭头正则 `/\s*(?:→|➔|➜|[\-–—]{1,2}\s*>)\s*/`;空值归一为 `-`。
- **三处模板统一改为分组渲染**(运单号主色标题 + 左侧 2px 主色竖条;字段 label 列 54px;`changed` 两行「旧值(灰) / → 新值(主色)」,`unchanged` 单行 + 灰字「未变更」):`loading-manage.vue`(`loading-route-change-dialog__change-*`)、`waybill-manage-page.vue`、`business-crud-page.vue`。样式块也同步替换(`__route-change-record-content` 从 `white-space: pre-wrap` 改为 flex column;箭头 `text-align: right`,grid item 会被 blockify 所以 span 上生效)。
- 校验:node 跑 `parseRouteChangeContent` 7 组用例(真实截图数据 / 路线顺序 / 混合 / 半角箭头分号 / 无箭头单值 / 纯文本 / 空)全部符合预期;`@vue/compiler-sfc` 编译三个 .vue 的 template + scss 全过;SSR 渲染真实模板 + agent-browser 截图确认观感(临时文件已删)。
- 已知代价:窄列(360px)下地址被 54px 标签列占位,单元格比原来高约一倍;换来的是一条运单/一个字段的分组边界一目了然。
## 16:05 /base/measurement-unit 创建时间/更新时间列截断修复
- 现象:表格里「创建时…」「更新时…」列被挤到 ~80px,`2026-0...` 截断;用户猜测要固定操作列。
- 根因:`option/base/measurement-unit.js` 两列只写了 `type/format/sortable`,**没有 width/minWidth** → el-table 未设宽列按剩余空间均分,被压到低于内容宽度 → 省略号。与 `menuFixed` 无关。
- 修法:`createTime`/`updateTime` 加 `width: 180`(与同模块 `option/base/common-address.js` 完全一致),`createUserName`/`updateUserName` 加 `minWidth: 110`。
- 结论(重要经验):**操作列不需要额外"固定"**,`menuFixed: 'right'` 早在 option 里(`main.js` 全局 `crudOption.menuWidth: 180`,avue 库默认 220)。不要为了"防截断"去加宽操作列,加宽只会增加横向滚动;时间列截断一律是缺 width。
- 验证方式(可复用):项目根建临时 html `__mu-check.html` → Vite 直接服务;`import { createOption } from '/src/option/base/measurement-unit.js'` + `app.use(Avue, { crudOption: { menuWidth: 180 } })`,用 `h(resolveComponent('avue-crud'), {...}, { menu: () => [...] })` 渲染(避免运行时模板编译)。
实测(容器 1200px):td 宽 `[50,70,150,150,130,200,100,110,110,180,180,180]` 合计 1610 → 横向滚动 410px;`scrollWidth > clientWidth` 的单元格 **0 个**;`操作` th 为 `position: sticky`,滚到最右后右边缘 = 容器右边缘 → fixed 生效。用完删文件 + `agent-browser close`。
## 16:25 变更内容列 hover 提示去除(分组改造的连带问题)
- 用户反馈:改完分组后,变更内容单元格 hover 出现提示浮层,要求去掉。
- 根因:`main.js:84` `app.use(ElementPlus, { table: { showOverflowTooltip: true } })` 全局开启 → EP 给每列 `.cell` 加 `.el-tooltip` 类(`white-space: nowrap`)。**我上一次改造把内层 `white-space: pre-wrap` 换成了 flex column、忘了补 white-space** → 继承 nowrap → 长地址横向溢出 → tooltip 激活。改动前靠 pre-wrap 自保所以没暴露。
- 修法(三处,列级豁免、不动全局配置):① 变更内容列显式 `:show-overflow-tooltip="false"`;② `__route-change-record-content` 补 `white-space: normal` 兜底。
- ⚠️ 验证踩坑:用「项目根临时 html + `node_modules/element-plus/dist/index.full.min.js` 全局构建 + `app.use(ElementPlus, { table: { showOverflowTooltip: true } })`」复现时,**el-table 表头/列宽渲染错乱**(表头逐列堆叠、每行只渲染出 2 个 cell),该环境不可用于布局验证;但单元格内容渲染正常,足以确认分组结构无误。要验证真实表格布局还是得走 Vite 服务真实页面。
另:自定义组件接模板字符串时,切出的模板里 `row.content` 要替换成 `content`(插槽作用域不存在于独立组件里),否则报 `Cannot read properties of undefined (reading 'content')`。
- 校验:编译三个 .vue 的 template + scss 全过;临时验证页 `__route-change-verify.html` 与 8899 端口静态服务已删除/停止。
- 未处理(等用户确认):备注列的 `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,等用户确认。
+13 -1
View File
@@ -40,6 +40,7 @@
- `searchIndex: 4` + `searchIcon: true` → 默认只显示前 4 个搜索项 + "展开/收起"(columnLen > searchIndex 时才出现该按钮)。
- **搜索项 label 与表格列 label 不同**(列「收款方」/搜索「客户名称」):单独定义 hide 列 + `searchProp` 指向后端字段(avue 在 search-change 里自动改名)。
- 搜索专用列加 `hide: true` + `showColumn: false`,否则会混进列显隐抽屉。
- ⚠️ **`createTime`/`updateTime` 必须显式给 `width: 180`**(全站统一,参考 `option/base/common-address.js`)。不写宽度的列参与"剩余空间均分",容易被压到 80px 出现 `2026-0...` 截断 —— 这是**列缺 width**,不是操作列没固定;`menuFixed: 'right'` 各页早已配好(`main.js` 全局 `crudOption.menuWidth: 180`,avue 默认 220),别用"加宽操作列"来处理时间列截断。创建人/更新人给 `minWidth: 110`。
- ⚠️ **column 数组顺序 = 表格列顺序**:搜索专用列别插在表格列中间(会把表格列顶到前面);deptName 这种"既搜索又展示"的列要放回表格列的正确位置。
- 分页:`page: { currentPage, pageSize, pageSizes, total }`;`on-load` 由 avue 内部(table-page)在 created / 翻页 / 改页容量时触发,`current-change`/`size-change` 里**只改 page 不要再调 onLoad**(否则双请求)。
- `onLoad(page)` 只用 `this.query`(avue 传的 page 对象含 total/pageSizes 等分页字段,别 spread 进查询参数)。
@@ -57,11 +58,15 @@
- 按钮层级:次要=无 type;中间步骤=`primary plain`;主操作=`primary`(禁绿、禁两个蓝实心)。顺序 `[辅助][取消][保存草稿][提交]`。
- 独立页标题 `.archive-page-form__title`;分组用全局 `<section-card>`;上传证件区 `width:100%; max-width:240px` + `.el-upload{height:151px}`。
- ⚠️ `<style scoped lang="scss">` 顶层禁 `//` 注释(Vite5 + sass `Unexpected '/'` → 500),顶层用 `/* */`。其余见 `src/styles/element-ui.scss` 与 `AGENTS.md` 4.4。
- **搜索栏「展开/收起」唯一标准写法**(settlement 5 处已用、avue 页面同款):
`<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">{{ searchExpanded ? '收起' : '展开' }}</el-button>`
放「查询/重置」**之后**(最右);禁 `el-tooltip` 包图标按钮、禁文案「折叠」。`ArrowUp/ArrowDown` 要 import 并放 `data()`(`:icon` 是表达式)。
显示条件与 avue 一致(`header-search3.js` 的 `isSearchIcon = searchIcon===true && columnLen > searchIndex`):搜索项 **≤4 项不渲染按钮** —— 故 data 里维护 `searchIndex: 4` + `searchItemCount: N`,用 computed `showSearchToggle` 控制。搜索栏操作区 `gap: 8px` 各模块一致,**不要去改**。
## 表单 / 自检 / 导出
- placeholder 只写 `请输入`/`请选择`;例外:`请输入或选择车辆`、日期区间 `起`/`止`、示例值。校验 message 须带字段名。
- `business-crud-page.vue`:option 经 `cloneOption` 克隆,独立表单页走 `PageAvueForm`。
- 自检:`curl "localhost:2889/src/xxx.vue"` 与 `"...?vue&type=style&index=0&scoped=true&lang.scss"`(200 / 500)。`vite build` 在 `facelogin.vue` 被沙箱中断,不能作唯一校验。dev `VITE_APP_API=/api` 代理 `172.16.203.228:8000`。
- 自检:`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`)。
## Element Plus 图标两种用法的机制差异(易误判)
@@ -69,3 +74,10 @@
但 `:icon="OfficeBuilding"` 是**表达式**,编译成 `_ctx.OfficeBuilding`,只会从 data / props / computed / setup 返回值里取 —— **组件对象必须放进 `data()`**;放进 `components: {}` 注册里模板取不到,会静默渲染不出图标(不报错,只是空白)。
项目既有惯例:需要 `:icon` 的图标一律 `import` 后在 `data()` 里 `return { Xxx }`(transport-plan-page、waybill-manage-page、business-crud-page 都是这么写的)。
注:标签形式(如 `<Location />`)不依赖 data,放不放 data 都能显示;仅 `:icon` 必须放 data。
## 全局 `showOverflowTooltip: true` 与自定义多行单元格(2026-10-14 踩)
`main.js` 的 `app.use(ElementPlus, { table: { showOverflowTooltip: true } })`(+ Avue `crudOption.showOverflowTooltip: true`)→ **全站所有表格列默认开启 hover 提示**,EP 会给该列 `.cell` 加 `.el-tooltip` 类(`white-space: nowrap` + 固定宽度 + 溢出省略)。
后果:**列里放自定义多行 / 分组内容时,内层文本继承 nowrap → 横向溢出 → 单元格被裁切 + hover 弹提示**(极易被误判成"组件 bug")。
修法(列级豁免,不动全局):① 列上显式 `:show-overflow-tooltip="false"`;② 内层容器补 `white-space: normal` 兜底。两处都写最稳。
☑️ 已用于路线变更「变更内容」列(`loading-manage.vue` / `components/waybill-manage-page.vue` / `components/business-crud-page.vue` 三处)。
另:`styles/element-ui.scss` 里已有两条基于 `.cell.el-tooltip` 的既有豁免规则(操作列 `:has(.el-link)`、`class-name="is-ellipsis-link"`),改表格列时先看这两条。
+415 -95
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -10,6 +10,13 @@ export const getReferenceAmount = (paymentType, referenceId, excludeId) =>
method: 'get',
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 remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
+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 getNextNo = () => request({ url: `${baseUrl}/next-no`, method: 'get' });
export const getCandidateDetails = (current, size, params) =>
request({
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 });
+4 -1
View File
@@ -38,7 +38,6 @@ export const createOption = () => ({
delBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 240,
menuFixed: 'right',
column: [
{
@@ -112,6 +111,7 @@ export const createOption = () => ({
{
label: '创建人',
prop: 'createUserName',
minWidth: 110,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
@@ -120,6 +120,7 @@ export const createOption = () => ({
{
label: '更新人',
prop: 'updateUserName',
minWidth: 110,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
@@ -131,6 +132,7 @@ export const createOption = () => ({
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
width: 180,
sortable: true,
addDisplay: false,
editDisplay: false,
@@ -142,6 +144,7 @@ export const createOption = () => ({
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
width: 180,
sortable: true,
addDisplay: false,
editDisplay: false,
@@ -35,8 +35,8 @@ export const sourceColumns = [
];
export const detailColumns = [
{ label: '单据号', prop: 'documentNo', minWidth: 180 },
{ label: '运单号', prop: 'waybillNo', minWidth: 160 },
{ label: '单据号', prop: 'documentNo', minWidth: 180, link: true },
{ label: '运单号', prop: 'waybillNo', minWidth: 160, link: true },
{ label: '车号', prop: 'vehicleNo', minWidth: 130 },
{ label: '发货地址', prop: 'departureAddress', minWidth: 180 },
{ label: '到货地址', prop: 'arrivalAddress', minWidth: 180 },
+1 -1
View File
@@ -40,7 +40,7 @@ export const settlementSummaryColumns = [
export const settlementDetailColumns = [
{ 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: 'departureAddress', minWidth: 180 },
{ label: '到货地址', prop: 'arrivalAddress', minWidth: 180 },
+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',
component: Layout,
+65 -13
View File
@@ -61,9 +61,18 @@ export const MK_BIZ = {
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' });
return res?.data?.data || [];
mkTemplateListCache = res?.data?.data || [];
return mkTemplateListCache;
}
/** 业务字典 mk_template:名称「开启MK」且键值为 1 时才请求 MK */
@@ -72,10 +81,37 @@ export function isMkEnabled(list = []) {
return String(matched?.dictKey ?? '').trim() === '1';
}
/** 加载业务字典并判断是否开启 MK */
export async function loadMkEnabled() {
const list = await loadMkTemplateList();
return isMkEnabled(list);
/** 清除 MK 开关缓存(字典变更后可调用) */
export function clearMkEnabledCache() {
mkEnabledCache = null;
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) {
@@ -99,19 +135,22 @@ export function isMkApprovedStatus(bizType, approvalStatus) {
}
/**
* 提交 MK 审核流
* 提交 MK 审核流。未开启 MK 时直接跳过,不请求流程实例相关接口。
* @param {Object} options
* @param {string} options.bizType 业务类型
* @param {string|number} options.bizId 业务主键(后端据此生成/复用 formInstanceId)
* @param {string|number} options.bizId 业务主键
* @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({
bizType,
bizId,
subjectName = '',
approvalStatus = '',
}) {
mkEnabled,
} = {}) {
const conf = MK_BIZ[bizType];
if (!conf) {
return Promise.reject(new Error(`不支持的MK业务类型:${bizType}`));
@@ -119,17 +158,29 @@ export async function submitMkApprovalFlow({
if (bizId == null || bizId === '') {
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();
if (!isMkEnabled(list)) {
return;
mkEnabledCache = false;
return false;
}
const bizIdText = String(bizId);
const forceNewFormInstance = isMkApprovedStatus(bizType, approvalStatus);
// 审核通过后再提交必须新建流程;其余场景已有有效实例则跳过
if (!forceNewFormInstance) {
const existsRes = await hasMkProcessInstance({ bizId: bizIdText });
if (existsRes?.data?.data) {
return;
return true;
}
}
if ((conf.rejected || []).includes(approvalStatus)) {
@@ -139,11 +190,12 @@ export async function submitMkApprovalFlow({
const subject = subjectName
? `${conf.subjectPrefix}:${subjectName}`
: `${conf.subjectPrefix}:${bizIdText}`;
return processSubmit({
await processSubmit({
templateCode,
bizId: bizIdText,
subject,
bizType,
forceNewFormInstance,
});
return true;
}
+123 -19
View File
@@ -1,29 +1,133 @@
/**
* 路线变更记录内容格式化
* 路线变更记录内容解析
*
* 存储格式(单行):
* YD-20260904-0022:发货地 A → B;到货地 C → D;YD-20260904-0025:发货地 E → F;到货地 G → H
* 路线顺序:A → B → C 调整为 B → A → C
*
* 展示格式(变更前 / 变更后分行):
* YD-20260904-0022:发货地 A →
* B
* 到货地 C →
* D
* 解析结果(分组,供变更记录列表按「分组标题 - 字段 - 变更前/后」渲染):
* [
* {
* title: 'YD-20260904-0022',
* text: '',
* fields: [{ label: '发货地', before: 'A', after: 'B', single: 'B', changed: true, unchanged: false }],
* },
* { title: '', text: '无法识别的原文', fields: [] },
* ]
*
* 说明:仅用于展示层,不改变已存储的 content 文本(历史数据同样适用)。
*/
export const formatRouteChangeContent = (content, emptyText = '-') => {
if (content === undefined || content === null) return emptyText;
const text = String(content).trim();
if (!text) return emptyText;
// 长标签必须排在短标签前(「发货地址」优先于「发货地」),否则会被前缀吃掉
const FIELD_LABELS = ['发货地址', '到货地址', '发货地', '到货地', '装货地', '卸货地', '收货地'];
const FIELD_LABEL_PATTERN = new RegExp(`^(${FIELD_LABELS.join('|')})\\s*([\\s\\S]*)$`);
// 变更前 / 变更后分隔符:→、➔、➜、->、-->、–>、—>
const ARROW_PATTERN = /\s*(?:→|➔|➜|[\-–—]{1,2}\s*>)\s*/;
// 运单地址之外的变更描述,例如「A → B → C 调整为 B → A → C」
const ADJUST_PATTERN = /^([\s\S]*?)\s调整为\s*([\s\S]*)$/;
const EMPTY_TEXT = ['', '-', '--', '—', 'null', 'undefined'];
// 冒号前内容超过该长度时不视为分组标题(运单号通常长于该值)
const TITLE_MAX_LENGTH = 12;
return text
.replace(/\r\n?/g, '\n')
// 多条记录之间换行
.replace(/[ \t]*[;;][ \t]*/g, '\n')
// 变更前地址 / 变更后地址分行(箭头保留在上一行行尾)
.replace(/[ \t]*[-—]+>[ \t]*|[ \t]*→[ \t]*/g, ' →\n')
.replace(/\n{2,}/g, '\n')
.replace(/[ \t]+\n/g, '\n')
.trim();
const normalizeValue = value => {
const text = String(value === undefined || value === null ? '' : value).trim();
return EMPTY_TEXT.includes(text.toLowerCase()) ? '-' : text;
};
const buildField = (label, rawValue) => {
const value = String(rawValue === undefined || rawValue === null ? '' : rawValue).trim();
const adjustMatched = value.match(ADJUST_PATTERN);
let before = value;
let after = '';
let hasPair = false;
if (adjustMatched) {
before = adjustMatched[1];
after = adjustMatched[2];
hasPair = true;
} else {
const parts = value.split(ARROW_PATTERN);
if (parts.length > 1) {
before = parts[0];
after = parts[1];
hasPair = true;
}
}
const beforeText = normalizeValue(before);
const afterText = normalizeValue(after);
return {
label,
before: beforeText,
after: afterText,
// 单值展示(未拆分出前后值时使用)
single: hasPair ? afterText : beforeText,
changed: hasPair && beforeText !== afterText,
unchanged: hasPair && beforeText === afterText,
};
};
const parseSegment = segment => {
// 形如「发货地 A → B」(前面只有「;」分隔,无分组标题)→ 归入上一条运单
const leadingMatched = segment.match(FIELD_LABEL_PATTERN);
if (leadingMatched && leadingMatched[2].trim()) {
return {
type: 'field',
title: '',
text: '',
fields: [buildField(leadingMatched[1], leadingMatched[2])],
};
}
const colonIndex = segment.search(/[::]/);
if (colonIndex === -1) {
return { type: 'text', title: '', text: segment, fields: [] };
}
const head = segment.slice(0, colonIndex).trim();
const tail = segment.slice(colonIndex + 1).trim();
const fieldMatched = tail.match(FIELD_LABEL_PATTERN);
// 形如「YD-20260904-0022:发货地 A → B」
if (fieldMatched && fieldMatched[2].trim()) {
return {
type: 'waybill',
title: head,
text: '',
fields: [buildField(fieldMatched[1], fieldMatched[2])],
};
}
// 形如「路线顺序:A → B → C 调整为 B → A → C」
if (head && head.length <= TITLE_MAX_LENGTH) {
return { type: 'group', title: head, text: '', fields: [buildField('', tail)] };
}
return { type: 'text', title: '', text: segment, fields: [] };
};
export const parseRouteChangeContent = content => {
if (content === undefined || content === null) return [];
const text = String(content).replace(/\r\n?/g, '\n').trim();
if (!text) return [];
const segments = text
.split(/[;;\n]+/)
.map(item => item.trim())
.filter(Boolean);
const groups = [];
segments.forEach(segment => {
const group = parseSegment(segment);
const last = groups[groups.length - 1];
// 同一运单下的「到货地…」等无标题字段,合并进上一个运单分组
if (group.type === 'field' && last && (last.type === 'waybill' || last.type === 'field')) {
last.fields.push(...group.fields);
return;
}
groups.push(group);
});
return groups;
};
+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 = {}) => {
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 { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { omitInvalidListStatus } from '@/utils/search-range';
import { getToken, isAdministrator } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
@@ -1182,7 +1183,7 @@ export default {
},
onLoad(page, params = {}) {
this.loading = true;
getList(page.currentPage, page.pageSize, { ...params, ...this.query })
getList(page.currentPage, page.pageSize, omitInvalidListStatus({ ...params, ...this.query }))
.then(res => {
const data = res.data.data;
this.page.total = data.total;
+2 -1
View File
@@ -104,6 +104,7 @@ import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { omitInvalidListStatus } from '@/utils/search-range';
import { getToken } from '@/utils/auth';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
@@ -507,7 +508,7 @@ export default {
this.onLoad(this.page, this.query);
},
buildQuery(params = {}) {
const values = { ...params, ...this.query };
const values = omitInvalidListStatus({ ...params, ...this.query });
const rangeMap = {
effectiveDateRange: ['effectiveDateStart', 'effectiveDateEnd'],
expiryDateRange: ['expiryDateStart', 'expiryDateEnd'],
+2 -1
View File
@@ -63,6 +63,7 @@
<script>
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/customer-type';
import { omitInvalidListStatus } from '@/utils/search-range';
import { mapGetters } from 'vuex';
export default {
@@ -290,7 +291,7 @@ export default {
},
onLoad(page, params = {}) {
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;
this.page.total = data.total;
this.data = data.records;
+2 -1
View File
@@ -97,6 +97,7 @@ import { exportBlob } from '@/api/common';
import { getDictionaryAll } from '@/api/system/dictbiz';
import { getDeptTree } from '@/api/system/dept';
import { downloadXls } from '@/utils/util';
import { omitInvalidListStatus } from '@/utils/search-range';
import { getToken } from '@/utils/auth';
import { Download } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
@@ -569,7 +570,7 @@ export default {
},
onLoad(page, params = {}) {
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;
this.page.total = data.total;
this.data = data.records;
+2 -1
View File
@@ -82,6 +82,7 @@
import { mapGetters } from 'vuex';
import { changeStatus, getDetail, getList, remove, submit } from '@/api/base/measurement-unit';
import { createOption } from '@/option/base/measurement-unit';
import { omitInvalidListStatus } from '@/utils/search-range';
import { isAdministrator } from '@/utils/auth';
export default {
@@ -218,7 +219,7 @@ export default {
},
onLoad(page = this.page, params = this.query) {
this.loading = true;
return getList(page.currentPage, page.pageSize, params)
return getList(page.currentPage, page.pageSize, omitInvalidListStatus(params))
.then(res => {
const pageData = res.data.data || {};
this.data = pageData.records || [];
+2 -1
View File
@@ -270,6 +270,7 @@ import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { omitInvalidListStatus } from '@/utils/search-range';
import { getToken, isAdministrator } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
@@ -1636,7 +1637,7 @@ export default {
},
onLoad(page, params = {}) {
this.loading = true;
getList(page.currentPage, page.pageSize, { ...params, ...this.query })
getList(page.currentPage, page.pageSize, omitInvalidListStatus({ ...params, ...this.query }))
.then(res => {
const data = res.data.data;
this.page.total = data.total;
+2 -1
View File
@@ -150,6 +150,7 @@ import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { omitInvalidListStatus } from '@/utils/search-range';
import { getToken, isAdministrator } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
@@ -1157,7 +1158,7 @@ export default {
},
onLoad(page, params = {}) {
this.loading = true;
getList(page.currentPage, page.pageSize, { ...params, ...this.query })
getList(page.currentPage, page.pageSize, omitInvalidListStatus({ ...params, ...this.query }))
.then(res => {
const data = res.data.data;
this.page.total = data.total;
+52 -7
View File
@@ -396,8 +396,11 @@ export default {
return this.selectionList.map(item => item.id).join(',');
},
},
created() {
this.applyDefaultDeptSearch();
created() {
// 超管默认查全部组织,不预填组织筛选;普通用户预填当前组织。
if (!this.isAdmin) {
this.applyDefaultDeptSearch();
}
this.initDeptTree();
},
methods: {
@@ -455,10 +458,23 @@ export default {
normalizeListQuery(params = {}) {
// 搜索条件优先于缓存的 query,避免默认组织 ID 覆盖用户新选择的组织。
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;
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 {
delete query.deptId;
}
@@ -466,6 +482,23 @@ export default {
return query;
},
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 deptColumn = this.findColumn(this.option.column, 'searchDeptId');
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() {
this.addressQuery = {
addressName: '',
@@ -933,7 +971,8 @@ export default {
}
api.submit(submitRow).then(
() => {
this.onLoad(this.page);
this.page.currentPage = 1;
this.onLoad(this.page, this.query);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
@@ -951,7 +990,7 @@ export default {
}
api.submit(submitRow).then(
() => {
this.onLoad(this.page);
this.onLoad(this.page, this.query);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
@@ -1006,7 +1045,13 @@ export default {
beforeOpen(done, type) {
this.dialogReadonly = type === 'view';
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();
return;
}
@@ -2991,12 +2991,51 @@
<el-table :data="waybillRouteChangeRecords" border>
<el-table-column prop="changeTime" label="变更时间" min-width="170" />
<el-table-column prop="changeUser" label="变更人" min-width="120" />
<el-table-column prop="content" label="变更内容" min-width="360">
<el-table-column
prop="content"
label="变更内容"
min-width="360"
:show-overflow-tooltip="false"
>
<template #default="{ row }">
<div
class="business-crud-page__route-change-record-content"
v-text="formatRouteChangeContent(row.content)"
/>
<div class="business-crud-page__route-change-record-content">
<div
v-for="(group, groupIndex) in parseRouteChangeContent(row.content)"
:key="groupIndex"
class="business-crud-page__route-change-group"
>
<div v-if="group.title" class="business-crud-page__route-change-group-title">
{{ group.title }}
</div>
<div v-if="group.text" class="business-crud-page__route-change-group-text">
{{ group.text }}
</div>
<div
v-for="(field, fieldIndex) in group.fields"
:key="fieldIndex"
class="business-crud-page__route-change-field"
>
<span class="business-crud-page__route-change-field-label">
{{ field.label }}
</span>
<template v-if="field.changed">
<span class="business-crud-page__route-change-before">
{{ field.before }}
</span>
<span class="business-crud-page__route-change-arrow">→</span>
<span class="business-crud-page__route-change-after">
{{ field.after }}
</span>
</template>
<span v-else class="business-crud-page__route-change-single">
{{ field.single }}
<span v-if="field.unchanged" class="business-crud-page__route-change-flag">
未变更
</span>
</span>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
@@ -5283,7 +5322,7 @@ import { formatContractOptionLabel } from '@/utils/contract';
import { openImportDialog } from '@/utils/import-excel';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
import { formatRouteChangeContent } from '@/utils/route-change';
import { parseRouteChangeContent } from '@/utils/route-change';
import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate';
@@ -6630,7 +6669,7 @@ export default {
},
methods: {
formatContractOptionLabel,
formatRouteChangeContent,
parseRouteChangeContent,
initStandaloneFormPage() {
if (!this.isStandaloneFormPage) return;
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
@@ -15900,10 +15939,65 @@ export default {
}
&__route-change-record-content {
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
display: flex;
flex-direction: column;
gap: 10px;
line-height: 22px;
white-space: normal;
text-align: left;
}
&__route-change-group {
padding-left: 8px;
border-left: 2px solid #409eff;
}
&__route-change-group-title {
margin-bottom: 4px;
color: #409eff;
font-weight: 500;
}
&__route-change-group-text {
color: #606266;
word-break: break-word;
overflow-wrap: anywhere;
}
&__route-change-field {
display: grid;
grid-template-columns: 54px minmax(0, 1fr);
column-gap: 8px;
row-gap: 2px;
}
&__route-change-field-label {
color: #909399;
white-space: nowrap;
}
&__route-change-before,
&__route-change-single {
color: #606266;
word-break: break-word;
overflow-wrap: anywhere;
}
&__route-change-arrow {
color: #c0c4cc;
text-align: right;
}
&__route-change-after {
color: #409eff;
word-break: break-word;
overflow-wrap: anywhere;
}
&__route-change-flag {
margin-left: 6px;
color: #c0c4cc;
font-size: 12px;
}
@media (max-width: 1200px) {
@@ -3020,12 +3020,57 @@
<el-table :data="waybillRouteChangeRecords" border>
<el-table-column prop="changeTime" label="变更时间" min-width="170" />
<el-table-column prop="changeUser" label="变更人" min-width="120" />
<el-table-column prop="content" label="变更内容" min-width="360">
<el-table-column
prop="content"
label="变更内容"
min-width="360"
:show-overflow-tooltip="false"
>
<template #default="{ row }">
<div
class="waybill-manage-page__route-change-record-content"
v-text="formatRouteChangeContent(row.content)"
/>
<div class="waybill-manage-page__route-change-record-content">
<div
v-for="(group, groupIndex) in parseRouteChangeContent(row.content)"
:key="groupIndex"
class="waybill-manage-page__route-change-group"
>
<div
v-if="group.title"
class="waybill-manage-page__route-change-group-title"
>
{{ group.title }}
</div>
<div v-if="group.text" class="waybill-manage-page__route-change-group-text">
{{ group.text }}
</div>
<div
v-for="(field, fieldIndex) in group.fields"
:key="fieldIndex"
class="waybill-manage-page__route-change-field"
>
<span class="waybill-manage-page__route-change-field-label">
{{ field.label }}
</span>
<template v-if="field.changed">
<span class="waybill-manage-page__route-change-before">
{{ field.before }}
</span>
<span class="waybill-manage-page__route-change-arrow">→</span>
<span class="waybill-manage-page__route-change-after">
{{ field.after }}
</span>
</template>
<span v-else class="waybill-manage-page__route-change-single">
{{ field.single }}
<span
v-if="field.unchanged"
class="waybill-manage-page__route-change-flag"
>
未变更
</span>
</span>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
@@ -3667,7 +3712,7 @@ import { wgs84ToGcj02 } from '@/utils/coordinate';
import { openImportDialog } from '@/utils/import-excel';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
import { formatRouteChangeContent } from '@/utils/route-change';
import { parseRouteChangeContent } from '@/utils/route-change';
import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate';
@@ -4755,7 +4800,7 @@ export default {
},
methods: {
formatContractOptionLabel,
formatRouteChangeContent,
parseRouteChangeContent,
// 页面被 keep-alive 缓存/卸载时调用,关闭本页所有 append-to-body 的弹窗,
// 避免 Teleport 出去的 DOM 残留在 body 上盖住后续页面(详见 data 里的说明)。
closeInnerDialogs() {
@@ -11846,10 +11891,65 @@ export default {
}
&__route-change-record-content {
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
display: flex;
flex-direction: column;
gap: 10px;
line-height: 22px;
white-space: normal;
text-align: left;
}
&__route-change-group {
padding-left: 8px;
border-left: 2px solid #409eff;
}
&__route-change-group-title {
margin-bottom: 4px;
color: #409eff;
font-weight: 500;
}
&__route-change-group-text {
color: #606266;
word-break: break-word;
overflow-wrap: anywhere;
}
&__route-change-field {
display: grid;
grid-template-columns: 54px minmax(0, 1fr);
column-gap: 8px;
row-gap: 2px;
}
&__route-change-field-label {
color: #909399;
white-space: nowrap;
}
&__route-change-before,
&__route-change-single {
color: #606266;
word-break: break-word;
overflow-wrap: anywhere;
}
&__route-change-arrow {
color: #c0c4cc;
text-align: right;
}
&__route-change-after {
color: #409eff;
word-break: break-word;
overflow-wrap: anywhere;
}
&__route-change-flag {
margin-left: 6px;
color: #c0c4cc;
font-size: 12px;
}
&__common-address-toolbar {
@@ -332,7 +332,7 @@
<script>
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 ContractAttachmentSection, {
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); },
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; },
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>
+6 -2
View File
@@ -1986,7 +1986,9 @@ export default {
bizType: 'contract-manage',
bizId: id,
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,
subjectName: payload.contractName || this.form.contractName || '',
approvalStatus: this.form.approvalStatus || 'change_rejected',
mkEnabled: this.mkEnabled,
}).then(() => {
this.$message.success('已重新提交变更审批');
this.closeForm();
@@ -3001,6 +3004,7 @@ export default {
bizId: row.id,
subjectName: row.contractName || '',
approvalStatus: row.approvalStatus || '',
mkEnabled: this.mkEnabled,
})
)
: request();
@@ -3338,7 +3342,7 @@ export default {
:deep(.el-form-item) {
align-items: center;
margin-bottom: 24px;
//margin-bottom: 24px;
}
:deep(.el-form-item__label) {
+100 -7
View File
@@ -1683,9 +1683,56 @@
><el-table-column prop="changeTime" label="变更时间" min-width="170" /><el-table-column
prop="changeUser"
label="变更人"
min-width="120" /><el-table-column prop="content" label="变更内容" min-width="360"
min-width="120" /><el-table-column
prop="content"
label="变更内容"
min-width="360"
:show-overflow-tooltip="false"
><template #default="{ row }"
><div class="loading-route-change-dialog__record-content">{{ formatRouteChangeContent(row.content) }}</div></template
><div class="loading-route-change-dialog__record-content">
<div
v-for="(group, groupIndex) in parseRouteChangeContent(row.content)"
:key="groupIndex"
class="loading-route-change-dialog__change-group"
>
<div
v-if="group.title"
class="loading-route-change-dialog__change-group-title"
>
{{ group.title }}
</div>
<div v-if="group.text" class="loading-route-change-dialog__change-group-text">
{{ group.text }}
</div>
<div
v-for="(field, fieldIndex) in group.fields"
:key="fieldIndex"
class="loading-route-change-dialog__change-field"
>
<span class="loading-route-change-dialog__change-field-label">
{{ field.label }}
</span>
<template v-if="field.changed">
<span class="loading-route-change-dialog__change-before">
{{ field.before }}
</span>
<span class="loading-route-change-dialog__change-arrow">→</span>
<span class="loading-route-change-dialog__change-after">
{{ field.after }}
</span>
</template>
<span v-else class="loading-route-change-dialog__change-single">
{{ field.single }}
<span
v-if="field.unchanged"
class="loading-route-change-dialog__change-flag"
>
未变更
</span>
</span>
</div>
</div>
</div></template
></el-table-column
><el-table-column
prop="remark"
@@ -1798,7 +1845,7 @@ import TableHeaderTools from '@/components/table-header-tools/main.vue';
import dayjs from 'dayjs';
import { isMobile } from '@/utils/validate';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
import { formatRouteChangeContent } from '@/utils/route-change';
import { parseRouteChangeContent } from '@/utils/route-change';
const defaultTransportType = '';
const defaultCandidateTransportType = '';
@@ -2277,7 +2324,7 @@ export default {
},
},
methods: {
formatRouteChangeContent,
parseRouteChangeContent,
formatTaskCarrierContractLabel(item = {}) {
const carrierName = item.carrierName || item.partyB || '';
const contractLabel = formatContractOptionLabel(item);
@@ -5129,10 +5176,56 @@ export default {
gap: 4px;
}
.loading-route-change-dialog__record-content {
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
display: flex;
flex-direction: column;
gap: 10px;
line-height: 22px;
white-space: normal;
text-align: left;
}
.loading-route-change-dialog__change-group {
padding-left: 8px;
border-left: 2px solid #409eff;
}
.loading-route-change-dialog__change-group-title {
margin-bottom: 4px;
color: #409eff;
font-weight: 500;
}
.loading-route-change-dialog__change-group-text {
color: #606266;
word-break: break-word;
overflow-wrap: anywhere;
}
.loading-route-change-dialog__change-field {
display: grid;
grid-template-columns: 54px minmax(0, 1fr);
column-gap: 8px;
row-gap: 2px;
}
.loading-route-change-dialog__change-field-label {
color: #909399;
white-space: nowrap;
}
.loading-route-change-dialog__change-before,
.loading-route-change-dialog__change-single {
color: #606266;
word-break: break-word;
overflow-wrap: anywhere;
}
.loading-route-change-dialog__change-arrow {
color: #c0c4cc;
text-align: right;
}
.loading-route-change-dialog__change-after {
color: #409eff;
word-break: break-word;
overflow-wrap: anywhere;
}
.loading-route-change-dialog__change-flag {
margin-left: 6px;
color: #c0c4cc;
font-size: 12px;
}
}
.loading-route-change-map-dialog {
+29 -7
View File
@@ -79,7 +79,7 @@
<p>{{ timeRange(row) }}</p>
<p>{{ goodsSummary(row) }}</p>
<p>创建人:{{ row.createUserName || '-' }}</p>
<p>创建时间:{{ row.createTime || '-' }}</p>
<p>创建时间:{{ formatDate(row.createTime) }}</p>
</section>
<section class="route-progress">
<div class="route-track">
@@ -155,6 +155,7 @@
<script>
import * as api from '@/api/business/master-order';
import { splitRegionTokens } from '@/utils/region-address';
import TableHeaderTools from '@/components/table-header-tools/main.vue';
import MasterOrderEditor from './components/master-order-editor.vue';
import MasterOrderDispatch from './components/master-order-dispatch.vue';
@@ -332,6 +333,10 @@ export default {
? `${String(row.planStartTime).slice(0, 10)} - ${String(row.planEndTime).slice(0, 10)}`
: '-';
},
// 创建时间只展示日期。
formatDate(value) {
return value ? String(value).slice(0, 10) : '-';
},
goodsSummary(row) {
const item = (row.goods || [])[0] || {};
if (!item.cargoName && !item.cargoType) return '-';
@@ -391,6 +396,26 @@ export default {
}`;
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 = {}) {
const text = String(value || '').trim();
if (!text) return '-';
@@ -457,15 +482,12 @@ export default {
if (this.isStationLikeName(rawAddress) && !this.isStationLikeName(rawName)) {
return {
name: rawAddress,
address: rawName || regionText || rawAddress,
address: this.completeAddress(rawName, rawAddress) || regionText || rawAddress,
};
}
const displayName = rawName || rawAddress || '-';
const tipAddress =
(rawAddress && rawAddress !== displayName ? rawAddress : '') ||
regionText ||
rawAddress ||
displayName;
this.completeAddress(rawName, rawAddress) || regionText || rawAddress || displayName;
return { name: displayName, address: tipAddress };
}
return {
@@ -475,7 +497,7 @@ export default {
siteCode,
nextTransportType,
}),
address: rawAddress || rawName || regionText || '-',
address: this.completeAddress(rawName, rawAddress) || regionText || '-',
};
},
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 { exportBlob } from '@/api/common';
import { config, option } from '@/option/business/process-config';
import { applyIncomingSearch } from '@/utils/search-range';
import { getToken, isAdministrator } from '@/utils/auth';
import { downloadXls } from '@/utils/util';
import { mapGetters } from 'vuex';
@@ -603,7 +604,8 @@ export default {
});
},
normalizeListQuery(params = {}) {
const query = { ...params, ...this.query };
// 新条件覆盖旧条件,清空的筛选项不再沿用上一次的值。
const query = applyIncomingSearch(this.query, params);
const rawDeptId = query.deptId ?? query.searchDeptId;
delete query.searchDeptId;
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
+6 -2
View File
@@ -1750,6 +1750,7 @@ export default {
bizId: row.id,
subjectName: row.projectName || '',
approvalStatus: row.approvalStatus || '',
mkEnabled: this.mkEnabled,
}).then(() => request())
: request();
afterMk.then(res => {
@@ -1956,6 +1957,7 @@ export default {
bizId: id,
subjectName: name,
approvalStatus: status,
mkEnabled: this.mkEnabled,
}).then(() => {
this.$message.success('提交成功!');
this.closeProjectForm();
@@ -2027,6 +2029,7 @@ export default {
bizId: id,
subjectName: this.form.projectName || '',
approvalStatus: this.form.approvalStatus || 'change_rejected',
mkEnabled: this.mkEnabled,
}).then(() => {
this.$message.success('提交成功!');
this.closeProjectForm();
@@ -2503,8 +2506,9 @@ export default {
const list = res.data.data || [];
this.platformCompanyOptions = list.map(item => ({
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();
});
},
+2 -1
View File
@@ -140,7 +140,7 @@ import * as api from '@/api/payment/billPayment';
import * as billLedgerApi from '@/api/payment/billLedger';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { getMkPublicDetail } from '@/api/mk-process';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
const emptyForm = () => ({
id: null,
@@ -348,6 +348,7 @@ export default {
bizId: this.form.id,
subjectName: this.form.paymentNo || this.form.billNo || '',
approvalStatus: previousApprovalStatus,
mkEnabled: await loadMkEnabled().catch(() => false),
});
this.$message.success('提交成功');
this.goBack();
+1
View File
@@ -132,6 +132,7 @@ export default {
bizId: row.id,
subjectName: row.paymentNo || row.billNo || '',
approvalStatus: previousApprovalStatus,
mkEnabled: this.mkEnabled,
});
this.$message.success('提交成功');
this.loadTable();
@@ -52,12 +52,13 @@
<el-icon><Delete /></el-icon>
<span>重置</span>
</el-button>
<el-tooltip :content="expanded ? '折叠' : '展开'" placement="top">
<el-button text @click="expanded = !expanded">
<el-icon><component :is="expanded ? ArrowUp : ArrowDown" /></el-icon>
<span>{{ expanded ? '折叠' : '展开' }}</span>
</el-button>
</el-tooltip>
<el-button
v-if="showSearchToggle"
text
:icon="expanded ? ArrowUp : ArrowDown"
@click="expanded = !expanded"
>{{ expanded ? '收起' : '展开' }}</el-button
>
</div>
</el-form>
</section>
@@ -81,10 +82,18 @@ export default {
Delete,
Search,
expanded: false,
// 与 avue-crud 一致:搜索项数量超过 searchIndex(默认一行 4 项)时才显示“展开/收起”
searchIndex: 4,
searchItemCount: 6,
billTypeOptions,
maturityStatusOptions,
};
},
computed: {
showSearchToggle() {
return this.searchItemCount > this.searchIndex;
},
},
};
</script>
@@ -43,11 +43,13 @@
<div class="bill-payment-search__actions">
<el-button type="primary" :loading="loading" @click="$emit('search')">查询</el-button>
<el-button @click="$emit('reset')">重置</el-button>
<el-tooltip :content="expanded ? '折叠' : '展开'" placement="top">
<el-button text @click="expanded = !expanded">
<el-icon><component :is="expanded ? ArrowUp : ArrowDown" /></el-icon>
</el-button>
</el-tooltip>
<el-button
v-if="showSearchToggle"
text
:icon="expanded ? ArrowUp : ArrowDown"
@click="expanded = !expanded"
>{{ expanded ? '收起' : '展开' }}</el-button
>
</div>
</el-form>
</section>
@@ -67,7 +69,20 @@ export default {
},
emits: ['search', 'reset'],
data() {
return { ArrowDown, ArrowUp, expanded: false, approvalStatusOptions };
return {
ArrowDown,
ArrowUp,
expanded: false,
// 与 avue-crud 一致:搜索项数量超过 searchIndex(默认一行 4 项)时才显示“展开/收起”
searchIndex: 4,
searchItemCount: 4,
approvalStatusOptions,
};
},
computed: {
showSearchToggle() {
return this.searchItemCount > this.searchIndex;
},
},
mounted() {
this.loadOrganizationOptions();
@@ -492,7 +492,7 @@ import { getDictionary } from '@/api/system/dictbiz';
import { invoiceApplicationDetailColumns } from '@/option/payment/invoiceApplication';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { getMkPublicDetail } from '@/api/mk-process';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
let localId = 0;
const emptyLine = () => ({
@@ -1208,6 +1208,7 @@ export default {
bizId: this.form.id,
subjectName: this.form.applicationNo || '',
approvalStatus: previousApprovalStatus,
mkEnabled: await loadMkEnabled().catch(() => false),
});
this.$message.success('提交成功');
this.goBack();
+24 -5
View File
@@ -45,11 +45,13 @@
<div class="invoice-page__search-actions">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
<el-tooltip :content="searchExpanded ? '折叠' : '展开'" placement="top">
<el-button text @click="searchExpanded = !searchExpanded">
<el-icon><component :is="searchExpanded ? ArrowUp : ArrowDown" /></el-icon>
</el-button>
</el-tooltip>
<el-button
v-if="showSearchToggle"
text
:icon="searchExpanded ? ArrowUp : ArrowDown"
@click="searchExpanded = !searchExpanded"
>{{ searchExpanded ? '收起' : '展开' }}</el-button
>
</div>
</el-form>
</section>
@@ -256,6 +258,9 @@ export default {
ArrowUp,
query: emptyQuery(),
searchExpanded: false,
// 与 avue-crud 一致:搜索项数量超过 searchIndex(默认一行 4 项)时才显示“展开/收起”
searchIndex: 4,
searchItemCount: 5,
searchVisible: true,
tableSize: 'default',
approvalStatusOptions: api.approvalStatusOptions,
@@ -275,6 +280,9 @@ export default {
visibleColumns() {
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
},
showSearchToggle() {
return this.searchItemCount > this.searchIndex;
},
},
mounted() {
this.loadOrganizationOptions();
@@ -376,6 +384,7 @@ export default {
bizId: row.id,
subjectName: row.applicationNo || '',
approvalStatus: previousApprovalStatus,
mkEnabled: this.mkEnabled,
});
this.$message.success('提交成功');
this.loadTable();
@@ -496,6 +505,16 @@ export default {
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) {
+2 -1
View File
@@ -219,7 +219,7 @@ import * as api from '@/api/payment/invoiceReceipt';
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { getMkPublicDetail } from '@/api/mk-process';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
const emptyForm = () => ({
id: null,
@@ -571,6 +571,7 @@ export default {
bizId: this.form.id,
subjectName: this.form.invoiceNo || '',
approvalStatus: previousApprovalStatus,
mkEnabled: await loadMkEnabled().catch(() => false),
});
this.$message.success('提交成功');
this.goBack();
+14 -5
View File
@@ -54,11 +54,13 @@
<div class="receipt-page__search-actions">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
<el-tooltip :content="searchExpanded ? '折叠' : '展开'" placement="top">
<el-button text @click="searchExpanded = !searchExpanded">
<el-icon><component :is="searchExpanded ? ArrowUp : ArrowDown" /></el-icon>
</el-button>
</el-tooltip>
<el-button
v-if="showSearchToggle"
text
:icon="searchExpanded ? ArrowUp : ArrowDown"
@click="searchExpanded = !searchExpanded"
>{{ searchExpanded ? '收起' : '展开' }}</el-button
>
</div>
</el-form>
</section>
@@ -241,6 +243,9 @@ export default {
ArrowUp,
query: emptyQuery(),
searchExpanded: false,
// 与 avue-crud 一致:搜索项数量超过 searchIndex(默认一行 4 项)时才显示“展开/收起”
searchIndex: 4,
searchItemCount: 6,
searchVisible: true,
tableSize: 'default',
approvalStatusOptions: api.approvalStatusOptions,
@@ -260,6 +265,9 @@ export default {
visibleColumns() {
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
},
showSearchToggle() {
return this.searchItemCount > this.searchIndex;
},
},
mounted() {
this.loadOrganizationOptions();
@@ -333,6 +341,7 @@ export default {
bizId: row.id,
subjectName: row.invoiceNo || '',
approvalStatus: previousApprovalStatus,
mkEnabled: this.mkEnabled,
});
this.$message.success('提交成功');
this.loadTable();
+88 -44
View File
@@ -36,25 +36,20 @@
<el-col v-if="isSettlementRelatedType" :span="6"
><el-form-item label="结算单号" prop="referenceId">
<div class="payment-form-page__settlement-nos">
<el-select
ref="settlementNoSelectRef"
:model-value="referenceSettlementNos"
multiple
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="1"
:disabled="readonly"
placeholder="请选择预结算/正式结算单"
class="payment-form-page__settlement-select"
@visible-change="handleSettlementSelectVisible"
<el-tooltip
:content="referenceLabel"
placement="top"
:disabled="!referenceLabel || referenceSettlementCount < 2"
>
<el-option
v-for="item in referenceSettlementLinks"
:key="`${item.id || item.settlementNo}`"
:label="item.settlementNo"
:value="item.settlementNo"
<el-input
:model-value="referenceDisplayText"
readonly
:disabled="readonly"
placeholder="请输入"
class="payment-form-page__settlement-input"
@click="!readonly && openReference()"
/>
</el-select>
</el-tooltip>
<el-button
v-if="!readonly"
class="payment-form-page__settlement-select-btn"
@@ -448,7 +443,7 @@
></el-table
>
<div v-else>
<div class="payment-form-page__reference-search">
<el-card class="payment-form-page__reference-search">
<el-form
:model="referenceQuery"
label-position="right"
@@ -486,7 +481,7 @@
<el-button @click="resetReferenceSearch">清空</el-button>
</div>
</el-form>
</div>
</el-card>
<el-tabs v-model="referenceTab" @tab-change="handleReferenceTabChange">
<el-tab-pane v-if="canSelectFormalSettlement" label="正式结算单" name="formal">
<el-table
@@ -678,7 +673,7 @@ import {
import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer';
import { formatContractOptionLabel } from '@/utils/contract';
import { downloadFileByUrl } from '@/utils/util';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import {
downloadKingdeeInvoiceFile,
isKingdeePathAttachment,
@@ -693,6 +688,9 @@ const attachmentViewerPlugins = [
fallbackPlugin(),
];
const paymentApplicationListRoute = '/payment/payment-application';
const paymentApplicationListRefreshKey = 'payment-application-list-refresh';
const ATTACHMENT_MATERIALS = {
weighingSlip: { key: 'weighingSlip', label: '磅单', keywords: ['磅单'] },
entrustOrder: { key: 'entrustOrder', label: '委托单', keywords: ['委托单'] },
@@ -952,6 +950,14 @@ export default {
referenceSettlementNos() {
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() {
if (this.referenceRows.length) {
return this.referenceRows.map(item => ({
@@ -1075,7 +1081,7 @@ export default {
!this.form.preSettlementId &&
!this.form.preSettlementIds?.length
) {
callback(new Error('请选择结算单'));
callback(new Error('请输入'));
return;
}
callback();
@@ -1227,7 +1233,7 @@ export default {
];
}
const referenceRow = this.createReferenceRow(data);
this.referenceRows = referenceRow.settlementNo ? [referenceRow] : [];
this.referenceRows = this.restoreReferenceRows(data, referenceRow);
this.paymentRecords = data.paymentRecords || [];
},
async initialize() {
@@ -1250,7 +1256,7 @@ export default {
invoices: data.invoices || [],
};
const referenceRow = this.createReferenceRow(data);
this.referenceRows = referenceRow.settlementNo ? [referenceRow] : [];
this.referenceRows = this.restoreReferenceRows(data, referenceRow);
this.paymentRecords = data.paymentRecords || [];
if (this.form.paymentType === 'project_advance') {
await this.loadProjectOptions();
@@ -1468,6 +1474,33 @@ export default {
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) {
const settlementAmount = Number(row.settlementAmount || 0);
const appliedAmount = Number(row.advanceAppliedAmount || row.appliedPaymentAmount || 0);
@@ -1476,11 +1509,13 @@ export default {
id: row.preSettlementId || row.settlementId || row.id,
settlementNo: row.preSettlementNo || row.formalSettlementNo || row.settlementNo || '',
referenceType: isPre ? 'pre' : 'formal',
settlementAmount,
payableAmount:
row.payableAmount === undefined || row.payableAmount === null
settlementAmount: Number(Number(settlementAmount || 0).toFixed(2)),
payableAmount: Number(
(row.payableAmount === undefined || row.payableAmount === null
? Math.max(0, settlementAmount - appliedAmount)
: Number(row.payableAmount || 0),
: Number(row.payableAmount || 0)
).toFixed(2)
),
};
},
createInvoiceRows(invoices = [], settlementNo = '') {
@@ -1538,9 +1573,24 @@ export default {
}
const first = rows[0];
this.form.invoices = [];
this.referenceRows = rows.map(item =>
this.createReferenceRow({ ...item, referenceType: 'pre' })
const paymentType =
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(
(total, item) => total + Number(item.settlementAmount || 0),
0
@@ -1551,8 +1601,6 @@ export default {
);
const paymentRatio = Number(this.form.paymentRatio || 0);
// 尾款付款场景下选择预结算单时保留尾款付款类型,不强制改为进度预付
const paymentType =
this.form.paymentType === 'settlement_payment' ? 'settlement_payment' : 'progress_advance';
this.amountSyncing = true;
Object.assign(this.form, {
paymentType,
@@ -2287,13 +2335,6 @@ export default {
}
return Promise.all(loaders);
},
handleSettlementSelectVisible(visible) {
// 结算单号仅做展示,通过「选择」按钮维护,避免下拉改值
if (!visible) return;
this.$nextTick(() => {
this.$refs.settlementNoSelectRef?.blur?.();
});
},
handleReferenceSearch() {
this.formalPage.current = 1;
this.prePage.current = 1;
@@ -2713,14 +2754,17 @@ export default {
bizId: this.form.id,
subjectName: this.form.paymentNo || '',
approvalStatus: previousApprovalStatus,
mkEnabled: await loadMkEnabled().catch(() => false),
});
this.$message.success('提交成功');
// 通知列表页(keep-alive)重新拉取数据
sessionStorage.setItem(paymentApplicationListRefreshKey, '1');
this.goBack();
},
goBack() {
removeSettlementTransfer(this.$route.query.transferToken);
this.$router.$avueRouter?.closeTag?.();
this.$router.push('/payment/payment-application');
this.$router.push(paymentApplicationListRoute);
},
formatMoney(value) {
const amount = Number(value);
@@ -2741,15 +2785,15 @@ export default {
width: 100%;
min-height: 32px;
}
.payment-form-page__settlement-select {
.payment-form-page__settlement-input {
flex: 1;
min-width: 0;
}
.payment-form-page__settlement-select :deep(.el-select__wrapper) {
width: 100%;
.payment-form-page__settlement-input :deep(.el-input__inner) {
cursor: pointer;
}
.payment-form-page__settlement-select :deep(.el-tag__close) {
display: none;
.payment-form-page__settlement-input.is-disabled :deep(.el-input__inner) {
cursor: not-allowed;
}
.payment-form-page__settlement-select-btn {
flex-shrink: 0;
+35
View File
@@ -152,6 +152,9 @@ import organizationSearch from '@/mixins/organization-search';
import { getMKApprovalUrl } from '@/api/system/business-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
const paymentApplicationListRoute = '/payment/payment-application';
const paymentApplicationListRefreshKey = 'payment-application-list-refresh';
export default {
name: 'PaymentApplication',
mixins: [organizationSearch],
@@ -193,7 +196,28 @@ export default {
this.mkEnabled = false;
});
},
activated() {
this.consumeListRefreshFlag();
},
watch: {
$route(to, from) {
if (
to.path === paymentApplicationListRoute &&
from?.path?.includes('/payment-application/form')
) {
this.consumeListRefreshFlag();
}
},
},
methods: {
/** 表单提交成功后带标记返回时,重新拉取列表 */
consumeListRefreshFlag() {
if (!sessionStorage.getItem(paymentApplicationListRefreshKey)) {
return;
}
sessionStorage.removeItem(paymentApplicationListRefreshKey);
this.onLoad(this.page);
},
hasPermission(code) {
return this.permission?.[code] !== false;
},
@@ -296,6 +320,7 @@ export default {
bizId: row.id,
subjectName: row.paymentNo || '',
approvalStatus: previousApprovalStatus,
mkEnabled: this.mkEnabled,
});
this.$message.success('提交成功');
this.refreshChange();
@@ -441,6 +466,16 @@ export default {
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) {
+13 -5
View File
@@ -80,11 +80,13 @@
<div class="claim-record-page__search-actions">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
<el-tooltip :content="searchExpanded ? '折叠' : '展开'" placement="top">
<el-button text @click="searchExpanded = !searchExpanded">
<el-icon><component :is="searchExpanded ? ArrowUp : ArrowDown" /></el-icon>
</el-button>
</el-tooltip>
<el-button
v-if="showSearchToggle"
text
:icon="searchExpanded ? ArrowUp : ArrowDown"
@click="searchExpanded = !searchExpanded"
>{{ searchExpanded ? '收起' : '展开' }}</el-button
>
</div>
</el-form>
</section-card>
@@ -191,6 +193,9 @@ export default {
ArrowUp,
query: emptyQuery(),
searchExpanded: false,
// 与 avue-crud 一致:搜索项数量超过 searchIndex(默认一行 4 项)时才显示“展开/收起”
searchIndex: 4,
searchItemCount: 11,
searchVisible: true,
tableSize: 'default',
claimStatusOptions: api.claimStatusOptions,
@@ -209,6 +214,9 @@ export default {
visibleColumns() {
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
},
showSearchToggle() {
return this.searchItemCount > this.searchIndex;
},
isAdmin() {
return isAdministrator(this.userInfo);
},
+15 -1
View File
@@ -46,6 +46,9 @@
<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>
<el-row :gutter="32" class="receipt-claim-batch-form-page__claim-info">
@@ -298,10 +301,21 @@ export default {
0
);
},
flowRemaining(flow) {
return Math.max(Number(flow.unclaimedAmount || 0) - Number(flow.claimedAmount || 0), 0);
},
maxAllocation(row) {
const current = Number(row.settlement.allocations?.[row.flow.id] || 0);
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) {
const total = this.settlementAllocatedTotal(settlement);
+16 -5
View File
@@ -9,6 +9,9 @@
<el-form-item label="对方户名">
<el-input v-model="query.counterpartyName" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="对方开户行">
<el-input v-model="query.counterpartyBank" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="对方账号">
<el-input v-model="query.counterpartyAccount" clearable placeholder="请输入" />
</el-form-item>
@@ -86,11 +89,13 @@
<div class="receipt-flow-page__search-actions">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
<el-tooltip :content="searchExpanded ? '折叠' : '展开'" placement="top">
<el-button text @click="searchExpanded = !searchExpanded">
<el-icon><component :is="searchExpanded ? ArrowUp : ArrowDown" /></el-icon>
</el-button>
</el-tooltip>
<el-button
v-if="showSearchToggle"
text
:icon="searchExpanded ? ArrowUp : ArrowDown"
@click="searchExpanded = !searchExpanded"
>{{ searchExpanded ? '收起' : '展开' }}</el-button
>
</div>
</el-form>
</section>
@@ -321,6 +326,9 @@ export default {
syncing: false,
pushingId: null,
searchExpanded: false,
// 与 avue-crud 一致:搜索项数量超过 searchIndex(默认一行 4 项)时才显示“展开/收起”
searchIndex: 4,
searchItemCount: 7,
page: { current: 1, size: 10, total: 0 },
detailDialog: { visible: false, loading: false, data: {}, claimRecords: [] },
};
@@ -330,6 +338,9 @@ export default {
visibleColumns() {
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
},
showSearchToggle() {
return this.searchItemCount > this.searchIndex;
},
isAdmin() {
return isAdministrator(this.userInfo);
},
@@ -14,7 +14,7 @@
label-width="auto"
>
<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-date-picker
v-if="field.type === 'date' && editable"
@@ -64,6 +64,12 @@
:precision="2"
: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.type === 'contract'">{{
displayValue(form.contractName)
@@ -246,24 +252,36 @@
:index="detailIndexMethod"
/>
<el-table-column
v-for="column in detailTableColumns"
:key="column.prop"
v-bind="column"
v-for="item in detailDisplayColumns"
:key="item.key"
: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"
show-overflow-tooltip
>
<template #default="{ row }">
<span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
<span v-else-if="column.prop === 'transportQuantity'">
{{ formatDetailTransportQuantity(row[column.prop]) }}
<span v-if="item.kind === 'fee'">
{{ formatMoney(parseFeeItems(row.feeItemsJson || row.feeItems)[item.name]) }}
</span>
<span v-else-if="column.prop === 'mileage'">
{{ formatDetailMileage(row[column.prop]) }}
<el-link
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 v-else-if="column.prop === 'transportType'">
{{ transportTypeName(row[column.prop]) }}
<span v-else-if="item.column.prop === 'mileage'">
{{ formatDetailMileage(row[item.column.prop]) }}
</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>
</el-table-column>
<el-table-column v-if="editable" label="操作" width="150" fixed="right" align="center">
@@ -378,6 +396,16 @@
</div>
</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="发票信息">
<div class="formal-editor__invoice-claim">
<el-form inline label-position="right" label-width="auto" @submit.prevent>
@@ -675,24 +703,27 @@
</div>
<el-dialog v-model="candidate.visible" title="选择预结算单" width="88%" append-to-body>
<el-form
:model="candidate.query"
inline
label-position="right"
label-width="88px"
class="formal-editor__candidate-search"
>
<el-form-item label="预结算单号"
<el-card>
<el-form
:model="candidate.query"
inline
label-position="right"
label-width="88px"
class="formal-editor__candidate-search"
>
<el-form-item label="预结算单号"
><el-input v-model="candidate.query.preSettlementNo" clearable
/></el-form-item>
<el-form-item label="合同编号"
/></el-form-item>
<el-form-item label="合同编号"
><el-input v-model="candidate.query.contractNo" clearable
/></el-form-item>
<el-form-item
/></el-form-item>
<el-form-item
><el-button @click="resetCandidates">重置</el-button
><el-button type="primary" @click="loadCandidates">查询</el-button></el-form-item
>
</el-form>
>
</el-form>
</el-card>
<el-table
ref="candidateTable"
v-loading="candidate.loading"
@@ -984,6 +1015,11 @@ import {
getDetailFees as getPreSettlementDetailFees,
} from '@/api/settlement/preSettlement';
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 {
createFormalSettlementForm,
@@ -1001,6 +1037,7 @@ import { h } from 'vue';
import { InfoFilled } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import { formatContractOptionLabel } from '@/utils/contract';
import { normalizeCurrencyCode } from '@/utils/currency';
import { downloadFileByUrl } from '@/utils/util';
import {
downloadKingdeeInvoiceFile,
@@ -1008,12 +1045,16 @@ import {
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 { isAdministrator } from '@/utils/auth';
// 结算币种为本位币(人民币)时汇率恒为1,汇率日期、结算汇率与按汇率换算的本位币合计都不展示
const LOCAL_CURRENCY_HIDDEN_FIELDS = ['exchangeRateDate', 'exchangeRate', 'localSettlementAmount'];
export default {
name: 'FormalSettlementEditor',
components: { InfoFilled, KingdeeInvoicePoolDialog },
components: { InfoFilled, KingdeeInvoicePoolDialog, ProjectAdvanceWriteOff },
props: {
modelValue: Boolean,
recordId: [String, Number],
@@ -1042,6 +1083,7 @@ export default {
receiptClaims: [],
adjustments: [],
changeRecords: [],
projectAdvanceWriteOffs: [],
attachments: [],
selectedAttachmentRows: [],
invoices: [],
@@ -1180,6 +1222,34 @@ export default {
title() {
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() {
return this.summaryFees.reduce((total, row) => total + Number(row.settlementAmount || 0), 0);
},
@@ -1272,6 +1342,13 @@ export default {
}
this.adjustments = data.adjustments || [];
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.selectedAttachmentRows = [];
this.invoices = data.invoices || [];
@@ -1302,6 +1379,7 @@ export default {
this.receiptClaims = [];
this.adjustments = [];
this.changeRecords = [];
this.projectAdvanceWriteOffs = [];
this.attachments = [];
this.selectedAttachmentRows = [];
this.invoices = [];
@@ -2028,6 +2106,21 @@ export default {
this.appliedDetailQuery = { ...this.detailQuery };
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) {
this.detailQuery = { documentNo: '', waybillNo: '', batchNo: '', cargoName: '' };
if (apply) this.handleDetailQuery();
@@ -2546,6 +2639,7 @@ export default {
matchedAmount: Number(row.matchedAmount || 0),
attachmentJson: row.attachmentJson || '',
})),
projectAdvanceWriteOffs: this.$refs.projectAdvanceWriteOffRef?.toSavePayload?.() || [],
remark: this.form.remark,
};
},
@@ -2554,6 +2648,9 @@ export default {
},
async handleDraftSave() {
// 保存草稿:不做表单校验,直接落草稿并返回列表刷新
if (this.$refs.projectAdvanceWriteOffRef?.validateBeforeSave?.() === false) {
return;
}
this.draftSaving = true;
try {
await save({
@@ -2581,6 +2678,9 @@ export default {
return this.$message.warning('请完善手工费用的费用类型和费用项');
}
if (!this.validateInvoices()) return;
if (this.$refs.projectAdvanceWriteOffRef?.validateBeforeSave?.() === false) {
return;
}
// 附件类型仅做提示,不阻断正式结算单新增/保存提交。
// this.validateAttachments();
this.saving = true;
@@ -2861,6 +2961,9 @@ export default {
</script>
<style scoped lang="scss">
.form-readonly--code {
color: var(--el-color-primary);
}
.formal-editor--page {
padding-bottom: 72px;
}
@@ -295,6 +295,16 @@ export default {
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) {
@@ -16,7 +16,7 @@
class="pre-settlement-editor__form"
>
<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-input
v-if="field.type === 'contract'"
@@ -40,7 +40,7 @@
type="date"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
:disabled="!editable || form.currency === 'RMB'"
:disabled="!editable"
placeholder="请选择"
@change="handleExchangeRateDateChange"
/>
@@ -50,7 +50,7 @@
:min="0"
:precision="2"
:controls="false"
:disabled="!editable || form.currency === 'RMB'"
:disabled="!editable"
@change="recalculateLocalAmount"
/>
<el-input
@@ -66,7 +66,11 @@
<span v-else-if="field.prop === 'settlementType'" class="form-readonly">
{{ settlementTypeName }}
</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 ? '系统自动生成' : '-') }}
</span>
<span v-else-if="field.prop === 'createTime'" class="form-readonly">
@@ -243,60 +247,52 @@
:index="detailIndexMethod"
/>
<el-table-column
v-for="column in visibleDetailColumns"
:key="column.prop"
:label="column.label"
:prop="column.prop"
:min-width="column.minWidth"
v-for="item in detailDisplayColumns"
:key="item.key"
: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"
show-overflow-tooltip
>
<template #default="{ row }">
<span v-if="item.kind === 'fee'">
{{
formatMoney(
parseFeeItems(row.feeItemsJson)[item.name],
row.currency || form.currency
)
}}
</span>
<el-link
v-if="column.link && row[column.prop]"
v-else-if="item.column.link && row[item.column.prop] && !publicMode"
type="primary"
@click="openAdjustDialog(row, true)"
@click.stop="handleDetailLink(row, item.column)"
>
{{ row[column.prop] }}
{{ row[item.column.prop] }}
</el-link>
<span v-else-if="column.money && column.prop === 'unitPrice'">
<span v-else-if="item.column.money && item.column.prop === 'unitPrice'">
{{ formatUnitPrice(row) }}
</span>
<span v-else-if="column.money">
{{ formatMoney(row[column.prop], row.currency || form.currency) }}
<span v-else-if="item.column.money">
{{ formatMoney(row[item.column.prop], row.currency || form.currency) }}
</span>
<span v-else-if="column.prop === 'transportQuantityText'">
<span v-else-if="item.column.prop === 'transportQuantityText'">
{{ formatQuantity(row) }}
</span>
<span v-else-if="pageMode && column.prop === 'mileage'">
{{ formatDetailMileage(row[column.prop]) }}
<span v-else-if="item.column.prop === 'mileage'">
{{ formatDetailMileage(row[item.column.prop]) }}
</span>
<span v-else-if="column.prop === 'transportType'">
{{ transportTypeName(row[column.prop]) }}
<span v-else-if="item.column.prop === 'transportType'">
{{ transportTypeName(row[item.column.prop]) }}
</span>
<span v-else-if="column.prop === 'cargoName'">
<span v-else-if="item.column.prop === 'cargoName'">
{{ formatCargoNames(row) }}
</span>
<span v-else-if="column.prop === 'cargoType'">
<span v-else-if="item.column.prop === 'cargoType'">
{{ formatCargoTypes(row) }}
</span>
<span v-else>{{ displayValue(row[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
)
}}
<span v-else>{{ displayValue(row[item.column.prop]) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" :width="editable ? 150 : 90" fixed="right" align="center">
@@ -421,6 +417,16 @@
</div>
</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="预付信息">
<el-table :data="advances" border>
<el-table-column type="index" label="序号" width="64" align="center" />
@@ -776,12 +782,13 @@
</el-table-column>
<el-table-column label="里程(KM)" min-width="140" align="center">
<template #default="{ row }">
<span v-if="adjustDialog.readonly">{{ formatDetailMileage(row.mileage) }}</span>
<el-input-number
v-else
v-model="row.mileage"
:min="0"
:precision="2"
:controls="false"
:disabled="adjustDialog.readonly"
@change="recalculateAdjustRow(row, 'mileage')"
/>
</template>
@@ -939,19 +946,27 @@ import { h } from 'vue';
import { mapGetters } from 'vuex';
import { InfoFilled } from '@element-plus/icons-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 {
adjustDetail,
getCandidateDetails,
getContractList,
getContractOptions,
getDetail,
getDetailFees,
getFeeOptions,
getNextNo,
removeDetail,
save,
submit,
} from '@/api/settlement/preSettlement';
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 {
@@ -974,14 +989,18 @@ import {
settlementSummaryColumns,
} from '@/option/settlement/preSettlementTable';
import { formatContractOptionLabel } from '@/utils/contract';
import { normalizeCurrencyCode } from '@/utils/currency';
import { downloadFileByUrl } from '@/utils/util';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import * as XLSX from 'xlsx';
import { isAdministrator } from '@/utils/auth';
// 结算币种为本位币(人民币)时汇率恒为1,汇率日期、结算汇率与按汇率换算的本位币合计都不展示
const LOCAL_CURRENCY_HIDDEN_FIELDS = ['exchangeRateDate', 'exchangeRate', 'localSettlementAmount'];
export default {
name: 'PreSettlementEditor',
components: { InfoFilled, ChangeRecordDetailDialog },
components: { InfoFilled, ChangeRecordDetailDialog, ProjectAdvanceWriteOff },
props: {
modelValue: {
type: Boolean,
@@ -1084,6 +1103,7 @@ export default {
size: 10,
},
advances: [],
projectAdvanceWriteOffs: [],
changeRecords: [],
attachments: [],
attachmentTypeOptions: [],
@@ -1188,6 +1208,14 @@ export default {
settlementTypeName() {
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() {
return '承运商合同';
},
@@ -1209,6 +1237,17 @@ export default {
});
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 分组的明细数据(用于拼接货物名称和类型)
groupedDetails() {
const groups = {};
@@ -1283,6 +1322,11 @@ export default {
},
async initialize() {
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) {
await this.loadFeeOptions();
await this.loadFeeCategoryOptions();
@@ -1293,15 +1337,36 @@ export default {
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() {
const rows = Array.isArray(this.initialData?.rows) ? this.initialData.rows : [];
if (!rows.length) return;
const first = rows[0];
const settlementType = this.initialData.settlementType || first.settlementType || 'payable';
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))) {
this.contractOptions.push({
id: contractId,
@@ -1312,8 +1377,8 @@ export default {
deptId: first.deptId,
deptName: first.deptName,
settlementType,
partyA: first.payerName,
partyB: first.payeeName,
partyA: contractPartyA,
partyB: contractPartyB,
});
}
this.handleContractChange(contractId);
@@ -1325,8 +1390,8 @@ export default {
projectName: first.projectName || this.form.projectName,
deptId: first.deptId || this.form.deptId,
deptName: first.deptName || this.form.deptName,
payerName: first.payerName || this.form.payerName,
payeeName: first.payeeName || this.form.payeeName,
payerName: this.form.payerName || contractPartyA,
payeeName: this.form.payeeName || contractPartyB,
settlementType,
currency: first.currency || 'RMB',
localCurrency: first.localCurrency || 'RMB',
@@ -1396,6 +1461,7 @@ export default {
this.appliedDetailQuery = { ...this.detailQuery };
this.detailPage = { current: 1, size: 10 };
this.advances = [];
this.projectAdvanceWriteOffs = [];
this.changeRecords = [];
this.attachments = [];
this.selectedAttachmentRows = [];
@@ -1435,6 +1501,13 @@ export default {
createUserName: row.createUserName || detail.createUserName,
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.attachments = this.parseAttachments(detail.attachmentsJson);
this.sortAttachments();
@@ -1579,6 +1652,7 @@ export default {
this.form.payerName = '';
this.form.payeeName = '';
this.summaryFees = [];
this.projectAdvanceWriteOffs = [];
return;
}
this.form.contractId = contract.id;
@@ -1589,10 +1663,12 @@ export default {
this.form.deptId = contract.deptId;
this.form.deptName = contract.deptName;
this.form.settlementType = contract.settlementType || 'payable';
this.form.payerName = contract.partyA;
this.form.payeeName = contract.partyB;
// 部分候选合同只带 id/名称,缺甲方乙方;此时保留已带出的收付方,不要清成 undefined
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.projectAdvanceWriteOffs = [];
if (this.form.currency !== 'RMB') {
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
this.fetchBfiExchangeRate();
@@ -1613,6 +1689,9 @@ export default {
this.$message.warning(`请完善结算合计第${invalidManualFeeIndex + 1}行的费用类型和费用项`);
return;
}
if (this.$refs.projectAdvanceWriteOffRef?.validateBeforeSave?.() === false) {
return;
}
const stateKey = shouldSubmit ? 'submitting' : 'saving';
this[stateKey] = true;
try {
@@ -1623,13 +1702,14 @@ export default {
if (shouldSubmit) {
const previousApprovalStatus = this.form.approvalStatus || '';
await submit({ id: this.form.id });
await submitMkApprovalFlow({
const mkStarted = await submitMkApprovalFlow({
bizType: 'pre-settlement',
bizId: this.form.id,
subjectName: this.form.preSettlementNo || this.form.contractName || '',
approvalStatus: previousApprovalStatus,
mkEnabled: await loadMkEnabled().catch(() => false),
});
this.$message.success('审批流程已发起');
this.$message.success(mkStarted ? '审批流程已发起' : '提交成功');
this.visible = false;
this.$emit('success', this.form.id);
return;
@@ -1659,6 +1739,7 @@ export default {
remark: row.remark,
manualFlag: row.manualFlag || 0,
})),
projectAdvanceWriteOffs: this.$refs.projectAdvanceWriteOffRef?.toSavePayload?.() || [],
};
},
addSummaryFee() {
@@ -1715,7 +1796,7 @@ export default {
return option?.dictValue || 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;
},
removeSummaryFee(index) {
@@ -1987,6 +2068,21 @@ export default {
this.appliedDetailQuery = { ...this.detailQuery };
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() {
this.detailQuery = {
documentNo: '',
@@ -2992,6 +3088,10 @@ export default {
line-height: 32px;
color: #303133;
}
.form-readonly--code {
color: var(--el-color-primary);
}
}
.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
@@ -368,6 +368,7 @@ export default {
bizId: row.id,
subjectName: row.formalSettlementNo || '',
approvalStatus: previousApprovalStatus,
mkEnabled: this.mkEnabled,
});
this.$message.success('提交成功');
this.loadTable();
+1
View File
@@ -661,6 +661,7 @@ export default {
bizId: row.id,
subjectName: row.preSettlementNo || '',
approvalStatus: previousApprovalStatus,
mkEnabled: this.mkEnabled,
});
this.$message.success('提交成功');
this.loadTable();
@@ -520,7 +520,7 @@
:index="transferIndexMethod"
/>
<el-table-column
v-for="column in tableColumns.slice(0, 13)"
v-for="column in transferTableColumns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
@@ -1174,6 +1174,13 @@ export default {
columns.splice(totalIndex, 0, ...dynamicColumns);
return columns;
},
// 批量转结算弹窗只展示前 13 列。预结算单号、正式结算单号正是本操作要生成的单号,
// 候选行上必为空,故不占列
transferTableColumns() {
return this.tableColumns
.filter(column => !['preSettlementNo', 'formalSettlementNo'].includes(column.prop))
.slice(0, 13);
},
columnSettingOptions() {
return this.displayTableColumns.map(column => ({
prop: column.prop,
@@ -1712,11 +1719,12 @@ export default {
this.$router.push({ path, query: { detailNo } });
},
async openRouteDetail(detailId = this.$route.query.detailId) {
if (
!detailId ||
this.settlementType !== 'payable' ||
this.$route.path !== '/settlement/payable-detail'
) {
// 结算明细点「单据号」会带 detailId 跳到本页;只有当前停在应收/应付明细页时才自动展开详情
const routePath =
this.settlementType === 'payable'
? '/settlement/payable-detail'
: '/settlement/receivable-detail';
if (!detailId || this.$route.path !== routePath) {
return;
}
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 (!row) {
this.$message.info('当前记录未找到对应的应付明细');
this.$message.info(
`当前记录未找到对应的${this.settlementType === 'payable' ? '应付' : '应收'}明细`
);
return;
}
await this.openDetailDialog(row);
@@ -2681,12 +2691,12 @@ export default {
return `${Number(value).toFixed(2)} ${currency}`;
},
formatDetailCell(row, prop) {
if (prop === 'mileage' && Number(row?.[prop]) === -1) {
return '-';
}
if (
prop === 'mileage' &&
(row?.[prop] === null ||
row?.[prop] === undefined ||
row?.[prop] === '' ||
Number(row[prop]) === -1)
(row?.[prop] === null || row?.[prop] === undefined || row?.[prop] === '')
) {
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>
</div>
</div>
<div v-if="oaSyncProgress.dumpFilePath" class="oa-sync-meta">
原始数据 JSON:{{ oaSyncProgress.dumpFilePath }}
</div>
</div>
<template #footer>
<span class="dialog-footer">
@@ -253,6 +256,7 @@ export default {
total: 0,
synced: 0,
skipped: 0,
dumpFilePath: '',
},
parentId: 0,
page: {
@@ -658,7 +662,7 @@ export default {
methods: {
handleOaOrgFullSync() {
this.$confirm(
'全量同步将拉取 OA 全部公司/部门;已存在且 OA 更新时间一致的记录会跳过。是否清除后同步?选择“是”将保留顶级「外部组织」与集团锚点(id=135/GWWL),删除其余组织后再同步;选择“否”则直接全量同步。',
'全量同步将拉取 OA 公司/部门(仅保留二级公司 oaId=2、135 及其子公司与部门);已存在且 OA 更新时间一致的记录会跳过。是否清除后同步?选择“是”将保留顶级「外部组织」与集团锚点(id=1/GWWL),删除其余组织后再同步;选择“否”则直接全量同步。',
'全量同步',
{
confirmButtonText: '是',
@@ -709,6 +713,7 @@ export default {
total: 0,
synced: 0,
skipped: 0,
dumpFilePath: '',
};
this.runOaOrgSyncPages();
},
@@ -742,6 +747,9 @@ export default {
this.oaSyncProgress.total = page.total || 0;
this.oaSyncProgress.synced += page.syncedCount || 0;
this.oaSyncProgress.skipped += page.skippedCount || 0;
if (page.dumpFilePath) {
this.oaSyncProgress.dumpFilePath = page.dumpFilePath;
}
if (page.finished) {
break;
}
+8
View File
@@ -364,6 +364,9 @@
<span class="oa-sync-stat__value oa-sync-stat__value--skip">{{ oaSyncProgress.skipped }}</span>
</div>
</div>
<div v-if="oaSyncProgress.dumpFilePath" class="oa-sync-meta">
原始数据 JSON:{{ oaSyncProgress.dumpFilePath }}
</div>
</div>
<template #footer>
<span class="dialog-footer">
@@ -450,6 +453,7 @@ export default {
total: 0,
synced: 0,
skipped: 0,
dumpFilePath: '',
},
page: {
pageSize: 10,
@@ -712,6 +716,7 @@ export default {
total: 0,
synced: 0,
skipped: 0,
dumpFilePath: '',
};
this.runOaSyncPages();
},
@@ -746,6 +751,9 @@ export default {
this.oaSyncProgress.total = page.total || 0;
this.oaSyncProgress.synced += page.syncedCount || 0;
this.oaSyncProgress.skipped += page.skippedCount || 0;
if (page.dumpFilePath) {
this.oaSyncProgress.dumpFilePath = page.dumpFilePath;
}
if (page.finished) {
this.oaSyncStatus = 'done';
break;
+2 -2
View File
@@ -158,7 +158,7 @@ import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
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 { mapGetters } from 'vuex';
import { excelOption, exportColumns, option } from '@/option/vehicle/accident-record';
@@ -550,7 +550,7 @@ export default {
};
},
buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
},
handleTemplate() {
exportBlob(
@@ -468,7 +468,11 @@ import {
publish,
changeStatus,
} 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 { isAdministrator } from '@/utils/auth';
@@ -1324,7 +1328,9 @@ export default {
});
},
buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
return omitInvalidListStatus(
normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap)
);
},
},
};
+87 -34
View File
@@ -208,7 +208,7 @@
v-if="
hasPermission('customer_archive_sync') &&
row.approvalStatus === 'approved' &&
row.kingdeeStatus === 'failed'
!['synced', 'exist'].includes(row.kingdeeStatus)
"
@click="handleSyncKingdee(row)"
>
@@ -486,7 +486,7 @@
:options="deptTree"
:props="deptCascaderProps"
disabled
placeholder="桂物物流集团"
placeholder="广西现代物流集团有限公司"
/>
</el-form-item>
</el-col>
@@ -1588,7 +1588,7 @@ const customerArchiveListRoute = '/vehicle/customer-archive';
const customerArchiveFormRoute = '/vehicle/customer-archive/form';
const customerArchiveListRefreshKey = 'customer-archive-list-refresh';
/** 客商所属组织固定为顶层组织 */
const FIXED_OWNER_DEPT_NAME = '桂物物流集团';
const FIXED_OWNER_DEPT_NAME = '广西现代物流集团有限公司';
const createTimeRangeMap = {
createTimeRange: ['createTimeStart', 'createTimeEnd'],
@@ -1791,6 +1791,8 @@ export default {
},
/** 业务字典 mk_template「开启MK」=1 */
mkEnabled: false,
/** 是否已完成 MK 开关加载,避免提交时误判 */
_mkEnabledResolved: false,
formRules: {
customerCode: [{ max: 20, message: '客商编号最多20个字符', trigger: 'blur' }],
fullName: [{ required: true, message: '请输入客商名称', trigger: 'blur' }],
@@ -1871,7 +1873,7 @@ export default {
editBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 320,
menuWidth: 400,
column: [
{
label: '客商编号',
@@ -2041,7 +2043,6 @@ export default {
search: true,
searchOrder: 6,
searchValue: '',
dataType: 'number',
minWidth: 100,
dicData: [
{ label: '全部', value: '' },
@@ -2340,7 +2341,13 @@ export default {
sessionStorage.setItem(customerArchiveListRefreshKey, '1');
},
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) {
const list = Array.isArray(values) ? values : values ? [values] : [];
@@ -3100,12 +3107,15 @@ export default {
});
},
initMkEnabled() {
this._mkEnabledResolved = false;
loadMkEnabled()
.then(enabled => {
this.mkEnabled = !!enabled;
this._mkEnabledResolved = true;
})
.catch(() => {
this.mkEnabled = false;
this._mkEnabledResolved = true;
});
},
initScoreQuantificationOptions() {
@@ -3205,7 +3215,7 @@ export default {
);
},
/**
* 客商所属组织固定为顶层「桂物物流集团」,表单不可编辑。
* 客商所属组织固定为顶层「广西现代物流集团有限公司」,表单不可编辑。
*/
applyFixedOwnerDept() {
if (!this.isArchivePage || this.readonly) {
@@ -4481,15 +4491,34 @@ export default {
});
},
/**
* 列表 / 新增 / 编辑提交共用:开启 MK 时提交审核流,并更新客商审批状态
* 列表 / 新增 / 编辑提交共用:
* 未开启 MK 时仅走本地提交审批(与新增一致,不请求 MK 流程);
* 开启 MK 时先提交审核流,再更新客商审批状态。
*/
submitCustomerApproval(id, fullName = '', approvalStatus = '') {
return submitMkApprovalFlow({
bizType: 'customer-archive',
bizId: id,
subjectName: fullName,
approvalStatus,
}).then(() => submitApproval(id));
const submitLocal = () => submitApproval(id);
const run = enabled => {
this.mkEnabled = !!enabled;
this._mkEnabledResolved = true;
if (!this.mkEnabled) {
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() {
const scores = this.archiveForm.scores || [];
@@ -5194,17 +5223,20 @@ export default {
this.$message.error('获取审核跳转地址失败');
});
},
handleApprove(row) {
this.$confirm('是否确认审核通过?', '提示', {
async handleApprove(row) {
await this.$confirm('是否确认审核通过?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => approve(row.id))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '审核通过!' });
});
});
const res = await approve(row.id);
// data=false 表示审核已通过但推送金蝶失败,msg 带具体原因
if (res.data.data === false) {
this.$message.warning(res.data.msg);
} else {
this.$message.success(res.data.msg || '审核通过!');
}
this.onLoad(this.page);
},
async handleSyncKingdee(row) {
if (this.kingdeeSyncingId) return;
@@ -5264,19 +5296,9 @@ export default {
this.onLoad(this.page);
},
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.page.currentPage = 1;
this.onLoad(this.page);
this.onLoad(this.page, this.query);
done();
},
selectionChange(list) {
@@ -5337,7 +5359,38 @@ export default {
};
},
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 { getToken, isAdministrator } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
import { normalizeSearchRangeParams } from '@/utils/search-range';
import { applyIncomingSearch, normalizeSearchRangeParams } from '@/utils/search-range';
import { mapGetters } from 'vuex';
import { ElLoading } from 'element-plus';
import { excelOption, exportColumns, option } from '@/option/vehicle/insurance-record';
@@ -409,7 +409,7 @@ export default {
: integerPart;
},
buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
},
validateRow(row) {
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 { formatUpdateUserName } from '@/utils/audit';
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 VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { mapGetters } from 'vuex';
@@ -903,7 +903,7 @@ export default {
};
},
buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
},
handleTemplate() {
exportBlob(
+4 -6
View File
@@ -168,7 +168,7 @@ import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
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 VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { mapGetters } from 'vuex';
@@ -371,9 +371,8 @@ export default {
placeholder: '日期时间选择器',
},
{
label: '里程/航程数(公里)',
label: '里程/航程数',
prop: 'mileage',
renderHeader: () => '里程/航程数',
type: 'input',
minWidth: 130,
slot: true,
@@ -391,6 +390,7 @@ export default {
],
value: '公里',
hide: true,
display: false,
},
{
label: '备注',
@@ -565,12 +565,10 @@ export default {
const mileageUnit = vehicleType === '船舶' ? '海里' : '公里';
const vehicleTypeColumn = this.findColumn(this.option.column, 'vehicleType');
const vehicleNoColumn = this.findColumn(this.option.column, 'vehicleNo');
const mileageColumn = this.findColumn(this.option.column, 'mileage');
const mileageUnitColumn = this.findColumn(this.option.column, 'mileageUnit');
vehicleNoColumn.placeholder =
vehicleType === '船舶' ? '请选择船号' : '请选择车牌号';
vehicleTypeColumn.disabled = Boolean(this.form.id);
mileageColumn.label = `里程/航程数(${mileageUnit})`;
mileageUnitColumn.value = mileageUnit;
this.form.mileageUnit = mileageUnit;
if (!this.isDetailLoading && oldValue && vehicleType !== oldValue) {
@@ -833,7 +831,7 @@ export default {
};
},
buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
},
handleTemplate() {
exportBlob(
+2 -2
View File
@@ -210,7 +210,7 @@ import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
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 { mapGetters } from 'vuex';
import { excelOption, exportColumns, option } from '@/option/vehicle/mileage-record';
@@ -607,7 +607,7 @@ export default {
},
buildQuery(params = {}) {
return normalizeSearchRangeParams(
{ ...params, ...this.query, vehicleType: '车辆' },
{ ...applyIncomingSearch(this.query, params), vehicleType: '车辆' },
createTimeRangeMap
);
},
@@ -155,7 +155,7 @@ import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
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 { mapGetters } from 'vuex';
import { excelOption, exportColumns, option } from '@/option/vehicle/tire-replacement-record';
@@ -500,7 +500,7 @@ export default {
};
},
buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
},
handleTemplate() {
exportBlob(
@@ -130,7 +130,7 @@ import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
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 { mapGetters } from 'vuex';
import { excelOption, exportColumns, option } from '@/option/vehicle/transport-change-record';
@@ -478,7 +478,7 @@ export default {
};
},
buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
},
handleTemplate() {
exportBlob(
+2 -2
View File
@@ -197,7 +197,7 @@ import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
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 VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { mapGetters } from 'vuex';
@@ -673,7 +673,7 @@ export default {
};
},
buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
},
handleTemplate() {
exportBlob(
+22 -22
View File
@@ -49,28 +49,28 @@ export default ({ mode, command }) => {
__VUE_I18N_LEGACY_API__: true,
__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: {
// port: 2889,
// proxy: {
// '/api': {
// target: 'http://172.16.203.221:8000',
// changeOrigin: true,
// },
// },
//},
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: {
// port: 2889,
// proxy: {
// '/api': {
// target: 'http://172.16.203.228:8000',
// changeOrigin: true,
// },
// },
// },
resolve: {
alias: {
'~': resolve(__dirname, './'),