Compare commits
186 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74988c0b5e | |||
| 4c117d87a5 | |||
| d6bcb12c20 | |||
| e1d87f5788 | |||
| 56b8f68449 | |||
| 0ac17dea0b | |||
| 39e3115fbe | |||
| 9c0f7cb3f4 | |||
| 7b478bdbf2 | |||
| 87d6ff97a3 | |||
| e3bd44e8fc | |||
| 324daf9d4d | |||
| 37ac683fb4 | |||
| c402d68aea | |||
| e8f6311477 | |||
| fa78a68df6 | |||
| 320422856c | |||
| 0add4c5cdc | |||
| f6621392b1 | |||
| 2a5eca8393 | |||
| e4ae1d3c77 | |||
| 96783fcd7b | |||
| 372e8826a2 | |||
| 8d9fe998c0 | |||
| 92a8d4aab1 | |||
| 59a1ca8a0d | |||
| b9de34aa0e | |||
| 5544cfb3ca | |||
| fcd5ef7df9 | |||
| 489b406214 | |||
| 353148eb7f | |||
| 293b4fc6ce | |||
| 53b588063e | |||
| 0889f29c59 | |||
| f1c004813e | |||
| d67cf55491 | |||
| d7969c191a | |||
| 2f64abfc83 | |||
| a657b9d0ef | |||
| 9558426da1 | |||
| 6384e80758 | |||
| 3f019c9ac5 | |||
| e7c76c803f | |||
| 90edc4b864 | |||
| 6a21b4ac8d | |||
| d05f8fd636 | |||
| d968c418a8 | |||
| d100e7a271 | |||
| 1c798046fd | |||
| 8d25e186b1 | |||
| c1863d0b6a | |||
| 4a0af452e8 | |||
| 447b2d9d76 | |||
| 603038d5ff | |||
| 673c47771b | |||
| 3168d365d0 | |||
| 5c97d3b5c5 | |||
| 09ce6472b0 | |||
| 5b435bef04 | |||
| d3af455374 | |||
| 0c4bb7cd82 | |||
| 4c9bbf1eb7 | |||
| 31794bf26a | |||
| 1622015724 | |||
| 0e57de5c71 | |||
| 5cb7eb16d5 | |||
| ccaf944782 | |||
| aee661772b | |||
| bbe71e340a | |||
| 1ac741f785 | |||
| f7d8fbf16b | |||
| 912c1169a6 | |||
| 847f09b14d | |||
| 6596722601 | |||
| 2ae817ec38 | |||
| 6f2383a7e5 | |||
| 8db52ef168 | |||
| 38ca250242 | |||
| 73c12cc1c7 | |||
| f4542cd0b8 | |||
| 4b165471e3 | |||
| d1e779bbcc | |||
| 6d80ffde6c | |||
| acfbb4c54e | |||
| 44809ad303 | |||
| f440343830 | |||
| c1bc55e133 | |||
| baec42f876 | |||
| 4d3b6cdaed | |||
| 96b675b3be | |||
| 6067d24e3b | |||
| e051327f2d | |||
| f7759fd9e7 | |||
| 91cb3ee8c2 | |||
| b2a6d73bac | |||
| 4287133f84 | |||
| 4b01316f64 | |||
| 0b170fe5c1 | |||
| 4f29b1e0eb | |||
| 6db599400e | |||
| c5a74acd00 | |||
| f23d65e41e | |||
| fb91768585 | |||
| 6b0cb37699 | |||
| c867c5d7a5 | |||
| 411fdd2425 | |||
| 903e0a4d7b | |||
| 6461c3d80f | |||
| 8380351529 | |||
| f8a238d0c0 | |||
| 5c6fcbe951 | |||
| 7997089b16 | |||
| 089b74479b | |||
| cf542313f7 | |||
| a19097d1be | |||
| 87aa64d0c1 | |||
| fbad28b717 | |||
| 19972aa8d8 | |||
| 7ea96bcdc0 | |||
| 5569f373c5 | |||
| 7887ae2342 | |||
| 9919fb9c89 | |||
| 6eab96b620 | |||
| b39501291c | |||
| 9315431273 | |||
| 2c9ce33c10 | |||
| 85602ff98f | |||
| 749b314ceb | |||
| 74780b8777 | |||
| 5bc1f69a7f | |||
| 6de87ed4e2 | |||
| 18aeb08c90 | |||
| cdd30b8211 | |||
| b7af496a43 | |||
| 6762cf7b83 | |||
| a87b5e4b95 | |||
| d8f4f22b9c | |||
| 9e5f53702f | |||
| 8640238cbe | |||
| b077ba5be4 | |||
| b9b579dc14 | |||
| 6ae23bb959 | |||
| ef1a43f947 | |||
| 4294c92e43 | |||
| b840530fe9 | |||
| e6a1370de2 | |||
| 65296618a7 | |||
| c4ca347f0e | |||
| b4ecc90470 | |||
| 0484c2359d | |||
| 7fca74907f | |||
| d7ddeacbbf | |||
| 3d59203dbe | |||
| 3700f5d82b | |||
| 05b16bbea0 | |||
| dc28bc2afa | |||
| 730c4e2a6a | |||
| 9b42551bcd | |||
| 66944bdaf9 | |||
| 09da05c75e | |||
| 8bc312502f | |||
| 9b40800aab | |||
| f49f36fb60 | |||
| 7fd8140761 | |||
| aabe67e4d6 | |||
| e63c7fdd42 | |||
| ec56ee07be | |||
| a5eeda6152 | |||
| 5c45e9b2c9 | |||
| 331a39b37d | |||
| aa0aec3704 | |||
| 622705325b | |||
| 4657fc29bd | |||
| 884de893db | |||
| cc73d544ff | |||
| 10fecb5a54 | |||
| bab6bdba27 | |||
| a554ccf38e | |||
| a748e52551 | |||
| e89e78b527 | |||
| 2a03991297 | |||
| 884cc4355f | |||
| 58aaf79445 | |||
| f067518c0e | |||
| 11460f3378 | |||
| 7f543937fb |
@@ -1,5 +1,6 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
pnpm-lock.yaml
|
||||
/dist
|
||||
|
||||
/tests/e2e/videos/
|
||||
|
||||
@@ -9,3 +9,10 @@
|
||||
2. `src/styles/element-ui.scss` 增加全局规则 `.vehicle-dispatch-dialog .el-form-item__content > .el-cascader { width:100%; max-width:100% }`。
|
||||
- 自检:dev(2889) `curl /src/styles/element-ui.scss` 与 `/src/option/transportCapacity/vehicle-dispatch.js` 均 200,编译产物含新规则。
|
||||
- 遗留观察:同文件里 `.vehicle-dispatch-page :deep(.avue--detail ...)` 那一批查看态禁用样式,同样因 Teleport 属死规则(未改,待确认是否需要迁到 `.vehicle-dispatch-dialog` 作用域)。
|
||||
|
||||
## 追加修正(同日 20:xx):第一次改完用户反馈「还是没对齐」
|
||||
|
||||
- 原因一:选择器写成了 `.el-form-item__content > .el-cascader`(直接子级)。**avue 表单项在 `el-form-item__content` 与控件之间还有两层包裹 div**(`packages/element-plus/form/index3.js`:content > div > [div|elTooltip] > 控件),必须用后代选择器。已改成 `.vehicle-dispatch-dialog .el-form-item__content .el-cascader`。
|
||||
- 原因二(更隐蔽):option 用 `labelWidth: 'auto'`,**EP 的 auto 是逐项计算**(实测:3 字 54/64px、2 字 40px、4 字无星号 68px、4 字带星号 78px),label 越宽控件可用宽度越小 → 车牌号 input 486px、使用部门 cascader 472px,仍差 14px。已加 `.vehicle-dispatch-dialog .el-form-item__label { flex: 0 0 calc(4em + 24px) !important; width: calc(4em + 24px) !important }`(本弹窗左右两列最长 label 均为 4 汉字)。
|
||||
- 验证方式(可复用):临时在项目根建 `cascader-width-check.html`,Vite 直接服务 → `import './src/option/...'` 用真实 option + `import './src/styles/element-ui.scss'`,挂 avue-crud 并 `mounted(){ this.$refs.crud.rowAdd() }` 弹出真实弹窗;用 agent-browser(`/Users/gxwebsoft/.workbuddy/binaries/node/workspace/node_modules/.bin/agent-browser`)`open` + `eval` 读 DOM 链路与 `getBoundingClientRect`。修完已删除该临时文件。
|
||||
- 结果:全部 label 80px,车牌号 input 与使用部门 cascader 均为 left=732 / width=470 / right=1202,完全一致。
|
||||
|
||||
@@ -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 次 ✓。
|
||||
|
||||
@@ -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,等用户确认。
|
||||
@@ -0,0 +1,31 @@
|
||||
# 2026-10-20
|
||||
|
||||
## settlement-import 列表页工具栏对齐 receipt-settlement
|
||||
|
||||
- `src/views/settlement/settlement-import.vue`:
|
||||
- 左侧只保留「新增」(`type="primary"`),删掉原来的 `plain` 文字「刷新」按钮(刷新统一收进右侧图标组,与 receipt-settlement / settlement-adjustment 一致)。
|
||||
- 右侧接入 `<settlement-table-tools>`(`./components/settlement-table-tools.vue`,即 `table-header-tools` 包装):刷新 / 列设置 / 搜索显隐 / 表格尺寸 4 键,`@refresh="refreshTable"`。
|
||||
- 表格列改为配置化,新增 `src/option/settlement/settlementImportTable.js`(`settlementImportTableColumns`),页面加 `tableColumns / visibleColumnKeys / tableSize / columnSettingOptions / visibleTableColumns`,`el-table` 加 `:size="tableSize"`。
|
||||
- 列渲染标志:`link / status / enum / billType / count`(注意**不能用 `type` 作 key**,`type` 是 el-table-column 真实 prop,会把列变成 selection/index 列)。
|
||||
- 新增 `displayValue()`;`settlementBillTypeLabel('')` 返回 `-`。
|
||||
- 序号列改 `fixed="left"`;数据列与操作列统一 `align: 'center'`,操作链接 `justify-content: center`。
|
||||
- 校验方式(无 dev server 时):项目根目录临时 `sfc-check.mjs` 用 `vue/compiler-sfc` 的 `parse + compileTemplate + compileScript`;SCSS 用 `sass.compileString`。用完即删。
|
||||
|
||||
## 追加:用户要求「列显隐 + 显示方式」→ 两个页面改 avue-crud
|
||||
|
||||
用户反馈上面那版不对:右侧工具按钮必须是 **avue-crud 原生**的 4 键,第 2 键是「列显隐」抽屉(不是「列设置」勾选弹窗),第 4 键是「显示方式」(表格↔卡片),不是表格尺寸循环。确认后范围:`settlement-import` + `formal-settlement`(receipt/payment 共用)都改 avue-crud。
|
||||
|
||||
- 关键结论:**「列显隐 + 显示方式」只有 avue-crud 原生才有**,avue 的 `dialog-column` 依赖 `inject: ['crud']`,脱离 avue-crud 拿不出来;`table-header-tools` 只是仿制件,做不到。所以目标页只能整页改 avue-crud。
|
||||
- avue 右侧按钮源码:`node_modules/@smallwei/avue/lib/packages/element-plus/crud/menu/header-menu3.js`(refreshBtn/columnBtn/searchBtn/gridBtn,`gridBtn` → `handleGridShow()` 切 `gridShow` 表格↔卡片);图标默认配置在同目录 `config.js`。
|
||||
- 新增 `src/option/settlement/settlementImport.js`(`settlementImportOption`)、`src/option/settlement/formalSettlement.js`(`formalSettlementOption`,表格列复用 `formalSettlementTable.js` 的 `formalSettlementTableColumns` + `COLUMN_EXTRA` 增量配置,避免两处维护)。
|
||||
- `settlement-import.vue` 整页改 avue-crud(删除手写的搜索区/表格/分页);`formal-settlement-table-panel.vue` 由 el-table 改成包一层 `<avue-crud>`,父页面保留数据加载与全部业务动作。
|
||||
- ⚠️ **avue 的搜索自定义控件要用 `column.renderSearch`(`h()` 渲染),不能用 `searchslot`** —— avue 3.9.2 根本不读 `searchslot`(全库无此字段),而 `searchslot: true` 还会被 `header-search2.js` 的 `searchXxx → xxx` 映射成 `slot: true`。真正的搜索插槽是 avue-crud 上 `${prop}-search`(`crud.searchSlot`,见 `index2.js` `getSlotList(["-search"])`),但其作用域**没有 `row`**(只有 value/column/label/size/readonly/disabled/dic),`mileage-record.vue` 用 `{ row }` 是错的。已用于批次号下拉(filterable + allowCreate)。
|
||||
- avue `formatter` 签名确认:`detail.js` → `column.formatter(row, row[column.prop], result, column)`。
|
||||
- `on-load` 由 `table-page2.js` 在 created / 翻页 / 改页容量时触发,参数是内部 `defaultPage`(已 `Object.assign` 合并外部 page)。
|
||||
- 分页透传写法(父持 page、子包 avue-crud):子组件用 computed `crudPage{get/set}`,setter 里 `Object.assign(this.page, value)` 原地回写,避免 prop 直接赋值告警。
|
||||
- ⚠️ `src/option/settlement/formalSettlementTable.js` **不能删** —— `formal-settlement-editor.vue` 还从它导入 `candidateColumns/candidateDetailColumns/detailColumns/sourceColumns/summaryColumns`(我一度删掉,Vite 转换报错后从 HEAD 恢复)。
|
||||
- 校验方式升级:沙箱里 curl 不通 dev server(502),改用 `vite.createServer({ middlewareMode:true })` + `server.transformRequest(path)` 就地转换,能同时暴露依赖解析失败(如上面误删文件)。脚本用完即删。
|
||||
|
||||
## 遗留未改(仍用 table-header-tools 仿制件)
|
||||
|
||||
pre-settlement、settlement-adjustment、receivable-payable-detail、transport-reconciliation-table-panel、business/{master-order,loading-manage,voucher-manage}、payment/* 手写页。若用户要全站一致,需逐页改 avue-crud。
|
||||
@@ -14,16 +14,82 @@
|
||||
|
||||
**坑四:avue-crud 弹窗 `append-to-body` 默认 true(`dialog-form3.js`:`validData(tableOption.dialogAppendToBody, true)`),DOM Teleport 到 body。** 于是页面里以页面容器为祖先的 scoped 选择器(`.xxx-page :deep(.yyy)`)**命中不到弹窗内容**,尤其 `el-cascader`(inline-block、宽度按内容收缩,不像 `el-input`/`el-autocomplete` 有 `--el-input-width:100%` 兜底)会明显比同行 input 窄。修法:option 里配 `dialogCustomClass: 'xxx-dialog'`,规则写到 `src/styles/element-ui.scss` 的 `.xxx-dialog ...`(项目既有约定,10+ 处同款);或把基准类放在 **Teleport 内部元素**上(如 ship.vue 的 `.ship-form` 挂在 el-form 上,自身带 scope 属性,照样生效)。
|
||||
|
||||
**坑五:avue 表单项控件不是 `.el-form-item__content` 的直接子级。** `form/index3.js` 渲染为 `content > div > [div|elTooltip] > 控件`,所以 `.el-form-item__content > .el-cascader` 这类**直接子级选择器对 avue 表单全部失效**(手写 `<el-form-item>` 才生效),一律用后代选择器。
|
||||
|
||||
**坑六:`labelWidth: 'auto'` 在 Element Plus 里逐项计算,不是全表单共享。** 实测同一弹窗:3 字 54/64px、2 字 40px、4 字 68px、4 字带必填星号 78px;label 越宽 → 控件可用宽度越小,同级控件宽度就不一致(车牌号 input 486 vs 使用部门 cascader 472)。要让同一行控件等宽必须统一 label 宽度:`.xxx-dialog .el-form-item__label { flex: 0 0 calc(Nem + 24px) !important; width: calc(Nem + 24px) !important }`(N=本列最长汉字数,与 ship.vue、`.dialog-form--carded` 同款)。
|
||||
|
||||
**验证 avue 弹窗 DOM/宽度的可复用办法:** 项目根建临时 html(Vite 直接服务)→ 引真实 option 与 `src/styles/element-ui.scss`,挂 avue-crud 并 `mounted(){ this.$refs.crud.rowAdd() }` 弹出真弹窗;用 `agent-browser`(`/Users/gxwebsoft/.workbuddy/binaries/node/workspace/node_modules/.bin/agent-browser`)`open` + `eval` 读 DOM 链路与 `getBoundingClientRect`,用完删文件。
|
||||
|
||||
**坑三:路由表重复定义 → 内容区整页空白(2026-09-18)。** `/business/contract-manage/detail` 在 `router/views/index.js` 定义两次、子路由同名 `合同详情`。重名 `addRoute` 会 `removeRoute(name)` 移除旧子记录,但**不从父记录 children 里摘除**,第一条 Layout 壳记录留在 matcher 且靠前 → `resolve()` 命中它 → `$route.matched.length === 1` → 二级 router-view 无匹配 → 侧栏/标签正常但内容空白。修法:删重复定义。排查:扫「顶层 path + 首个子路由 name」重复项。
|
||||
|
||||
## 列表页右上角按钮组(table-header-tools 标准件)
|
||||
⚠️ **2026-10-14 修正:最终方向是手写列表页改用 `avue-crud`**。用户要求与其它模块"完全一致",而 avue 的列显隐抽屉(`el-drawer`+表格,隐藏/冻结/过滤/排序,行拖拽)是 avue-crud 内部子组件(`inject: ['crud']`),脱离 avue-crud 拿不出来;第 4 键也是 avue 的"表格↔卡片视图"切换。所以 `table-header-tools` 只是过渡仿制件,**新页面不要再接**,直接写 avue-crud。
|
||||
|
||||
手写 `el-table` 列表页若暂时保留,右上角统一用 `src/components/table-header-tools/main.vue`,复刻 avue-crud 默认 4 键:刷新 `el-icon-refresh` / 列设置 `el-icon-operation` / 搜索显隐 `el-icon-search` / 表格尺寸 `el-icon-grid`(default→large→small 循环)。
|
||||
|
||||
- 接入范式:`<table-header-tools :columns="columns" v-model:visible-column-keys="visibleColumnKeys" v-model:table-size="tableSize" v-model:search-visible="searchVisible" @refresh="loadTable" />`;配套:搜索区 `v-if="searchVisible"`、el-table `:size="tableSize"`、列用 computed `visibleColumns = columns.filter(c => visibleColumnKeys.includes(c.prop))`。⚠️ 不能 `v-for` + `v-if` 同元素(Vue3 v-if 优先级更高,取不到 column 变量)。
|
||||
- 表格在子组件时(如 `payment/components/bill-*-table.vue`):子组件加 props `searchVisible` + emits `refresh`/`update:searchVisible`,父页面 `v-model:search-visible` + `@refresh`,搜索子组件加 `v-if="searchVisible"`。
|
||||
- `icon="el-icon-xxx"` 字符串**依赖 Avue 的全局 el-icon 映射**(`main.js` 只注册 EP 的 PascalCase 图标),临时验证页没 `app.use(Avue)` 会渲染成空圆按钮 —— 别误判成组件 bug。
|
||||
- 组件已加 `deactivated()` 关列设置弹窗(防 keep-alive 下 Teleport 弹窗残留)。
|
||||
- 已接入(过渡态,待逐页改 avue):business/{master-order,loading-manage,voucher-manage}、settlement、payment 模块手写页。
|
||||
|
||||
## avue-crud 列表页改造要点(payment 模块样板,2026-10-14)
|
||||
- **列显隐抽屉**:点第 2 键弹出,标题"列显隐"(语言包 `crud.showTitle`),表头 列名/隐藏/冻结/过滤/排序。直接读写 avue crud 的列配置,页面无需实现。
|
||||
- **第 4 键**:`handleGridShow()` = 表格 ↔ 卡片视图(`tableCard` 按列通用渲染),页面无需写卡片模板。
|
||||
- `searchOrder` 是**降序**排序(值越大越靠前):`list.sort((a,b) => (b.searchOrder||0) - (a.searchOrder||0))`。首次改造踩过坑。
|
||||
- `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 进查询参数)。
|
||||
- 搜索参数转换放 `searchChange(params, done)`:日期区间数组拆成 `xxxStartDate/xxxEndDate`;所属组织级联用 `normalizeOrganizationSearch(params, 'deptName')`(见 `mixins/organization-search.js`,option 里该列加 `searchslot: true`)。
|
||||
- 单元格空值统一 `-`:列级 `formatter: (row, value) => ...`;金额 `Number(v||0).toFixed(2)`;状态/链接用 `slot: true` + `#prop` 插槽。
|
||||
- 操作列用 `#menu` 插槽(`menuWidth: 320`),`addBtn/viewBtn/delBtn/editBtn: false` 关掉 avue 自带按钮。
|
||||
|
||||
## 地址/行政区划展示(全站既有规则)
|
||||
「省/市/区县」都要空格分段,但**省与自治区不重复展示**:`上海市浦东新区`→`上海市 浦东新区`,`广西壮族自治区南宁市邕宁区`→`南宁市 邕宁区`(同 `formatRoadLocation` / `formatRouteCityDistrict` / 总单列表 `formatRoadAddress` 一致)。
|
||||
- 统一入口:`src/utils/region-address.js` 的 `formatRegionAddress(value)`(按后缀切分行政区划,`州+市`、`县+区` 连写合并;**只有整串都是行政区划**才重排,站点名「南宁站/广州南站」与含门牌的详细地址原样返回)。v-for 列表里用它做展示时,直接把函数挂进 `methods` 用 ES6 简写(`formatRegionAddress,`)。
|
||||
- ⚠️ 路线节点的名称字段来自后端 `routeJson`:`routes[i].departureName` = 第 i 段**到货地/途经地**(不是本段发货地),末段 = 最终收货地;`master.departureName/arrivalName` 才是总发货/总收货。总单调度/详情都按「上一个节点的 departureName」拼分段起点。
|
||||
|
||||
## 样式硬规则
|
||||
- 底栏 `flex; justify-content:flex-end`,**不写 gap**(靠 EP 默认 `.el-button+.el-button{margin-left:12px}`);浮动底栏必须 `position:fixed`(祖先 overflow:hidden 使 sticky 失效)+ `:global(.avue--collapse .x){left:60px}`、`:global(.avue-layout--horizontal .x){left:0}`,`:global()` 要包整个选择器。
|
||||
- 按钮层级:次要=无 type;中间步骤=`primary plain`;主操作=`primary`(禁绿、禁两个蓝实心)。顺序 `[辅助][取消][保存草稿][提交]`。
|
||||
- 独立页标题 `.archive-page-form__title`;分组用全局 `<section-card>`;上传证件区 `width:100%; max-width:240px` + `.el-upload{height:151px}`。
|
||||
- ⚠️ `<style scoped lang="scss">` 顶层禁 `//` 注释(Vite5 + sass `Unexpected '/'` → 500),顶层用 `/* */`。其余见 `src/styles/element-ui.scss` 与 `AGENTS.md` 4.4。
|
||||
- **搜索栏「展开/收起」唯一标准写法**(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`)。
|
||||
|
||||
## avue-crud 才是「右侧 4 键」的唯一来源(2026-10-20 定论)
|
||||
用户要的右侧工具栏是 **avue-crud 原生**:刷新 / **列显隐**(el-drawer,含 隐藏/冻结/过滤/排序 + 行拖拽)/ 搜索显隐 / **显示方式**(表格↔卡片)。
|
||||
- 源码:`node_modules/@smallwei/avue/lib/packages/element-plus/crud/menu/header-menu3.js`(refreshBtn/columnBtn/searchBtn/gridBtn),`gridBtn` → `handleGridShow()` 切 `gridShow`;图标默认在同目录 `config.js`。「列显隐」文案来自 avue 语言包 `crud.showTitle`(项目 `src/lang/zh.js` 已覆盖)。
|
||||
- **列显隐抽屉依赖 `inject: ['crud']`,脱离 avue-crud 拿不出来**;`components/table-header-tools` 的「列设置勾选弹窗 + 表格尺寸循环」是仿制件,**做不到这两点**。所以「要和 avue 一致」= 整页改用 avue-crud,没有中间方案。
|
||||
- ⚠️ 搜索自定义控件用 `column.renderSearch = scope => h(...)`(`scope.row` 可用,同 `mixins/organization-search.js`)。**不要用 `searchslot`**:avue 3.9.2 全库无此字段,且 `header-search2.js` 的 `searchXxx → xxx` 映射会把 `searchslot` 变成 `slot: true`。
|
||||
- avue-crud 上的 `${prop}-search` 插槽确实存在(`index2.js` 的 `getSlotList(["-search"])`),但**作用域没有 `row`**(只有 value/column/label/size/readonly/disabled/dic)——`mileage-record.vue` 里 `{ row }` 是错的写法,别照抄。
|
||||
- `formatter` 签名:`detail.js` → `column.formatter(row, row[column.prop], result, column)`。
|
||||
- `on-load` 由 `table-page2.js` 在 created / 翻页 / 改页容量时触发,参数是内部 `defaultPage`(已合并外部 page)。分页若由父组件持有、子组件包一层 avue-crud,用 computed `crudPage{get/set}` + setter 里 `Object.assign(this.page, value)` 原地回写。
|
||||
- 已改:settlement-import.vue、formal-settlement.vue + components/formal-settlement-table-panel.vue(receipt/payment 共用)。未改:pre-settlement、settlement-adjustment、receivable-payable-detail、transport-reconciliation-table-panel、business 与 payment 的手写页。
|
||||
- ⚠️ `src/option/settlement/formalSettlementTable.js` 别删:`formal-settlement-editor.vue` 还依赖它的 candidate/detail/source/summary 列。
|
||||
- 沙箱里 curl 打不通本地 dev server(一律 502)。改用 `vite.createServer({middlewareMode:true})` + `server.transformRequest(path)` 就地转换校验,还能暴露依赖解析失败。
|
||||
|
||||
## Element Plus 图标两种用法的机制差异(易误判)
|
||||
`main.js` 里 `for (...ElementPlusIconsVue) app.component(key, component)` 全局注册了全部图标,因此模板里可以写标签形式 `<Location />`(靠全局注册解析)。
|
||||
但 `: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"`),改表格列时先看这两条。
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>cascader width check</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module">
|
||||
import { createApp, h, ref, resolveComponent } from 'vue';
|
||||
import ElementPlus from 'element-plus';
|
||||
import 'element-plus/dist/index.css';
|
||||
import Avue from '@smallwei/avue';
|
||||
import '@smallwei/avue/lib/index.css';
|
||||
import './src/styles/element-ui.scss';
|
||||
|
||||
const option = {
|
||||
dialogCustomClass: 'vehicle-dispatch-dialog',
|
||||
labelPosition: 'right',
|
||||
labelWidth: 'auto',
|
||||
addBtn: false,
|
||||
editBtn: false,
|
||||
delBtn: false,
|
||||
viewBtn: false,
|
||||
selection: false,
|
||||
column: [
|
||||
{ label: '车牌号', prop: 'plateNo', formslot: true, span: 12 },
|
||||
{ label: '使用部门', prop: 'useDepartment', formslot: true, span: 12 },
|
||||
],
|
||||
};
|
||||
|
||||
const tree = [
|
||||
{ id: 1, title: '桂物物流集团', children: [{ id: 2, title: '广西桂物物流服务集团有限公司' }] },
|
||||
];
|
||||
|
||||
const Demo = {
|
||||
data() {
|
||||
return { option, form: {}, data: [], page: { currentPage: 1, pageSize: 10, total: 0 } };
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => this.$refs.crud.rowAdd());
|
||||
},
|
||||
render() {
|
||||
const crud = resolveComponent('avue-crud');
|
||||
return h(
|
||||
crud,
|
||||
{
|
||||
ref: 'crud',
|
||||
modelValue: this.form,
|
||||
'onUpdate:modelValue': v => (this.form = v),
|
||||
option: this.option,
|
||||
data: this.data,
|
||||
},
|
||||
{
|
||||
'plateNo-form': () =>
|
||||
h(resolveComponent('el-autocomplete'), {
|
||||
modelValue: this.form.plateNo,
|
||||
'onUpdate:modelValue': v => (this.form.plateNo = v),
|
||||
placeholder: '请选择车牌号',
|
||||
fetchSuggestions: (q, cb) => cb([]),
|
||||
}),
|
||||
'useDepartment-form': () =>
|
||||
h(resolveComponent('el-cascader'), {
|
||||
modelValue: this.form.useDepartment,
|
||||
'onUpdate:modelValue': v => (this.form.useDepartment = v),
|
||||
options: tree,
|
||||
props: { label: 'title', value: 'id', children: 'children', checkStrictly: true },
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
placeholder: '请选择使用部门',
|
||||
}),
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const app = createApp(Demo);
|
||||
app.use(ElementPlus);
|
||||
app.use(Avue);
|
||||
app.mount('#app');
|
||||
window.__app = app;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+5
-2
@@ -6,7 +6,8 @@
|
||||
"prod": "vite --mode production",
|
||||
"build": "vite build",
|
||||
"build:prod": "vite build --mode production",
|
||||
"serve": "vite preview --host"
|
||||
"serve": "vite preview --host",
|
||||
"typecheck": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
@@ -47,9 +48,11 @@
|
||||
"prettier": "^2.8.7",
|
||||
"sass": "^1.85.1",
|
||||
"terser": "^5.31.1",
|
||||
"typescript": "^7.0.2",
|
||||
"unplugin-auto-import": "^0.11.2",
|
||||
"vite": "^5.4.19",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-vue-setup-extend": "^0.4.0"
|
||||
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||
"vue-tsc": "^3.3.11"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+415
-95
File diff suppressed because it is too large
Load Diff
@@ -58,3 +58,14 @@ export const changeStatus = (id, status) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getBfiExchangeRate = (currencyCode, effectdate) => {
|
||||
return request({
|
||||
url: '/blade-transport/bfi/exchange-rate',
|
||||
method: 'get',
|
||||
params: {
|
||||
currencyCode,
|
||||
effectdate,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -36,6 +36,8 @@ export const approve = id =>
|
||||
url: `${baseUrl}/approve`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
// 审批通过后同步推送合规(附件转传可达 20s+),覆盖全局 10s 超时
|
||||
timeout: 120000,
|
||||
});
|
||||
|
||||
export const reject = id =>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import request from '@/axios';
|
||||
|
||||
const baseUrl = '/blade-transport/hcs';
|
||||
|
||||
// 合规推送涉及 HCS 远程多次调用(风险校验+附件转传+起草),可达 20s+,覆盖全局 10s 超时
|
||||
export const repush = (bizType, id) =>
|
||||
request({
|
||||
url: `${baseUrl}/sync/${bizType}/${id}`,
|
||||
method: 'post',
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
// 批量推送为逐条同步推送,耗时随勾选行数线性增长,进一步放宽超时
|
||||
export const repushBatch = (bizType, ids) =>
|
||||
request({
|
||||
url: `${baseUrl}/sync/${bizType}/batch`,
|
||||
method: 'post',
|
||||
data: ids,
|
||||
timeout: 300000,
|
||||
});
|
||||
|
||||
export const getTemplates = () =>
|
||||
request({
|
||||
url: `${baseUrl}/master-data/template`,
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
export const refreshMasterData = () =>
|
||||
request({
|
||||
url: `${baseUrl}/master-data/refresh`,
|
||||
method: 'post',
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
export const getRisk = customerCode =>
|
||||
request({
|
||||
url: `${baseUrl}/risk`,
|
||||
method: 'get',
|
||||
params: { customerCode },
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// 客商风险批量查询(客商列表页展示用):入参编号数组,后端逐个查 C101-1,单个失败跳过
|
||||
export const getRiskBatch = customerCodes =>
|
||||
request({
|
||||
url: `${baseUrl}/risk/batch`,
|
||||
method: 'get',
|
||||
params: { customerCodes: (customerCodes || []).join(',') },
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
// 客商风险按名称查询(客商档案表单展示用):名称反查编号后口径同编号查询
|
||||
export const getRiskByName = customerName =>
|
||||
request({
|
||||
url: `${baseUrl}/risk/by-name`,
|
||||
method: 'get',
|
||||
params: { customerName },
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
export const uploadAttachment = formData =>
|
||||
request({
|
||||
url: `${baseUrl}/attachment/upload`,
|
||||
method: 'post',
|
||||
data: formData,
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
export const downloadAttachment = (fdId, name) =>
|
||||
request({
|
||||
url: `${baseUrl}/attachment/download`,
|
||||
method: 'get',
|
||||
params: { fdId, name },
|
||||
responseType: 'blob',
|
||||
timeout: 120000,
|
||||
});
|
||||
|
||||
export const applyContractSpec = data =>
|
||||
request({
|
||||
url: `${baseUrl}/contract/spec-apply`,
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
export const confirmContractSpec = (id, result) =>
|
||||
request({
|
||||
url: `${baseUrl}/contract/spec/confirm`,
|
||||
method: 'post',
|
||||
params: { id, result },
|
||||
});
|
||||
|
||||
export const unwrapData = response => {
|
||||
const data = response?.data?.data ?? response?.data;
|
||||
return data && typeof data === 'object' && 'data' in data ? data.data : data;
|
||||
};
|
||||
@@ -24,11 +24,16 @@ export const getDetail = id =>
|
||||
},
|
||||
});
|
||||
|
||||
export const getCarrierContracts = projectIds =>
|
||||
export const getCarrierContracts = (projectIds, waybillIds = []) =>
|
||||
request({
|
||||
url: `${baseUrl}/carrier-contracts`,
|
||||
method: 'get',
|
||||
params: { projectIds: (projectIds || []).join(',') },
|
||||
params: {
|
||||
projectIds: (projectIds || []).join(','),
|
||||
...(waybillIds || []).filter(Boolean).length
|
||||
? { waybillIds: (waybillIds || []).filter(Boolean).join(',') }
|
||||
: {},
|
||||
},
|
||||
});
|
||||
|
||||
export const saveDraft = row =>
|
||||
|
||||
@@ -43,6 +43,8 @@ export const approve = id =>
|
||||
url: `${baseUrl}/approve`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
// 审批通过后同步推送合规(远程多次调用),覆盖全局 10s 超时
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
export const reject = id =>
|
||||
|
||||
@@ -14,6 +14,13 @@ export const cancel = api.cancel;
|
||||
export const reassign = api.reassign;
|
||||
export const complete = api.complete;
|
||||
export const batchComplete = api.batchComplete;
|
||||
/** 确认回单:回单节点配成「需业务员确认」时用;后端校验司机打卡节点齐全且无未了结异常后才置完成 */
|
||||
export const confirmReturn = id =>
|
||||
request({
|
||||
url: `${baseUrl}/confirm-return`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
});
|
||||
export const changeRoute = data =>
|
||||
request({
|
||||
url: `${baseUrl}/change-route`,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import request from '@/axios';
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
/** 审核按钮统一超时:审批通过后可能同步推送金蝶等耗时操作,覆盖全局 10s */
|
||||
export const APPROVE_TIMEOUT = 30000;
|
||||
|
||||
/**
|
||||
* 文件流返回
|
||||
* @param url 接口地址
|
||||
|
||||
@@ -6,17 +6,17 @@ 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 getExpiryCounts = () => request({ url: `${baseUrl}/expiry-counts`, method: 'get' });
|
||||
export const getAvailableOptions = (keyword, deptId, selectedId) =>
|
||||
export const getAvailableOptions = (keyword, deptId, selectedId, payerName, payeeName) =>
|
||||
request({
|
||||
url: `${baseUrl}/available-options`,
|
||||
method: 'get',
|
||||
params: { keyword, deptId, selectedId },
|
||||
params: { keyword, deptId, selectedId, payerName, payeeName },
|
||||
});
|
||||
export const getAvailablePage = (current, size, keyword, deptId, selectedId) =>
|
||||
export const getAvailablePage = (current, size, keyword, deptId, selectedId, payerName, payeeName) =>
|
||||
request({
|
||||
url: `${baseUrl}/available-page`,
|
||||
method: 'get',
|
||||
params: { current, size, keyword, deptId, selectedId },
|
||||
params: { current, size, keyword, deptId, selectedId, payerName, payeeName },
|
||||
});
|
||||
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
||||
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from '@/axios';
|
||||
import { APPROVE_TIMEOUT } from '@/api/common';
|
||||
|
||||
const baseUrl = '/blade-transport/invoice-application';
|
||||
|
||||
@@ -18,11 +19,12 @@ export const getReceiverInformation = settlementIds =>
|
||||
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
|
||||
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||
export const approve = data =>
|
||||
request({ url: `${baseUrl}/approve`, method: 'post', data, timeout: APPROVE_TIMEOUT });
|
||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||
export const syncKingdee = id =>
|
||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
|
||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
|
||||
|
||||
export const invoiceTypeOptions = [
|
||||
{ label: '电子专票', value: 'electronic_special' },
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import request from '@/axios';
|
||||
import { APPROVE_TIMEOUT } from '@/api/common';
|
||||
|
||||
const baseUrl = '/blade-transport/invoice-receipt';
|
||||
|
||||
export const getList = (current, size, params) =>
|
||||
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
|
||||
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
|
||||
export const getInvoicePool = keyword =>
|
||||
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params: { keyword } });
|
||||
export const queryInvoicePool = params =>
|
||||
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params });
|
||||
export const downloadInvoicePoolAttachment = invoiceNo =>
|
||||
// 金蝶票池发票文件下载(后端代理 A801-2 流式回源),耗时较普通接口长,放宽到 120s
|
||||
request({
|
||||
url: `${baseUrl}/invoice-pool/attachment/download`,
|
||||
method: 'get',
|
||||
params: { invoiceNo },
|
||||
responseType: 'blob',
|
||||
timeout: 120000,
|
||||
});
|
||||
export const getSettlementCandidates = (keyword, receiptId) =>
|
||||
request({
|
||||
url: `${baseUrl}/settlement-candidates`,
|
||||
@@ -22,11 +32,12 @@ export const getReferenceInformation = settlementIds =>
|
||||
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
|
||||
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||
export const approve = data =>
|
||||
request({ url: `${baseUrl}/approve`, method: 'post', data, timeout: APPROVE_TIMEOUT });
|
||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||
export const syncKingdee = id =>
|
||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
|
||||
export const pushKingdee = ids =>
|
||||
request({ url: `${baseUrl}/push-kingdee`, method: 'post', timeout: 30000, data: ids });
|
||||
|
||||
export const approvalStatusOptions = [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
@@ -36,8 +47,9 @@ export const approvalStatusOptions = [
|
||||
{ label: '已作废', value: 'voided' },
|
||||
];
|
||||
|
||||
export const kingdeeStatusOptions = [
|
||||
{ label: '未同步', value: 'unsynced' },
|
||||
{ label: '已同步', value: 'synced' },
|
||||
{ label: '同步失败', value: 'failed' },
|
||||
export const kingdeePushStatusOptions = [
|
||||
{ label: '未推送', value: 'unsynced' },
|
||||
{ label: '已保存收票单', value: 'pushed' },
|
||||
{ label: '已指定应付', value: 'assigned' },
|
||||
{ label: '推送失败', value: 'failed' },
|
||||
];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from '@/axios';
|
||||
import { APPROVE_TIMEOUT } from '@/api/common';
|
||||
|
||||
const baseUrl = '/blade-transport/payment-application';
|
||||
export const getList = (current, size, params) =>
|
||||
@@ -10,16 +11,29 @@ 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 });
|
||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||
export const approve = data =>
|
||||
request({ url: `${baseUrl}/approve`, method: 'post', data, timeout: APPROVE_TIMEOUT });
|
||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||
// 同步金蝶涉及多次金蝶远程接口调用(A103-2 预检 + A103-1/A103-3 推送),单笔可达 20s+,覆盖全局 10s 超时
|
||||
export const syncKingdee = id =>
|
||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
|
||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
|
||||
// 批量同步逐单单据级事务串行推送,耗时随单量线性增长
|
||||
export const syncKingdeeBatch = ids =>
|
||||
request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', data: ids });
|
||||
request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', timeout: 30000, data: ids });
|
||||
// 回写任务按 paymentNo 分批远程查询 A103-2/A103-4,同样可能超过全局超时
|
||||
export const syncKingdeeResult = () =>
|
||||
request({ url: `${baseUrl}/sync-kingdee-result`, method: 'post', timeout: 30000 });
|
||||
|
||||
export const paymentTypeOptions = [
|
||||
{ label: '项目预付', value: 'project_advance' },
|
||||
@@ -36,5 +50,8 @@ export const approvalStatusOptions = [
|
||||
export const kingdeeStatusOptions = [
|
||||
{ label: '未生成', value: 'unsynced' },
|
||||
{ label: '已生成', value: 'synced' },
|
||||
{ label: '付款中', value: 'paying' },
|
||||
{ label: '已付款', value: 'paid' },
|
||||
{ label: '已关闭', value: 'closed' },
|
||||
{ label: '生成失败', value: 'failed' },
|
||||
];
|
||||
|
||||
@@ -8,14 +8,24 @@ export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get'
|
||||
export const updateAttachments = data =>
|
||||
request({ url: `${baseUrl}/attachments`, method: 'post', data });
|
||||
export const voidClaim = id => request({ url: `${baseUrl}/void`, method: 'post', params: { id } });
|
||||
export const queryKingdeeResult = () =>
|
||||
request({ url: `${baseUrl}/query-kingdee-result`, method: 'post' });
|
||||
|
||||
export const claimStatusOptions = [
|
||||
{ label: '已认领', value: 'claimed' },
|
||||
{ label: '已作废', value: 'voided' },
|
||||
];
|
||||
|
||||
export const pushStatusOptions = [
|
||||
{ label: '未推送', value: 'none' },
|
||||
{ label: '已推送', value: 'pushed' },
|
||||
{ label: '推送失败', value: 'failed' },
|
||||
];
|
||||
|
||||
export const kingdeeBillStatusOptions = [
|
||||
{ label: '未生成', value: 'none' },
|
||||
{ label: '已推送', value: 'pushed' },
|
||||
{ label: '审核通过', value: 'approved' },
|
||||
{ label: '已收款', value: 'received' },
|
||||
{ label: '处理失败', value: 'failed' },
|
||||
];
|
||||
|
||||
@@ -12,10 +12,25 @@ export const getSettlementCandidates = (keyword, flowId) =>
|
||||
params: { keyword, flowId },
|
||||
});
|
||||
export const claim = data => request({ url: `${baseUrl}/claim`, method: 'post', data });
|
||||
export const sync = data => request({ url: `${baseUrl}/sync`, method: 'post', data });
|
||||
export const sync = () => request({ url: `${baseUrl}/sync`, method: 'post' });
|
||||
export const pushKingdee = flowId =>
|
||||
request({ url: `${baseUrl}/push-kingdee`, method: 'post', params: { flowId } });
|
||||
|
||||
export const claimStatusOptions = [
|
||||
{ label: '未认领', value: 'unclaimed' },
|
||||
{ label: '部分认领', value: 'partial' },
|
||||
{ label: '认领完成', value: 'claimed' },
|
||||
];
|
||||
|
||||
export const pushStatusOptions = [
|
||||
{ label: '未推送', value: 'none' },
|
||||
{ label: '已推送', value: 'pushed' },
|
||||
{ label: '推送失败', value: 'failed' },
|
||||
];
|
||||
|
||||
export const kingdeeBillStatusOptions = [
|
||||
{ label: '已推送', value: 'pushed' },
|
||||
{ label: '审核通过', value: 'approved' },
|
||||
{ label: '已收款', value: 'received' },
|
||||
{ label: '处理失败', value: 'failed' },
|
||||
];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from '@/axios';
|
||||
import { APPROVE_TIMEOUT } from '@/api/common';
|
||||
|
||||
const baseUrl = '/blade-transport/formal-settlement';
|
||||
|
||||
@@ -27,11 +28,20 @@ export const getCandidateDetails = (current, size, params) =>
|
||||
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
|
||||
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||
export const approve = data =>
|
||||
request({ url: `${baseUrl}/approve`, method: 'post', data, timeout: APPROVE_TIMEOUT });
|
||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||
export const syncKingdee = id =>
|
||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
|
||||
// 同步金蝶涉及多次金蝶远程接口调用(A102-4 预检 + A102-2 保存),可达 20s+,覆盖全局 10s 超时
|
||||
// config.meta.suppressErrorMessage=true 时由页面弹框汇总结果,避免拦截器重复 toast
|
||||
export const syncKingdee = (id, config = {}) =>
|
||||
request({
|
||||
url: `${baseUrl}/sync-kingdee`,
|
||||
method: 'post',
|
||||
timeout: 30000,
|
||||
params: { id },
|
||||
...config,
|
||||
});
|
||||
export const getDetailFees = detailId =>
|
||||
request({ url: `${baseUrl}/detail-fees`, method: 'get', params: { detailId } });
|
||||
export const getReceiptClaims = formalSettlementId =>
|
||||
@@ -48,3 +58,5 @@ export const applyPayments = data =>
|
||||
request({ url: `${baseUrl}/apply-payments`, method: 'post', data });
|
||||
export const claimInvoices = data =>
|
||||
request({ url: `${baseUrl}/claim-invoices`, method: 'post', data });
|
||||
export const queryInvoicePool = params =>
|
||||
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from '@/axios';
|
||||
import { APPROVE_TIMEOUT } from '@/api/common';
|
||||
|
||||
const baseUrl = '/blade-transport/pre-settlement';
|
||||
|
||||
@@ -24,6 +25,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`,
|
||||
@@ -40,7 +43,8 @@ export const removeDetail = (id, detailId) =>
|
||||
|
||||
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
||||
|
||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||
export const approve = data =>
|
||||
request({ url: `${baseUrl}/approve`, method: 'post', data, timeout: APPROVE_TIMEOUT });
|
||||
|
||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ export const approve = id => request({ url: `${baseUrl}/approve`, method: 'post'
|
||||
export const returnBill = (id, reason) =>
|
||||
request({ url: `${baseUrl}/return`, method: 'post', data: { id, reason } });
|
||||
export const repush = id => request({ url: `${baseUrl}/repush`, method: 'post', params: { id } });
|
||||
export const syncKingdee = id =>
|
||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
|
||||
export const kingdeeWriteOffPrecheck = formalSettlementId =>
|
||||
request({ url: `${baseUrl}/kingdee-writeoff-precheck`, method: 'get', params: { formalSettlementId } });
|
||||
|
||||
export const approvalStatusOptions = [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
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 });
|
||||
|
||||
export const removeDraft = id =>
|
||||
request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||
|
||||
export const getHistoryWaybills = params =>
|
||||
request({
|
||||
url: `${baseUrl}/history-waybills`,
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
@@ -1,5 +1,8 @@
|
||||
import request from '@/axios';
|
||||
|
||||
/** 提交审核流(MK)统一超时:客商/项目/合同/预结算/正式结算/付款/开票/收票/汇票付款 */
|
||||
export const MK_SUBMIT_TIMEOUT = 30000;
|
||||
|
||||
/**
|
||||
* 调用 MK processSubmit 提交审核流
|
||||
* @param {Object} data
|
||||
@@ -15,6 +18,7 @@ export const processSubmit = data => {
|
||||
url: '/blade-system/businessProcess/processSubmit',
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: MK_SUBMIT_TIMEOUT,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -28,6 +32,7 @@ export const processDelete = data => {
|
||||
url: '/blade-system/businessProcess/processDelete',
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: MK_SUBMIT_TIMEOUT,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+10
-1
@@ -77,7 +77,8 @@ export const syncOaDepartment = (
|
||||
size = 20,
|
||||
signal,
|
||||
incremental = false,
|
||||
modifiedSince
|
||||
modifiedSince,
|
||||
companyOaId
|
||||
) => {
|
||||
return request({
|
||||
url: '/blade-system/dept/sync-oa-department',
|
||||
@@ -87,6 +88,7 @@ export const syncOaDepartment = (
|
||||
size,
|
||||
incremental: !!incremental,
|
||||
modifiedSince: modifiedSince || undefined,
|
||||
companyOaId: companyOaId || undefined,
|
||||
},
|
||||
timeout: 60000,
|
||||
signal,
|
||||
@@ -137,3 +139,10 @@ export const getPlatformCompanySelect = () => {
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
|
||||
export const getBusinessDeptSelect = () => {
|
||||
return request({
|
||||
url: '/blade-system/dept/business-dept-select',
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import request from '@/axios';
|
||||
|
||||
export const getRoleMenuDataScopeTree = roleId => {
|
||||
return request({
|
||||
url: '/blade-system/role-menu-data-scope/tree',
|
||||
method: 'get',
|
||||
params: {
|
||||
roleId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const submitRoleMenuDataScope = data => {
|
||||
return request({
|
||||
url: '/blade-system/role-menu-data-scope/submit',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
};
|
||||
@@ -152,6 +152,28 @@ export const getScoreTemplate = quantificationId => {
|
||||
});
|
||||
};
|
||||
|
||||
export const queryKingdeeCustomer = creditCode => {
|
||||
return request({
|
||||
url: '/blade-transport/bfi/customer/query-by-credit-code',
|
||||
method: 'get',
|
||||
timeout: 30000,
|
||||
params: {
|
||||
creditCode,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const syncKingdee = id => {
|
||||
return request({
|
||||
url: '/blade-transport/customer-archive/sync-kingdee',
|
||||
method: 'post',
|
||||
timeout: 30000,
|
||||
params: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const recognizeBusinessLicenseOcr = imageUrl => {
|
||||
return request({
|
||||
url: '/blade-transport/baidu-ocr/recognize-url',
|
||||
|
||||
+54
-18
@@ -74,12 +74,15 @@ const redirectToLogin = () => {
|
||||
* 统一弹出错误提示并返回带标记的 Error。
|
||||
* `_notified` 标记用于让业务层(批量导入等)识别「拦截器已经提示过」,
|
||||
* 避免同一条错误被弹两次。
|
||||
* meta.suppressErrorMessage=true 时由业务层自行弹框汇总,拦截器不再 toast。
|
||||
*/
|
||||
const notifiedError = message => {
|
||||
ElMessage({
|
||||
message: message,
|
||||
type: 'error',
|
||||
});
|
||||
const notifiedError = (message, config) => {
|
||||
if (config?.meta?.suppressErrorMessage !== true) {
|
||||
ElMessage({
|
||||
message: message,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
const error = new Error(message);
|
||||
error._notified = true;
|
||||
return error;
|
||||
@@ -102,12 +105,27 @@ const isJsonContentType = res => {
|
||||
return contentType.includes('application/json') || contentType.includes('text/json');
|
||||
};
|
||||
|
||||
/**
|
||||
* 是否为可按文本读取的 Blob / Blob-like(含跨 realm)。
|
||||
* 禁止对普通业务 JSON 对象调用 .text()。
|
||||
*/
|
||||
const isBlobLikeData = value =>
|
||||
value != null &&
|
||||
typeof value === 'object' &&
|
||||
typeof value.size === 'number' &&
|
||||
typeof value.type === 'string' &&
|
||||
typeof value.text === 'function';
|
||||
|
||||
/**
|
||||
* 判断响应体是否已是解析好的对象(无需再解析)。
|
||||
* 注意 Blob 也是 object,必须排除,否则会把待解析的文件流误判为已完成。
|
||||
*/
|
||||
const isPlainObject = value =>
|
||||
value !== null && typeof value === 'object' && !(value instanceof Blob);
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
!(value instanceof Blob) &&
|
||||
!isBlobLikeData(value) &&
|
||||
!(value instanceof ArrayBuffer);
|
||||
//跨域请求,允许保存cookie
|
||||
axios.defaults.withCredentials = true;
|
||||
// NProgress Configuration
|
||||
@@ -187,15 +205,20 @@ axios.interceptors.response.use(
|
||||
// 用户就会以为系统崩了——而后端其实返回了明确的业务提示(如「未读取到数据行…」)。
|
||||
// 这里按 Content-Type 判断:JSON 才解析回对象;真·文件流(Excel 等)保持原样不动。
|
||||
// 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。
|
||||
// 仅对真正的 Blob/Blob-like 调 .text(),避免业务 JSON 已解析后出现
|
||||
// 「e.data.text is not a function」。
|
||||
if (isJsonContentType(res) && !isPlainObject(res.data)) {
|
||||
try {
|
||||
let text = '';
|
||||
if (typeof res.data?.text === 'function') {
|
||||
text = await res.data.text();
|
||||
} else if (typeof res.data === 'string') {
|
||||
text = res.data;
|
||||
} else if (typeof res.data?.arrayBuffer === 'function') {
|
||||
text = new TextDecoder().decode(await res.data.arrayBuffer());
|
||||
const raw = res.data;
|
||||
if (isBlobLikeData(raw) || (typeof Blob !== 'undefined' && raw instanceof Blob)) {
|
||||
text = await raw.text();
|
||||
} else if (typeof raw === 'string') {
|
||||
text = raw;
|
||||
} else if (raw && typeof raw.arrayBuffer === 'function') {
|
||||
text = new TextDecoder().decode(await raw.arrayBuffer());
|
||||
} else if (raw instanceof ArrayBuffer) {
|
||||
text = new TextDecoder().decode(raw);
|
||||
}
|
||||
const parsed = typeof text === 'string' && text ? JSON.parse(text) : null;
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
@@ -205,10 +228,11 @@ axios.interceptors.response.use(
|
||||
// 声明是 JSON 却解析失败:保持原样,交给下游按文件处理
|
||||
}
|
||||
}
|
||||
//获取状态信息
|
||||
const status = res.data.error_code || res.data.code || res.status;
|
||||
// 获取状态信息:文件流没有 code/msg,必须退回 HTTP status,禁止读 Blob 上的幽灵字段
|
||||
const payload = isPlainObject(res.data) ? res.data : {};
|
||||
const status = Number(payload.error_code || payload.code || res.status);
|
||||
const statusWhiteList = website.statusWhiteList || [];
|
||||
const message = res.data.msg || res.data.error_description || '系统错误';
|
||||
const message = payload.msg || payload.error_description || '系统错误';
|
||||
//如果在白名单里则自行catch逻辑处理
|
||||
if (statusWhiteList.includes(status)) return Promise.reject(res);
|
||||
|
||||
@@ -260,21 +284,33 @@ axios.interceptors.response.use(
|
||||
|
||||
// OAuth2 业务错误码提示
|
||||
if (status > 2000 && !validatenull(res.data.error_description)) {
|
||||
return Promise.reject(notifiedError(message));
|
||||
return Promise.reject(notifiedError(message, config));
|
||||
}
|
||||
|
||||
// 其余非 200 响应统一提示
|
||||
if (status !== 200) {
|
||||
return Promise.reject(notifiedError(message));
|
||||
return Promise.reject(notifiedError(message, config));
|
||||
}
|
||||
return res;
|
||||
},
|
||||
error => {
|
||||
NProgress.done();
|
||||
// 网络级失败(HTTP 状态 >500、网关错误、超时、断网等 validateStatus 放行之外的响应)
|
||||
// 原为静默 reject,接口报错页面无任何感知;统一补错误提示,取消请求与刷新令牌请求除外
|
||||
if (
|
||||
!axios.isCancel(error) &&
|
||||
!isRefreshTokenRequest(error?.config) &&
|
||||
error?.config?.meta?.suppressErrorMessage !== true
|
||||
) {
|
||||
ElMessage({
|
||||
message: error?.response?.data?.msg || '请求失败,请稍后重试',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error(error));
|
||||
// 保留原始 axios 错误对象:下游(如导入失败明细解析)需要 error.response
|
||||
// 读取响应体。若包装成 new Error(error),response 会丢失、message 退化成
|
||||
// "[object Object]",用户只能看到无意义的通用提示。
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="title" width="92%" append-to-body destroy-on-close @open="handleOpen">
|
||||
<el-form :model="query" inline label-width="130px" @submit.prevent>
|
||||
<el-form-item label="发票号码">
|
||||
<el-input v-model="query.invoiceNo" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开票日期">
|
||||
<el-date-picker
|
||||
v-model="query.invoiceDateRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
format="YYYY-MM-DD"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="开票单位">
|
||||
<el-input v-model="query.salerName" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="受票单位">
|
||||
<el-input v-model="query.buyerName" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开票单位(销方)">
|
||||
<el-select
|
||||
v-model="query.salerTaxNo"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="keyword => searchCustomers(keyword, 'saler')"
|
||||
:loading="customerLoading.saler"
|
||||
placeholder="输入名称搜索或直接粘贴税号"
|
||||
style="width: 220px"
|
||||
@keyup.enter="handleQuery"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in customerOptions.saler"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="受票单位(购方)">
|
||||
<el-select
|
||||
v-model="query.buyerTaxNo"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="keyword => searchCustomers(keyword, 'buyer')"
|
||||
:loading="customerLoading.buyer"
|
||||
placeholder="输入名称搜索或直接粘贴税号"
|
||||
style="width: 220px"
|
||||
@keyup.enter="handleQuery"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in customerOptions.buyer"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="发票类型">
|
||||
<el-select
|
||||
v-model="query.invoiceType"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="不限"
|
||||
style="width: 230px"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in invoiceTypeOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="认领状态">
|
||||
<el-select v-model="query.claimed" clearable placeholder="不限" style="width: 120px">
|
||||
<el-option label="未认领" :value="false" />
|
||||
<el-option label="已认领" :value="true" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="登记状态">
|
||||
<el-select v-model="query.registered" clearable placeholder="不限" style="width: 120px">
|
||||
<el-option label="未登记" :value="false" />
|
||||
<el-option label="已登记" :value="true" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button :disabled="loading" @click="resetQuery">重置</el-button>
|
||||
<el-button type="primary" :disabled="loading" @click="handleQuery">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table
|
||||
ref="poolTable"
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
:row-key="rowKey"
|
||||
border
|
||||
height="520"
|
||||
:highlight-current-row="!multiple"
|
||||
@selection-change="selection => (selected = selection)"
|
||||
@row-dblclick="handleRowDblclick"
|
||||
>
|
||||
<el-table-column v-if="multiple" type="selection" width="52" fixed="left" align="center" reserve-selection />
|
||||
<el-table-column v-else width="52" align="center" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<el-radio
|
||||
:model-value="current && current.invoiceNo"
|
||||
:value="row.invoiceNo"
|
||||
@change="current = row"
|
||||
>
|
||||
<span></span>
|
||||
</el-radio>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="invoiceNo" label="发票号码" min-width="180" />
|
||||
<el-table-column prop="invoiceDate" label="开票日期" min-width="120" />
|
||||
<el-table-column prop="invoiceType" label="发票类型" min-width="140" />
|
||||
<el-table-column prop="issuerName" label="开票单位" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="receiverName" label="受票单位" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="金额(含税)" min-width="130" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税率" min-width="90" align="center">
|
||||
<template #default="{ row }">{{ formatRate(row.taxRate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税额" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="剩余可用金额" min-width="130" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.availableTaxAmount) }}</template>
|
||||
</el-table-column>
|
||||
<!--<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="210" show-overflow-tooltip />-->
|
||||
<el-table-column label="认领状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.claimed" type="danger" size="small">已认领</el-tag>
|
||||
<el-tag v-else type="info" size="small">未认领</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="登记状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.registered" type="warning" size="small">已登记</el-tag>
|
||||
<el-tag v-else type="info" size="small">未登记</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
type="primary"
|
||||
:underline="false"
|
||||
:disabled="attachmentBusy === row.invoiceNo"
|
||||
@click.stop="viewAttachment(row)"
|
||||
>查看</el-link
|
||||
>
|
||||
<el-link
|
||||
type="primary"
|
||||
:underline="false"
|
||||
:disabled="attachmentBusy === row.invoiceNo"
|
||||
@click.stop="downloadAttachment(row)"
|
||||
>下载</el-link
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="!multiple" label="操作" width="90" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click.stop="confirmRows([row])">选择</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="kingdee-pool-dialog__pager">
|
||||
<span class="kingdee-pool-dialog__pager-tip">按名称检索范围为已同步票,未找到请改用发票号码或税号精确查找</span>
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:disabled="loading"
|
||||
@current-change="load"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm">{{ confirmText }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/axios';
|
||||
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
|
||||
import {
|
||||
downloadKingdeeInvoiceFile,
|
||||
isKingdeePathAttachment,
|
||||
viewKingdeeInvoiceFile,
|
||||
} from '@/utils/kingdee-attachment';
|
||||
|
||||
export default {
|
||||
name: 'KingdeeInvoicePoolDialog',
|
||||
props: {
|
||||
modelValue: { type: Boolean, required: true },
|
||||
title: { type: String, default: '查询金蝶票据池' },
|
||||
confirmText: { type: String, default: '确定' },
|
||||
multiple: { type: Boolean, default: false },
|
||||
initialInvoiceNo: { type: String, default: '' },
|
||||
queryApi: { type: Function, required: true },
|
||||
},
|
||||
emits: ['update:modelValue', 'confirm'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
// 与后端 BfiConstant.INVOICE_TYPE_NAMES 对齐:value 为发票类型编码(镜像按 invoice_type_code 精确匹配)
|
||||
// 选票场景只开放数电票(普票/专票)
|
||||
invoiceTypeOptions: [
|
||||
{ value: '26', label: '数电发票(普通发票)' },
|
||||
{ value: '27', label: '数电发票(增值税专用发票)' },
|
||||
],
|
||||
rows: [],
|
||||
selected: [],
|
||||
current: null,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
// 正在查看/下载附件的发票号(行级 loading 守卫)
|
||||
attachmentBusy: null,
|
||||
// 销/购方客商下拉:远程按名称搜客商档案,选中带出税号查询;手动输入的裸税号直接作为值过滤
|
||||
customerOptions: { saler: [], buyer: [] },
|
||||
customerLoading: { saler: false, buyer: false },
|
||||
query: this.emptyQuery(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
visible: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
emptyQuery() {
|
||||
return {
|
||||
invoiceNo: '',
|
||||
// 默认查近一个月,随请求传后端并在页面回显
|
||||
invoiceDateRange: [this.$dayjs().subtract(1, 'month').format('YYYY-MM-DD'), this.$dayjs().format('YYYY-MM-DD')],
|
||||
salerName: '',
|
||||
buyerName: '',
|
||||
salerTaxNo: '',
|
||||
buyerTaxNo: '',
|
||||
invoiceType: '',
|
||||
claimed: null,
|
||||
registered: null,
|
||||
};
|
||||
},
|
||||
// 镜像行主键为 id,回源入库前的响应仍可能无 id,用发票号码兜底
|
||||
rowKey(row) {
|
||||
return row.id || row.invoiceNo;
|
||||
},
|
||||
// 票池行附件字段为复数 attachmentsJson,行内首项即发票文件({source,path,name})
|
||||
rowAttachment(row = {}) {
|
||||
const value = row.attachmentsJson;
|
||||
if (!value) return null;
|
||||
let parsed = value;
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
parsed = JSON.parse(value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return Array.isArray(parsed) ? parsed[0] || null : parsed || null;
|
||||
},
|
||||
attachmentName(row) {
|
||||
const attachment = this.rowAttachment(row);
|
||||
return attachment?.name || `${row.invoiceNo || '发票'}.pdf`;
|
||||
},
|
||||
async viewAttachment(row) {
|
||||
const attachment = this.rowAttachment(row);
|
||||
if (!isKingdeePathAttachment(attachment)) {
|
||||
this.$message.warning('当前发票暂无可查看附件');
|
||||
return;
|
||||
}
|
||||
this.attachmentBusy = row.invoiceNo;
|
||||
try {
|
||||
await viewKingdeeInvoiceFile(row.invoiceNo, this.attachmentName(row));
|
||||
} finally {
|
||||
this.attachmentBusy = null;
|
||||
}
|
||||
},
|
||||
async downloadAttachment(row) {
|
||||
const attachment = this.rowAttachment(row);
|
||||
if (!isKingdeePathAttachment(attachment)) {
|
||||
this.$message.warning('当前发票暂无可下载附件');
|
||||
return;
|
||||
}
|
||||
this.attachmentBusy = row.invoiceNo;
|
||||
try {
|
||||
await downloadKingdeeInvoiceFile(row.invoiceNo, this.attachmentName(row));
|
||||
} finally {
|
||||
this.attachmentBusy = null;
|
||||
}
|
||||
},
|
||||
handleOpen() {
|
||||
this.query = this.emptyQuery();
|
||||
this.query.invoiceNo = this.initialInvoiceNo || '';
|
||||
this.current = null;
|
||||
this.selected = [];
|
||||
this.$refs.poolTable?.clearSelection();
|
||||
this.page.current = 1;
|
||||
this.load();
|
||||
},
|
||||
handleQuery() {
|
||||
this.page.current = 1;
|
||||
this.load();
|
||||
},
|
||||
handleSizeChange() {
|
||||
this.page.current = 1;
|
||||
this.load();
|
||||
},
|
||||
resetQuery() {
|
||||
this.query = this.emptyQuery();
|
||||
this.handleQuery();
|
||||
},
|
||||
// 客商名称远程搜索:含 18 位社会信用代码特征(数字+大写字母)时按裸税号直查;
|
||||
// 否则搜客商档案,取客商发票信息(blade_customer_invoice_info)的抬头/税号作选项——票池税号是开票税号,非统一信用代码
|
||||
async searchCustomers(keyword, side) {
|
||||
const text = String(keyword || '').trim();
|
||||
if (!text) return;
|
||||
if (/^[0-9A-Z]{15,20}$/.test(text)) {
|
||||
this.customerOptions[side] = [{ label: `${text}(手输税号)`, value: text }];
|
||||
return;
|
||||
}
|
||||
this.customerLoading[side] = true;
|
||||
try {
|
||||
const data = this.unwrap(await getCustomerList(1, 20, { fullName: text }));
|
||||
const customers = (data.records || []).filter(item => item.id);
|
||||
if (!customers.length) {
|
||||
this.customerOptions[side] = [];
|
||||
return;
|
||||
}
|
||||
// 列表接口不带发票信息,逐户取 detail(detail 组装 invoices,每户一张发票信息)
|
||||
const options = await Promise.all(
|
||||
customers.map(async customer => {
|
||||
const detail = this.unwrap(await request({ url: '/blade-transport/customer-archive/detail', method: 'get', params: { id: customer.id } }));
|
||||
return (detail?.invoices || [])
|
||||
.filter(invoice => invoice.taxNo)
|
||||
.map(invoice => ({ label: `${invoice.invoiceTitle || customer.fullName}(${invoice.taxNo})`, value: invoice.taxNo }));
|
||||
})
|
||||
);
|
||||
this.customerOptions[side] = options.flat().filter(
|
||||
(option, index, list) => list.findIndex(item => item.value === option.value) === index
|
||||
);
|
||||
} finally {
|
||||
this.customerLoading[side] = false;
|
||||
}
|
||||
},
|
||||
unwrap(response) {
|
||||
const body = response?.data || response || {};
|
||||
return body?.data || body;
|
||||
},
|
||||
async load() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const range = this.query.invoiceDateRange || [];
|
||||
const data = this.unwrap(
|
||||
await this.queryApi({
|
||||
current: this.page.current,
|
||||
size: this.page.size,
|
||||
invoiceNo: this.query.invoiceNo || undefined,
|
||||
invoiceDateStart: range[0] || undefined,
|
||||
invoiceDateEnd: range[1] || undefined,
|
||||
salerName: this.query.salerName || undefined,
|
||||
buyerName: this.query.buyerName || undefined,
|
||||
salerTaxNo: this.query.salerTaxNo || undefined,
|
||||
buyerTaxNo: this.query.buyerTaxNo || undefined,
|
||||
invoiceType: this.query.invoiceType || undefined,
|
||||
claimed: this.query.claimed ?? undefined,
|
||||
registered: this.query.registered ?? undefined,
|
||||
})
|
||||
);
|
||||
this.rows = data.records || [];
|
||||
this.page.total = Number(data.total || 0);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
handleRowDblclick(row) {
|
||||
if (!this.multiple) {
|
||||
this.confirmRows([row]);
|
||||
}
|
||||
},
|
||||
handleConfirm() {
|
||||
const picked = this.multiple ? this.selected : this.current ? [this.current] : [];
|
||||
this.confirmRows(picked);
|
||||
},
|
||||
confirmRows(rows) {
|
||||
if (!rows.length) {
|
||||
this.$message.warning(this.multiple ? '请至少选择一条发票' : '请选择一张发票');
|
||||
return;
|
||||
}
|
||||
this.$emit('confirm', rows);
|
||||
this.visible = false;
|
||||
},
|
||||
formatMoney(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number.toFixed(2) : '-';
|
||||
},
|
||||
formatRate(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? `${number}%` : '-';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.kingdee-pool-dialog__pager {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 12px;
|
||||
gap: 16px;
|
||||
}
|
||||
.kingdee-pool-dialog__pager-tip {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -84,6 +84,10 @@ export default {
|
||||
columnSettingVisible: false,
|
||||
};
|
||||
},
|
||||
deactivated() {
|
||||
// keep-alive 缓存页面时关闭列设置弹窗,避免 Teleport 到 body 的弹窗切页后残留
|
||||
this.columnSettingVisible = false;
|
||||
},
|
||||
methods: {
|
||||
toggleTableSize() {
|
||||
const index = TABLE_SIZE_OPTIONS.indexOf(this.tableSize);
|
||||
|
||||
@@ -43,9 +43,23 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
||||
<el-table-column prop="uploadTime" label="上传时间" width="180" align="center" sortable />
|
||||
<el-table-column v-if="!readonly" label="操作" width="100" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-link type="danger" @click="removeAttachment($index)">删除</el-link>
|
||||
<el-table-column
|
||||
v-if="!readonly || showViewActions"
|
||||
label="操作"
|
||||
:width="readonly ? 120 : 170"
|
||||
align="center"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row, $index }">
|
||||
<div class="vehicle-attachment-table__actions">
|
||||
<el-link type="primary" :disabled="kingdeeBusy" @click="previewAttachment(row)"
|
||||
>查看</el-link
|
||||
>
|
||||
<el-link type="primary" :disabled="kingdeeBusy" @click="downloadAttachment(row)"
|
||||
>下载</el-link
|
||||
>
|
||||
<el-link v-if="!readonly" type="danger" @click="removeAttachment($index)">删除</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -71,6 +85,7 @@
|
||||
width="90%"
|
||||
top="4vh"
|
||||
class="vehicle-attachment-table__viewer-dialog"
|
||||
@closed="handlePreviewClosed"
|
||||
>
|
||||
<pdf-preview
|
||||
v-if="documentPreviewVisible && previewFile.url && previewFile.isPdf"
|
||||
@@ -111,6 +126,11 @@ import '@open-file-viewer/core/style.css';
|
||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||
import PdfPreview from '@/components/pdf-preview/main.vue';
|
||||
import { downloadFileByUrl } from '@/utils/util';
|
||||
import {
|
||||
downloadKingdeeInvoiceFile,
|
||||
isKingdeePathAttachment,
|
||||
requestKingdeeFile,
|
||||
} from '@/utils/kingdee-attachment';
|
||||
|
||||
const attachmentFileTypes = [
|
||||
'pdf',
|
||||
@@ -156,6 +176,16 @@ export default {
|
||||
type: Number,
|
||||
default: 500,
|
||||
},
|
||||
// 金蝶侧附件(piaozone path 型)代理下载所依赖的发票号;行内自带 invoiceNo 时优先用行内值
|
||||
kingdeeInvoiceNo: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 查看态也展示「查看/下载」操作列(编辑态始终展示,含删除)
|
||||
showViewActions: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
@@ -167,6 +197,10 @@ export default {
|
||||
imagePreviewIndex: 0,
|
||||
documentPreviewVisible: false,
|
||||
previewFile: {},
|
||||
// 金蝶侧附件代理请求进行中(行级按钮共享 loading 守卫,防重复触发)
|
||||
kingdeeBusy: false,
|
||||
// 金蝶附件预览用的 objectURL,弹窗关闭时回收
|
||||
kingdeeObjectUrl: '',
|
||||
viewerPlugins,
|
||||
viewerToolbar: {
|
||||
download: true,
|
||||
@@ -201,6 +235,12 @@ export default {
|
||||
attachmentUrl(row = {}) {
|
||||
return row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || row.src || '';
|
||||
},
|
||||
kingdeeInvoiceNoOf(row = {}) {
|
||||
return row.invoiceNo || this.kingdeeInvoiceNo || '';
|
||||
},
|
||||
isKingdeeRow(row = {}) {
|
||||
return isKingdeePathAttachment(row) && Boolean(this.kingdeeInvoiceNoOf(row));
|
||||
},
|
||||
attachmentExtension(row = {}) {
|
||||
const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0];
|
||||
const index = source.lastIndexOf('.');
|
||||
@@ -248,6 +288,10 @@ export default {
|
||||
return `${(value / 1024 / 1024).toFixed(1)}MB`;
|
||||
},
|
||||
downloadAttachment(row) {
|
||||
// 金蝶侧附件(piaozone path 型)经后端代理 A801-2 下载,OSS 直链维持前端直连下载
|
||||
if (this.isKingdeeRow(row)) {
|
||||
return this.downloadKingdeeAttachment(row);
|
||||
}
|
||||
const url = this.attachmentUrl(row);
|
||||
if (!url) {
|
||||
this.$message.warning('附件地址为空,无法下载');
|
||||
@@ -255,20 +299,47 @@ export default {
|
||||
}
|
||||
downloadFileByUrl(url, this.attachmentName(row));
|
||||
},
|
||||
async downloadKingdeeAttachment(row) {
|
||||
if (this.kingdeeBusy) return;
|
||||
this.kingdeeBusy = true;
|
||||
try {
|
||||
await downloadKingdeeInvoiceFile(this.kingdeeInvoiceNoOf(row), this.attachmentName(row));
|
||||
} finally {
|
||||
this.kingdeeBusy = false;
|
||||
}
|
||||
},
|
||||
batchDownload() {
|
||||
const rows = this.selectedRows.length ? this.selectedRows : this.rows;
|
||||
const downloadableRows = rows.filter(row => this.attachmentUrl(row));
|
||||
if (!downloadableRows.length) {
|
||||
const kingdeeRows = rows.filter(row => this.isKingdeeRow(row));
|
||||
const urlRows = rows.filter(row => !this.isKingdeeRow(row) && this.attachmentUrl(row));
|
||||
if (!kingdeeRows.length && !urlRows.length) {
|
||||
this.$message.warning(
|
||||
this.selectedRows.length ? '所选附件暂无可下载地址' : '暂无可下载附件'
|
||||
);
|
||||
return;
|
||||
}
|
||||
downloadableRows.forEach((row, index) => {
|
||||
urlRows.forEach((row, index) => {
|
||||
window.setTimeout(() => this.downloadAttachment(row), index * 500);
|
||||
});
|
||||
// 金蝶侧附件走代理接口,串行触发避免并发重复回源
|
||||
this.downloadKingdeeRows(kingdeeRows);
|
||||
},
|
||||
async downloadKingdeeRows(rows) {
|
||||
if (this.kingdeeBusy) return;
|
||||
this.kingdeeBusy = true;
|
||||
try {
|
||||
for (const row of rows) {
|
||||
await downloadKingdeeInvoiceFile(this.kingdeeInvoiceNoOf(row), this.attachmentName(row));
|
||||
}
|
||||
} finally {
|
||||
this.kingdeeBusy = false;
|
||||
}
|
||||
},
|
||||
previewAttachment(row) {
|
||||
// 金蝶侧附件经 A801-2 代理取流后进弹窗预览,OSS 直链维持原逻辑
|
||||
if (this.isKingdeeRow(row)) {
|
||||
return this.previewKingdeeAttachment(row);
|
||||
}
|
||||
const url = this.attachmentUrl(row);
|
||||
if (!url) {
|
||||
this.$message.warning('附件地址为空,无法预览');
|
||||
@@ -291,6 +362,42 @@ export default {
|
||||
};
|
||||
this.documentPreviewVisible = true;
|
||||
},
|
||||
async previewKingdeeAttachment(row) {
|
||||
// OFD 版式文件浏览器无法在线渲染,直接提示下载,避免拉流后预览失败
|
||||
if (this.attachmentExtension(row) === 'ofd') {
|
||||
this.$message.warning('OFD 版式文件不支持在线预览,请使用下载获取后查看');
|
||||
return;
|
||||
}
|
||||
if (this.kingdeeBusy) return;
|
||||
this.kingdeeBusy = true;
|
||||
try {
|
||||
const blob = await requestKingdeeFile(this.kingdeeInvoiceNoOf(row));
|
||||
this.releaseKingdeeObjectUrl();
|
||||
const isPdf = this.isPdf(row) || String(blob.type || '').includes('application/pdf');
|
||||
this.kingdeeObjectUrl = URL.createObjectURL(blob);
|
||||
this.previewFile = {
|
||||
name: this.attachmentName(row),
|
||||
url: this.kingdeeObjectUrl,
|
||||
mimeType: isPdf ? 'application/pdf' : blob.type || '',
|
||||
isPdf,
|
||||
};
|
||||
this.documentPreviewVisible = true;
|
||||
} catch (error) {
|
||||
if (!error?.silent) this.$message.error(error?.message || '附件预览失败');
|
||||
} finally {
|
||||
this.kingdeeBusy = false;
|
||||
}
|
||||
},
|
||||
handlePreviewClosed() {
|
||||
this.releaseKingdeeObjectUrl();
|
||||
this.previewFile = {};
|
||||
},
|
||||
releaseKingdeeObjectUrl() {
|
||||
if (this.kingdeeObjectUrl) {
|
||||
URL.revokeObjectURL(this.kingdeeObjectUrl);
|
||||
this.kingdeeObjectUrl = '';
|
||||
}
|
||||
},
|
||||
handlePreviewUnsupported() {
|
||||
this.$message.warning('当前文件暂不支持在线预览');
|
||||
},
|
||||
@@ -311,6 +418,27 @@ export default {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__file-name {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__file-name:hover,
|
||||
&__file-name:focus-visible {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
:deep(.el-link) {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
&__upload {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
||||
@@ -12,6 +12,8 @@ import { h } from 'vue';
|
||||
* 1. 混入本 mixin:mixins: [organizationSearch]
|
||||
* 2. option 中「所属组织」列加上 searchslot: true(prop 为 organizationName 或 deptName 均可)
|
||||
* 3. 在 created / 初始化时调用 this.loadOrganizationOptions()
|
||||
* 4. 需要「未分配组织」筛选项的页面在 data 里置 enableUnassignedOrgFilter: true;
|
||||
* 该项只追加进搜索树 organizationSearchTreeOptions,表单级联 organizationTreeOptions 不含哨兵
|
||||
*
|
||||
* 说明:
|
||||
* - 自动识别 organizationName / deptName 两种 prop,并挂载 column.renderSearch
|
||||
@@ -26,6 +28,12 @@ import { h } from 'vue';
|
||||
*/
|
||||
const ORG_PROPS = ['organizationName', 'deptName'];
|
||||
|
||||
// 「未分配组织」筛选项的哨兵值:不是真实组织名,选中后提交给后端的就是这串字面量。
|
||||
// 后端 TransportBusiConstant.UNASSIGNED_ORG_FILTER、DriverMapper.xml / TransportVehicleMapper.xml
|
||||
// 的 organizationName 分支以及司机、车辆导出查询都按它改判为 organization_name = '';
|
||||
// 后端换成空串会让整段组织条件不生效,筛选静默失效。前端与后端这几处必须同步修改。
|
||||
const UNASSIGNED_ORG_FILTER = '__UNASSIGNED_ORG__';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -34,9 +42,18 @@ export default {
|
||||
organizationTreeOptions: [],
|
||||
organizationTreeFlatOptions: [],
|
||||
organizationLoading: false,
|
||||
// 组织筛选是否追加「未分配组织」一项;页面在 data 里覆盖为 true 才生效
|
||||
enableUnassignedOrgFilter: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 搜索栏级联的选项:开关打开时在真实组织最前面追加「未分配组织」。
|
||||
// 不能直接改 organizationTreeOptions——司机/车辆页的编辑表单也用这棵树选组织。
|
||||
organizationSearchTreeOptions() {
|
||||
return this.enableUnassignedOrgFilter
|
||||
? [this.unassignedOrgOption(), ...this.organizationTreeOptions]
|
||||
: this.organizationTreeOptions;
|
||||
},
|
||||
// 所属组织级联配置:checkStrictly 允许选中任意层级,不必选到末级
|
||||
organizationCascaderProps() {
|
||||
return {
|
||||
@@ -61,7 +78,7 @@ export default {
|
||||
? this.excludeExternalOrganization(source)
|
||||
: source;
|
||||
this.organizationTreeOptions = this.normalizeOrgTree(tree);
|
||||
this.organizationTreeFlatOptions = this.flattenOrgTree(this.organizationTreeOptions);
|
||||
this.organizationTreeFlatOptions = this.flattenOrgTree(this.organizationSearchTreeOptions);
|
||||
const option = this.tableOption || this.option;
|
||||
if (!option) return;
|
||||
ORG_PROPS.forEach(prop => {
|
||||
@@ -100,12 +117,20 @@ export default {
|
||||
};
|
||||
});
|
||||
},
|
||||
// 哨兵节点的 id 与 rawLabel 都取哨兵值,resolveOrgName 取到的仍是哨兵而不是某个真实组织名
|
||||
unassignedOrgOption() {
|
||||
return { id: UNASSIGNED_ORG_FILTER, label: '未分配组织', rawLabel: UNASSIGNED_ORG_FILTER };
|
||||
},
|
||||
flattenOrgTree(tree = []) {
|
||||
return (tree || []).flatMap(item => [
|
||||
{ id: String(item.id), label: item.label, rawLabel: item.rawLabel },
|
||||
...this.flattenOrgTree(item.children || []),
|
||||
]);
|
||||
},
|
||||
// 组织列渲染用:空串或纯空白即「未分配组织」(组织列 NOT NULL,无组织存空串)
|
||||
isBlankOrganizationName(value) {
|
||||
return String(value ?? '').trim() === '';
|
||||
},
|
||||
// 兼容两种入参:级联路径数组(取末级)或单个部门 id
|
||||
findOrgById(id) {
|
||||
const value = Array.isArray(id) ? id.at(-1) : id;
|
||||
@@ -155,7 +180,7 @@ export default {
|
||||
// 级联单选:modelValue 必须是 id 路径数组,否则控件内部状态会错乱导致无法再次选中
|
||||
if (scope.row) scope.row[prop] = Array.isArray(value) ? value : [];
|
||||
},
|
||||
options: this.organizationTreeOptions,
|
||||
options: this.organizationSearchTreeOptions,
|
||||
props: {
|
||||
label: 'label',
|
||||
value: 'id',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const transportTypeOptions = [
|
||||
export const transportTypeOptions = [
|
||||
{ label: '公路运输', value: '公路运输' },
|
||||
{ label: '铁路运输', value: '铁路运输' },
|
||||
{ label: '水路运输', value: '水路运输' },
|
||||
@@ -42,6 +42,7 @@ export const waybillStatusOptions = [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
{ label: '待执行', value: 'pending' },
|
||||
{ label: '进行中', value: 'running' },
|
||||
{ label: '待回单确认', value: 'pending_return_confirm' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
{ label: '已取消', value: 'cancelled' },
|
||||
];
|
||||
@@ -143,6 +144,17 @@ export const contractArchiveStatusOptions = [
|
||||
{ label: '已归档', value: '已归档' },
|
||||
];
|
||||
|
||||
export const hcsSyncStatusOptions = [
|
||||
{ label: '待推送', value: 'unsynced' },
|
||||
{ label: '已推送', value: 'synced' },
|
||||
{ label: '推送失败', value: 'failed' },
|
||||
];
|
||||
|
||||
export const yesNoOptions = [
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
];
|
||||
|
||||
export const nodeOptions = [
|
||||
{ label: '接单', value: '接单' },
|
||||
{ label: '到场', value: '到场' },
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
createCrudOption,
|
||||
effectiveTypeOptions,
|
||||
nonNegativeRule,
|
||||
hcsSyncStatusOptions,
|
||||
selectRule,
|
||||
signTypeOptions,
|
||||
textRule,
|
||||
yesNoOptions,
|
||||
} from './common';
|
||||
|
||||
const contractSettlementModeOptions = [
|
||||
@@ -252,8 +254,8 @@ export const option = createCrudOption([
|
||||
searchOrder: 6,
|
||||
searchPlaceholder: '请输入',
|
||||
order: 185,
|
||||
disabled: true,
|
||||
placeholder: '不填写,则系统自动生成',
|
||||
maxlength: 100,
|
||||
placeholder: '不填写则按部门规则自动生成',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
@@ -606,6 +608,50 @@ export const option = createCrudOption([
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '合规状态',
|
||||
prop: 'hcsSyncStatus',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchOrder: 6,
|
||||
searchPlaceholder: '请选择',
|
||||
slot: true,
|
||||
dicData: hcsSyncStatusOptions,
|
||||
minWidth: 110,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '金蝶合同ID',
|
||||
prop: 'kingdeeId',
|
||||
minWidth: 130,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '是否同步金蝶',
|
||||
prop: 'kingdeeSynced',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 7,
|
||||
dicData: yesNoOptions,
|
||||
hide: true,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '是否同步合规',
|
||||
prop: 'hcsSynced',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 8,
|
||||
dicData: yesNoOptions,
|
||||
hide: true,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '当前节点',
|
||||
prop: 'currentNode',
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
businessModeOptions,
|
||||
businessTypeOptions,
|
||||
createCrudOption,
|
||||
hcsSyncStatusOptions,
|
||||
nonNegativeRule,
|
||||
projectApprovalStatusOptions,
|
||||
projectSourceOptions,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
selectRule,
|
||||
settlementModeOptions,
|
||||
textRule,
|
||||
yesNoOptions,
|
||||
} from './common';
|
||||
|
||||
const projectEffectiveTypeOptions = [
|
||||
@@ -97,7 +99,8 @@ export const config = {
|
||||
action: 'startChange',
|
||||
label: '变更',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['approved'],
|
||||
// 立项通过、变更通过后均可再次发起变更
|
||||
status: ['approved', 'change_approved'],
|
||||
permission: 'project_apply_change',
|
||||
},
|
||||
],
|
||||
@@ -185,7 +188,7 @@ export const option = createCrudOption([
|
||||
searchPlaceholder: '请输入',
|
||||
searchOrder: 8,
|
||||
minWidth: 150,
|
||||
rules: textRule('平台公司', 50, true),
|
||||
rules: textRule('平台公司', 500, true),
|
||||
},
|
||||
{
|
||||
label: '平台公司ID',
|
||||
@@ -405,6 +408,57 @@ export const option = createCrudOption([
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '合规状态',
|
||||
prop: 'hcsSyncStatus',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 1,
|
||||
slot: true,
|
||||
dicData: hcsSyncStatusOptions,
|
||||
minWidth: 110,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '金蝶项目ID',
|
||||
prop: 'kingdeeId',
|
||||
minWidth: 130,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '金蝶项目编号',
|
||||
prop: 'kingdeeNumber',
|
||||
minWidth: 130,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '是否同步金蝶',
|
||||
prop: 'kingdeeSynced',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 2,
|
||||
dicData: yesNoOptions,
|
||||
hide: true,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '是否同步合规',
|
||||
prop: 'hcsSynced',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 3,
|
||||
dicData: yesNoOptions,
|
||||
hide: true,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '生效类型',
|
||||
prop: 'effectiveType',
|
||||
|
||||
@@ -471,7 +471,7 @@ export const config = {
|
||||
endDateRange: ['endDateStart', 'endDateEnd'],
|
||||
createTimeRange: ['createTimeStart', 'createTimeEnd'],
|
||||
},
|
||||
actions: ['copy', 'cancel', 'reassign', 'complete', 'batchComplete'],
|
||||
actions: ['copy', 'cancel', 'reassign', 'complete', 'batchComplete', 'confirmReturn'],
|
||||
detailButton: true,
|
||||
statusProp: 'businessStatus',
|
||||
statusTextProp: 'businessStatusName',
|
||||
@@ -483,6 +483,8 @@ export const config = {
|
||||
row.requireAccept === true ||
|
||||
row.requireAccept === 1 ||
|
||||
requiresDriverAcceptConfirmation(row);
|
||||
// 待回单确认由末位打卡产生,司机必然已接单
|
||||
if (status === 'pending_return_confirm') return '待回单确认';
|
||||
// 需司机接单且尚未接单 → 待执行(含库中仍为 running 的历史数据展示校正)
|
||||
if (needAccept && !hasDriverAcceptRecord(row) && !terminalStatuses.includes(status)) {
|
||||
return '待执行';
|
||||
@@ -504,7 +506,7 @@ export const config = {
|
||||
return !isDriverRejectedWaybill(row);
|
||||
},
|
||||
canCancel(row) {
|
||||
// 进行中的运单不允许取消
|
||||
// 进行中的运单不允许取消;待回单确认仍可取消(与页面 canCancel 一致)
|
||||
return !isInProgressStatus(row, row.businessStatusName);
|
||||
},
|
||||
deleteStatus: ['draft'],
|
||||
|
||||
@@ -13,6 +13,8 @@ export const invoiceApplicationTableColumns = [
|
||||
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
|
||||
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
|
||||
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
|
||||
{ prop: 'kingdeeLockStatus', label: '金蝶锁状态', minWidth: 150 },
|
||||
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
|
||||
];
|
||||
|
||||
export const invoiceApplicationDetailColumns = [
|
||||
|
||||
@@ -11,6 +11,8 @@ export const invoiceReceiptTableColumns = [
|
||||
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
|
||||
{ prop: 'createUserName', label: '创建人', minWidth: 110 },
|
||||
{ prop: 'createTime', label: '创建时间', minWidth: 170 },
|
||||
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
|
||||
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
|
||||
{ prop: 'kingdeeReceivedInvoiceNo', label: '金蝶单据号', minWidth: 150 },
|
||||
{ prop: 'kingdeePushStatusName', label: '金蝶状态', minWidth: 130 },
|
||||
{ prop: 'kingdeePushMsg', label: '失败原因', minWidth: 160 },
|
||||
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
|
||||
];
|
||||
|
||||
@@ -1,20 +1,176 @@
|
||||
export const paymentApplicationTableColumns = [
|
||||
{ prop: 'paymentNo', label: '单据号', minWidth: 140, link: true },
|
||||
{ prop: 'paymentTypeName', label: '付款类型', minWidth: 110 },
|
||||
{ prop: 'projectName', label: '所属项目', minWidth: 140 },
|
||||
{ prop: 'deptName', label: '所属组织', minWidth: 120 },
|
||||
{ prop: 'contractNo', label: '合同编号', minWidth: 130 },
|
||||
{ prop: 'contractName', label: '合同名称', minWidth: 140 },
|
||||
{ prop: 'payerName', label: '付款方', minWidth: 130 },
|
||||
{ prop: 'payeeName', label: '收款方', minWidth: 130 },
|
||||
{ prop: 'appliedAmount', label: '申请付款金额', minWidth: 140, money: true },
|
||||
{ prop: 'paidAmount', label: '已付款金额', minWidth: 130, money: true },
|
||||
{ prop: 'matchedInvoiceAmount', label: '匹配发票金额', minWidth: 130, money: true },
|
||||
{ prop: 'applicantName', label: '申请人', minWidth: 100 },
|
||||
{ prop: 'applyDate', label: '申请日期', minWidth: 120 },
|
||||
{ prop: 'approvalStatusName', label: '审核状态', minWidth: 110, status: true },
|
||||
{ prop: 'currentNode', label: '当前节点', minWidth: 120 },
|
||||
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 120 },
|
||||
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 130 },
|
||||
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 130 },
|
||||
];
|
||||
/**
|
||||
* 付款申请列表(avue-crud)配置
|
||||
* ------------------------------------------------------------------
|
||||
* 列表页与其它模块统一使用 avue-crud,头部 4 个按钮(刷新 / 列显隐 / 搜索显隐 /
|
||||
* 表格卡片切换)由 avue-crud 自带,不做二次封装。
|
||||
*
|
||||
* 搜索项与表格列 label 不一致的(如表格列「收款方」/ 搜索项「客户名称」),
|
||||
* 单独定义一个 hide 的搜索列,并用 searchProp 映射到后端字段。
|
||||
*
|
||||
* ⚠️ avue 的 searchOrder 为降序排序,值越大越靠前(见 header-search2.js:
|
||||
* `list.sort((a, b) => (b.searchOrder || 0) - (a.searchOrder || 0))`)。
|
||||
*/
|
||||
|
||||
const textFormatter = (row, value) =>
|
||||
value === null || value === undefined || value === '' ? '-' : value;
|
||||
|
||||
const moneyFormatter = (row, value) => Number(value || 0).toFixed(2);
|
||||
|
||||
// 仅用于搜索、不在表格中出现的列
|
||||
const searchOnlyColumn = ({ label, prop, searchOrder, searchProp, dicData }) => {
|
||||
const column = {
|
||||
label,
|
||||
prop,
|
||||
search: true,
|
||||
searchOrder,
|
||||
searchPlaceholder: '请输入',
|
||||
hide: true,
|
||||
showColumn: false,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
};
|
||||
if (searchProp) column.searchProp = searchProp;
|
||||
if (dicData) {
|
||||
column.type = 'select';
|
||||
column.searchPlaceholder = '请选择';
|
||||
column.dicData = dicData;
|
||||
}
|
||||
return column;
|
||||
};
|
||||
|
||||
export const paymentApplicationOption = ({
|
||||
paymentTypeOptions = [],
|
||||
approvalStatusOptions = [],
|
||||
kingdeeStatusOptions = [],
|
||||
} = {}) => ({
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
tip: false,
|
||||
searchBtnText: '查询',
|
||||
emptyBtnText: '重置',
|
||||
searchShow: true,
|
||||
searchIcon: true,
|
||||
searchLabelWidth: 88,
|
||||
// 查询/重置紧跟首行 4 个条件(4×5 + 菜单 4 = 24),不再独占下一整行
|
||||
searchMenuSpan: 4,
|
||||
searchIndex: 4,
|
||||
searchMenuPosition: 'right',
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 70,
|
||||
indexFixed: 'left',
|
||||
selection: true,
|
||||
selectionWidth: 52,
|
||||
selectionFixed: 'left',
|
||||
addBtn: false,
|
||||
viewBtn: false,
|
||||
delBtn: false,
|
||||
editBtn: false,
|
||||
copyBtn: false,
|
||||
menuWidth: 320,
|
||||
menuFixed: 'right',
|
||||
dialogClickModal: false,
|
||||
column: [
|
||||
// ---------- 搜索项(前 4 项默认展示,其余折叠;searchOrder 越大越靠前) ----------
|
||||
{
|
||||
label: '单据号',
|
||||
prop: 'paymentNo',
|
||||
slot: true,
|
||||
search: true,
|
||||
searchSpan: 5,
|
||||
searchOrder: 9,
|
||||
searchPlaceholder: '请输入',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
label: '日期',
|
||||
prop: 'applyDateRange',
|
||||
type: 'daterange',
|
||||
searchType: 'daterange',
|
||||
search: true,
|
||||
searchSpan: 5,
|
||||
searchOrder: 8,
|
||||
hide: true,
|
||||
showColumn: false,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
...searchOnlyColumn({
|
||||
label: '客户名称',
|
||||
prop: 'payeeNameSearch',
|
||||
searchOrder: 7,
|
||||
searchProp: 'payeeName',
|
||||
}),
|
||||
searchSpan: 5,
|
||||
},
|
||||
{
|
||||
...searchOnlyColumn({
|
||||
label: '项目名称',
|
||||
prop: 'projectNameSearch',
|
||||
searchOrder: 6,
|
||||
searchProp: 'projectName',
|
||||
}),
|
||||
searchSpan: 5,
|
||||
},
|
||||
searchOnlyColumn({
|
||||
label: '关联结算单',
|
||||
prop: 'settlementNo',
|
||||
searchOrder: 4,
|
||||
}),
|
||||
searchOnlyColumn({
|
||||
label: '付款类型',
|
||||
prop: 'paymentType',
|
||||
searchOrder: 3,
|
||||
dicData: paymentTypeOptions,
|
||||
}),
|
||||
searchOnlyColumn({
|
||||
label: '审核状态',
|
||||
prop: 'approvalStatus',
|
||||
searchOrder: 2,
|
||||
dicData: approvalStatusOptions,
|
||||
}),
|
||||
searchOnlyColumn({
|
||||
label: '金蝶单据状态',
|
||||
prop: 'kingdeeStatus',
|
||||
searchOrder: 1,
|
||||
dicData: kingdeeStatusOptions,
|
||||
}),
|
||||
// ---------- 表格列 ----------
|
||||
{ label: '付款类型', prop: 'paymentTypeName', minWidth: 110, formatter: textFormatter },
|
||||
{ label: '所属项目', prop: 'projectName', minWidth: 140, formatter: textFormatter },
|
||||
{
|
||||
label: '所属组织',
|
||||
prop: 'deptName',
|
||||
search: true,
|
||||
searchOrder: 5,
|
||||
searchslot: true,
|
||||
minWidth: 120,
|
||||
formatter: textFormatter,
|
||||
},
|
||||
{ label: '合同编号', prop: 'contractNo', minWidth: 130, formatter: textFormatter },
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 140, formatter: textFormatter },
|
||||
{ label: '付款方', prop: 'payerName', minWidth: 130, formatter: textFormatter },
|
||||
{ label: '收款方', prop: 'payeeName', minWidth: 130, formatter: textFormatter },
|
||||
{ label: '申请付款金额', prop: 'appliedAmount', minWidth: 140, formatter: moneyFormatter },
|
||||
{ label: '已付款金额', prop: 'paidAmount', minWidth: 130, formatter: moneyFormatter },
|
||||
{
|
||||
label: '匹配发票金额',
|
||||
prop: 'matchedInvoiceAmount',
|
||||
minWidth: 130,
|
||||
formatter: moneyFormatter,
|
||||
},
|
||||
{ label: '申请人', prop: 'applicantName', minWidth: 100, formatter: textFormatter },
|
||||
{ label: '申请日期', prop: 'applyDate', minWidth: 120, formatter: textFormatter },
|
||||
{ label: '审核状态', prop: 'approvalStatusName', slot: true, minWidth: 110 },
|
||||
{ label: '当前节点', prop: 'currentNode', minWidth: 120, formatter: textFormatter },
|
||||
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 120, formatter: textFormatter },
|
||||
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 130, formatter: textFormatter },
|
||||
{ label: '金蝶单据状态', prop: 'kingdeeStatusName', minWidth: 130, formatter: textFormatter },
|
||||
{ label: '金蝶锁状态', prop: 'kingdeeLockStatus', slot: true, minWidth: 150 },
|
||||
{ label: '附件同步', prop: 'attachmentSyncStatusName', minWidth: 100, formatter: textFormatter },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -8,5 +8,11 @@ export const receiptFlowTableColumns = [
|
||||
{ prop: 'summary', label: '摘要', minWidth: 160 },
|
||||
{ prop: 'transactionTime', label: '交易时间', minWidth: 170 },
|
||||
{ prop: 'claimStatusName', label: '认领状态', minWidth: 110, status: true },
|
||||
{ prop: 'kingdeeClaimStatusName', label: '公告状态', minWidth: 110 },
|
||||
{ prop: 'claimedAmount', label: '已认领金额', minWidth: 130, money: true },
|
||||
{ prop: 'unclaimedAmount', label: '金蝶未认领金额', minWidth: 140, money: true },
|
||||
{ prop: 'pushStatusName', label: '推送状态', minWidth: 110, status: true },
|
||||
{ prop: 'pushFailReason', label: '推送失败原因', minWidth: 160 },
|
||||
{ prop: 'kingdeeBillNo', label: '金蝶收款单号', minWidth: 150 },
|
||||
{ prop: 'kingdeeBillStatusName', label: '金蝶状态', minWidth: 110 },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* 正式结算单列表(avue-crud)配置
|
||||
* ------------------------------------------------------------------
|
||||
* 列表页与其它模块统一使用 avue-crud,头部 4 个按钮(刷新 / 列显隐 / 搜索显隐 /
|
||||
* 显示方式)由 avue-crud 自带,不做二次封装。
|
||||
*
|
||||
* 表格列复用 formalSettlementTable.js 的 formalSettlementTableColumns(与弹窗内
|
||||
* 明细表共用同一份列定义),此处只补充 avue 需要的展示/搜索属性,避免两处维护。
|
||||
*
|
||||
* ⚠️ searchOrder 为降序排序,值越大越靠前。
|
||||
* ⚠️ 仅用于搜索的列必须 hide + showColumn: false,否则会混进「列显隐」抽屉。
|
||||
* ⚠️ column 数组顺序 = 表格列顺序,搜索专用列统一放在数组最前面。
|
||||
*/
|
||||
|
||||
import {
|
||||
approvalStatusOptions,
|
||||
invoiceStatusOptions,
|
||||
kingdeeSyncStatusOptions,
|
||||
paymentStatusOptions,
|
||||
} from './formalSettlementSearch';
|
||||
import { formalSettlementTableColumns } from './formalSettlementTable';
|
||||
|
||||
const textFormatter = (row, value) =>
|
||||
value === null || value === undefined || value === '' ? '-' : value;
|
||||
|
||||
/** 金额统一展示:两位小数 + 币种 */
|
||||
const moneyFormatter = (row, value) =>
|
||||
`${Number(value || 0).toFixed(2)} ${row?.currency || 'RMB'}`;
|
||||
|
||||
const pickLabel = (options, value) =>
|
||||
options.find(item => item.value === value)?.label || value || '-';
|
||||
|
||||
const searchOnlyColumn = ({ label, prop, searchOrder, type = 'input', dicData }) => {
|
||||
const column = {
|
||||
label,
|
||||
prop,
|
||||
search: true,
|
||||
searchOrder,
|
||||
searchSpan: 5,
|
||||
hide: true,
|
||||
showColumn: false,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
};
|
||||
if (type === 'select') {
|
||||
column.type = 'select';
|
||||
column.searchPlaceholder = '请选择';
|
||||
column.dicData = dicData || [];
|
||||
} else if (type === 'daterange') {
|
||||
column.type = 'daterange';
|
||||
column.searchType = 'daterange';
|
||||
column.searchPlaceholder = '请选择';
|
||||
} else {
|
||||
column.searchPlaceholder = '请输入';
|
||||
}
|
||||
return column;
|
||||
};
|
||||
|
||||
/** 表格列的 avue 增量配置:搜索项、插槽、覆盖宽度等 */
|
||||
const COLUMN_EXTRA = {
|
||||
formalSettlementNo: {
|
||||
slot: true,
|
||||
search: true,
|
||||
searchSpan: 5,
|
||||
searchOrder: 12,
|
||||
searchPlaceholder: '请输入',
|
||||
},
|
||||
payerName: { search: true, searchSpan: 5, searchOrder: 7, searchPlaceholder: '请输入' },
|
||||
payeeName: { search: true, searchSpan: 5, searchOrder: 6, searchPlaceholder: '请输入' },
|
||||
projectName: { search: true, searchSpan: 5, searchOrder: 11, searchPlaceholder: '请输入' },
|
||||
deptName: { search: true, searchslot: true, searchSpan: 5, searchOrder: 10 },
|
||||
contractNo: { search: true, searchSpan: 5, searchOrder: 9, searchPlaceholder: '请输入' },
|
||||
invoiceStatusName: { slot: true },
|
||||
paymentStatusName: { slot: true },
|
||||
approvalStatusName: { slot: true },
|
||||
adjustFlag: {
|
||||
formatter: row => row.adjustStatusName || (row.originFormalSettlementNo ? '调整后单' : '-'),
|
||||
},
|
||||
kingdeeSyncStatus: {
|
||||
slot: true,
|
||||
search: true,
|
||||
type: 'select',
|
||||
dicData: kingdeeSyncStatusOptions,
|
||||
searchSpan: 5,
|
||||
searchOrder: 2,
|
||||
searchPlaceholder: '请选择',
|
||||
},
|
||||
kingdeeLockStatus: { slot: true },
|
||||
createTime: { width: 180 },
|
||||
};
|
||||
|
||||
const buildTableColumns = () =>
|
||||
formalSettlementTableColumns.map(column => {
|
||||
const extra = { ...(COLUMN_EXTRA[column.prop] || {}) };
|
||||
if (column.money) {
|
||||
extra.formatter = moneyFormatter;
|
||||
extra.align = 'right';
|
||||
}
|
||||
// link / status 标记的列统一走单元格插槽
|
||||
if (column.link || column.status) extra.slot = true;
|
||||
if (!extra.formatter) extra.formatter = textFormatter;
|
||||
return { ...column, ...extra };
|
||||
});
|
||||
|
||||
export const formalSettlementOption = () => ({
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
tip: false,
|
||||
searchBtnText: '查询',
|
||||
emptyBtnText: '重置',
|
||||
searchShow: true,
|
||||
searchIcon: true,
|
||||
searchLabelWidth: 88,
|
||||
// 查询/重置紧跟首行 4 个条件(4×5 + 菜单 4 = 24),不再独占下一整行
|
||||
searchMenuSpan: 4,
|
||||
searchIndex: 4,
|
||||
searchMenuPosition: 'right',
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 64,
|
||||
indexFixed: 'left',
|
||||
selection: true,
|
||||
selectionWidth: 52,
|
||||
selectionFixed: 'left',
|
||||
addBtn: false,
|
||||
viewBtn: false,
|
||||
delBtn: false,
|
||||
editBtn: false,
|
||||
menuWidth: 320,
|
||||
menuFixed: 'right',
|
||||
column: [
|
||||
// ---------- 仅搜索列 ----------
|
||||
searchOnlyColumn({
|
||||
label: '预付结算单号',
|
||||
prop: 'preSettlementNo',
|
||||
searchOrder: 8,
|
||||
}),
|
||||
searchOnlyColumn({
|
||||
label: '发票状态',
|
||||
prop: 'invoiceStatus',
|
||||
searchOrder: 5,
|
||||
type: 'select',
|
||||
dicData: invoiceStatusOptions,
|
||||
}),
|
||||
searchOnlyColumn({
|
||||
label: '收付款状态',
|
||||
prop: 'paymentStatus',
|
||||
searchOrder: 4,
|
||||
type: 'select',
|
||||
dicData: paymentStatusOptions,
|
||||
}),
|
||||
searchOnlyColumn({
|
||||
label: '审核状态',
|
||||
prop: 'approvalStatus',
|
||||
searchOrder: 3,
|
||||
type: 'select',
|
||||
dicData: approvalStatusOptions,
|
||||
}),
|
||||
searchOnlyColumn({
|
||||
label: '生成日期',
|
||||
prop: 'createDateRange',
|
||||
searchOrder: 1,
|
||||
type: 'daterange',
|
||||
}),
|
||||
// ---------- 表格列(部分兼搜索项) ----------
|
||||
...buildTableColumns(),
|
||||
],
|
||||
});
|
||||
|
||||
export const kingdeeSyncName = value => pickLabel(kingdeeSyncStatusOptions, value);
|
||||
export const paymentStatusName = value => pickLabel(paymentStatusOptions, value);
|
||||
@@ -8,6 +8,8 @@ export const formalSettlementFormFields = [
|
||||
{ label: '收款方', prop: 'payeeName', readonly: true },
|
||||
{ label: '结算类型', prop: 'settlementTypeName', readonly: true },
|
||||
{ label: '结算金额', prop: 'settlementAmount', readonly: true, money: true },
|
||||
{ label: '业务模式', prop: 'businessModel', type: 'select', optionsKey: 'businessModelOptions' },
|
||||
{ label: '运输方式', prop: 'transportType', type: 'select', optionsKey: 'transportTypeOptions' },
|
||||
{ label: '汇率日期', prop: 'exchangeRateDate', type: 'date', required: true },
|
||||
{ label: '结算汇率', prop: 'exchangeRate', type: 'number', required: true },
|
||||
{ label: '本位币合计', prop: 'localSettlementAmount', readonly: true, money: true },
|
||||
@@ -25,6 +27,8 @@ export const createFormalSettlementForm = () => ({
|
||||
settlementType: '',
|
||||
sourcePreSettlementIds: [],
|
||||
sourceDetailIds: [],
|
||||
businessModel: '',
|
||||
transportType: '',
|
||||
exchangeRateDate: '',
|
||||
exchangeRate: 1,
|
||||
createUserName: '',
|
||||
|
||||
@@ -36,7 +36,7 @@ export const kingdeeSyncStatusOptions = [
|
||||
|
||||
export const formalSettlementSearchFields = [
|
||||
{ label: '结算单号', prop: 'formalSettlementNo', type: 'input' },
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', type: 'input' },
|
||||
{ label: '预付结算单号', prop: 'preSettlementNo', type: 'input' },
|
||||
{ label: '项目', prop: 'projectName', type: 'input' },
|
||||
{ label: '所属组织', prop: 'deptName', type: 'cascader' },
|
||||
{ label: '合同编号', prop: 'contractNo', type: 'input' },
|
||||
|
||||
@@ -8,6 +8,8 @@ export const formalSettlementTableColumns = [
|
||||
{ label: '合同编号', prop: 'contractNo', minWidth: 150 },
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 170 },
|
||||
{ label: '原币结算金额', prop: 'settlementAmount', minWidth: 145, money: true },
|
||||
{ label: '业务模式', prop: 'businessModelName', minWidth: 120 },
|
||||
{ label: '运输方式', prop: 'transportTypeName', minWidth: 120 },
|
||||
{ label: '本位币结算金额', prop: 'localSettlementAmount', minWidth: 155, money: true },
|
||||
{ label: '结算汇率', prop: 'exchangeRate', minWidth: 110 },
|
||||
{ label: '申请付款金额(含预付)', prop: 'appliedPaymentAmount', minWidth: 185, money: true },
|
||||
@@ -19,22 +21,26 @@ export const formalSettlementTableColumns = [
|
||||
{ label: '审核状态', prop: 'approvalStatusName', minWidth: 110, status: true },
|
||||
{ label: '当前节点', prop: 'currentNode', minWidth: 130 },
|
||||
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
|
||||
{ label: '调整标识', prop: 'adjustFlag', minWidth: 110 },
|
||||
{ label: '来源原单号', prop: 'originFormalSettlementNo', minWidth: 170 },
|
||||
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 },
|
||||
{ label: '金蝶单据状态', prop: 'kingdeeSyncStatus', minWidth: 135 },
|
||||
{ label: '金蝶锁状态', prop: 'kingdeeLockStatus', minWidth: 150 },
|
||||
{ label: '附件同步', prop: 'attachmentSyncStatusName', minWidth: 100 },
|
||||
{ label: '创建人', prop: 'createUserName', minWidth: 110 },
|
||||
{ label: '创建时间', prop: 'createTime', minWidth: 170 },
|
||||
];
|
||||
|
||||
export const sourceColumns = [
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', minWidth: 160 },
|
||||
{ label: '预付结算单号', prop: 'preSettlementNo', minWidth: 160 },
|
||||
{ label: '结算金额', prop: 'settlementAmount', minWidth: 130, money: true },
|
||||
{ label: '申请预付金额', prop: 'advanceAppliedAmount', minWidth: 140, money: true },
|
||||
{ label: '已付款金额', prop: 'advancePaidAmount', minWidth: 130, money: true },
|
||||
];
|
||||
|
||||
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 },
|
||||
@@ -46,11 +52,11 @@ export const detailColumns = [
|
||||
{ label: '运输总量', prop: 'transportQuantity', minWidth: 120 },
|
||||
{ label: '里程(KM)', prop: 'mileage', minWidth: 120 },
|
||||
{ label: '批次号', prop: 'batchNo', minWidth: 120 },
|
||||
{ label: '计费方式', prop: 'billingType', minWidth: 130 },
|
||||
{ label: '运输单价', prop: 'unitPrice', minWidth: 120, money: true },
|
||||
{ label: '运费', prop: 'freightAmount', minWidth: 120, money: true },
|
||||
{ label: '调整金额', prop: 'adjustAmount', minWidth: 120, money: true },
|
||||
{ label: '结算金额(含税)', prop: 'settlementAmountTax', minWidth: 155, money: true },
|
||||
{ label: '结算金额(不含税)', prop: 'settlementAmountNoTax', minWidth: 170, money: true },
|
||||
{ label: '备注', prop: 'remark', minWidth: 180 },
|
||||
];
|
||||
|
||||
@@ -64,7 +70,7 @@ export const summaryColumns = [
|
||||
];
|
||||
|
||||
export const candidateColumns = [
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', minWidth: 170 },
|
||||
{ label: '预付结算单号', prop: 'preSettlementNo', minWidth: 170 },
|
||||
{ label: '项目名称', prop: 'projectName', minWidth: 140 },
|
||||
{ label: '所属组织', prop: 'deptName', minWidth: 140 },
|
||||
{ label: '合同编号', prop: 'contractNo', minWidth: 150 },
|
||||
@@ -84,7 +90,7 @@ export const candidateDetailColumns = [
|
||||
{ label: '合同编号', prop: 'contractNo', minWidth: 150 },
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 170 },
|
||||
{ label: '来源', prop: 'sourceType', minWidth: 110 },
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', minWidth: 160 },
|
||||
{ label: '预付结算单号', prop: 'preSettlementNo', minWidth: 160 },
|
||||
{ label: '正式结算单号', prop: 'formalSettlementNo', minWidth: 160 },
|
||||
{ label: '运单号', prop: 'waybillNo', minWidth: 150 },
|
||||
{ label: '车号', prop: 'vehicleNo', minWidth: 130 },
|
||||
@@ -92,6 +98,7 @@ export const candidateDetailColumns = [
|
||||
{ label: '货物名称', prop: 'cargoName', minWidth: 140 },
|
||||
{ label: '货物类型', prop: 'cargoType', minWidth: 130 },
|
||||
{ label: '批次号', prop: 'batchNo', minWidth: 120 },
|
||||
{ label: '计费方式', prop: 'billingType', minWidth: 130 },
|
||||
{ label: '应收应付合计', prop: 'totalAmount', minWidth: 140, money: true },
|
||||
{ label: '状态', prop: 'settlementStatusName', minWidth: 110 },
|
||||
];
|
||||
|
||||
@@ -4,7 +4,7 @@ export const settlementTypeOptions = [
|
||||
];
|
||||
|
||||
export const preSettlementFormFields = [
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', readonly: true },
|
||||
{ label: '预付结算单号', prop: 'preSettlementNo', readonly: true },
|
||||
{ label: '合同名称', prop: 'contractId', type: 'contract', required: true },
|
||||
{ label: '合同编号', prop: 'contractNo', readonly: true },
|
||||
{ label: '项目名称', prop: 'projectName', readonly: true },
|
||||
@@ -12,6 +12,8 @@ export const preSettlementFormFields = [
|
||||
{ label: '付款方', prop: 'payerName', readonly: true },
|
||||
{ label: '结算类型', prop: 'settlementType', readonly: true },
|
||||
{ label: '结算金额', prop: 'settlementAmount', readonly: true, money: true },
|
||||
{ label: '业务模式', prop: 'businessModel', type: 'select', optionsKey: 'businessModelOptions' },
|
||||
{ label: '运输方式', prop: 'transportType', type: 'select', optionsKey: 'transportTypeOptions' },
|
||||
{ label: '汇率日期', prop: 'exchangeRateDate', type: 'date' },
|
||||
{ label: '结算汇率', prop: 'exchangeRate', type: 'number', class: 'exchange-rate-input' },
|
||||
{ label: '本位币合计', prop: 'localSettlementAmount', readonly: true, money: true },
|
||||
@@ -64,6 +66,8 @@ export const emptyPreSettlementForm = () => ({
|
||||
settlementType: 'payable',
|
||||
currency: 'RMB',
|
||||
settlementAmount: 0,
|
||||
businessModel: '',
|
||||
transportType: '',
|
||||
localCurrency: 'RMB',
|
||||
localSettlementAmount: 0,
|
||||
exchangeRateDate: '',
|
||||
|
||||
@@ -7,7 +7,7 @@ export const approvalStatusOptions = [
|
||||
];
|
||||
|
||||
export const preSettlementSearchFields = [
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', type: 'input' },
|
||||
{ label: '预付结算单号', prop: 'preSettlementNo', type: 'input' },
|
||||
{ label: '预付单号', prop: 'advanceNo', type: 'input' },
|
||||
{ label: '项目名称', prop: 'projectName', type: 'input' },
|
||||
{ label: '所属组织', prop: 'deptName', type: 'cascader' },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const preSettlementTableColumns = [
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', minWidth: 150, link: true, fixed: 'left' },
|
||||
{ label: '预付结算单号', prop: 'preSettlementNo', minWidth: 150, link: true, fixed: 'left' },
|
||||
{ label: '来源', prop: 'sourceType', minWidth: 100 },
|
||||
{ label: '付款方', prop: 'payerName', minWidth: 160 },
|
||||
{ label: '收款方', prop: 'payeeName', minWidth: 160 },
|
||||
@@ -8,6 +8,8 @@ export const preSettlementTableColumns = [
|
||||
{ label: '合同编号', prop: 'contractNo', minWidth: 140 },
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 160 },
|
||||
{ label: '原币结算金额', prop: 'settlementAmount', minWidth: 140, money: true, precision: 2 },
|
||||
{ label: '业务模式', prop: 'businessModelName', minWidth: 120 },
|
||||
{ label: '运输方式', prop: 'transportTypeName', minWidth: 120 },
|
||||
{ label: '本位币结算金额', prop: 'localSettlementAmount', minWidth: 150, money: true, precision: 2 },
|
||||
{ label: '结算汇率', prop: 'exchangeRate', minWidth: 110, precision: 2 },
|
||||
{
|
||||
@@ -40,7 +42,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 },
|
||||
@@ -52,6 +54,7 @@ export const settlementDetailColumns = [
|
||||
{ label: '运输总量', prop: 'transportQuantityText', minWidth: 120 },
|
||||
{ label: '里程(KM)', prop: 'mileage', minWidth: 120 },
|
||||
{ label: '批次号', prop: 'batchNo', minWidth: 130 },
|
||||
{ label: '计费方式', prop: 'billingType', minWidth: 130 },
|
||||
{ label: '运输单价', prop: 'unitPrice', minWidth: 120, money: true },
|
||||
{ label: '运费', prop: 'freightAmount', minWidth: 120, money: true },
|
||||
{ label: '原金额', prop: 'originalAmount', minWidth: 120, money: true },
|
||||
@@ -94,5 +97,6 @@ export const candidateDetailColumns = [
|
||||
{ label: '货物名称', prop: 'cargoName', minWidth: 140 },
|
||||
{ label: '货物类型', prop: 'cargoType', minWidth: 130 },
|
||||
{ label: '批次号', prop: 'batchNo', minWidth: 120 },
|
||||
{ label: '计费方式', prop: 'billingType', minWidth: 130 },
|
||||
{ label: '结算金额', prop: 'totalAmount', minWidth: 130, money: true },
|
||||
];
|
||||
|
||||
@@ -20,7 +20,7 @@ export const searchFields = [
|
||||
{ label: '货物名称', prop: 'cargoName', type: 'input' },
|
||||
{ label: '合同编号', prop: 'contractNo', type: 'input' },
|
||||
{ label: '合同名称', prop: 'contractName', type: 'input' },
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', type: 'input' },
|
||||
{ label: '预付结算单号', prop: 'preSettlementNo', type: 'input' },
|
||||
{ label: '正式结算单号', prop: 'formalSettlementNo', type: 'input' },
|
||||
{ label: '批次号', prop: 'batchNo', type: 'input' },
|
||||
{ label: '车号', prop: 'vehicleNo', type: 'input' },
|
||||
@@ -35,7 +35,7 @@ export const tableColumns = [
|
||||
{ label: '合同编号', prop: 'contractNo', minWidth: 140 },
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 140 },
|
||||
{ label: '来源', prop: 'sourceType', minWidth: 110 },
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', minWidth: 140, link: true },
|
||||
{ label: '预付结算单号', prop: 'preSettlementNo', minWidth: 140, link: true },
|
||||
{ label: '正式结算单号', prop: 'formalSettlementNo', minWidth: 140, link: true },
|
||||
{ label: '运单号', prop: 'waybillNo', minWidth: 140, link: true },
|
||||
{ label: '车号', prop: 'vehicleNo', minWidth: 120 },
|
||||
@@ -88,6 +88,8 @@ export const updateFeeFormFields = [
|
||||
];
|
||||
|
||||
export const transferSearchFields = [
|
||||
{ label: '单据号', prop: 'documentNo', type: 'input' },
|
||||
{ label: '合同编码', prop: 'contractNo', type: 'input' },
|
||||
{ label: '合同名称', prop: 'contractName', type: 'input' },
|
||||
{ label: '批次号', prop: 'batchNo', type: 'input' },
|
||||
{ label: '生成日期', prop: 'generateDateRange', type: 'daterange' },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const settlementAdjustmentTableColumns = [
|
||||
{ label: '单据号', prop: 'adjustmentNo', minWidth: 160, link: true, fixed: 'left' },
|
||||
{ label: '结算单号', prop: 'formalSettlementNo', minWidth: 160 },
|
||||
{ label: '结算单号', prop: 'formalSettlementNo', minWidth: 160, link: true },
|
||||
{ label: '结算单类型', prop: 'settlementTypeName', minWidth: 110 },
|
||||
{ label: '项目名称', prop: 'projectName', minWidth: 140 },
|
||||
{ label: '所属组织', prop: 'deptName', minWidth: 140 },
|
||||
@@ -9,6 +9,8 @@ export const settlementAdjustmentTableColumns = [
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 170 },
|
||||
{ label: '调整金额', prop: 'adjustmentAmount', minWidth: 130, money: true },
|
||||
{ label: '审核状态', prop: 'approvalStatusName', minWidth: 110, status: true },
|
||||
{ label: '金蝶单据状态', prop: 'kingdeeSyncStatus', minWidth: 130, kingdee: true },
|
||||
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 },
|
||||
{ label: '当前节点', prop: 'currentNode', minWidth: 120 },
|
||||
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
|
||||
{ label: '创建人', prop: 'createUserName', minWidth: 110 },
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 导入结算单列表(avue-crud)配置
|
||||
* ------------------------------------------------------------------
|
||||
* 列表页与其它模块统一使用 avue-crud,头部 4 个按钮(刷新 / 列显隐 / 搜索显隐 /
|
||||
* 显示方式)由 avue-crud 自带,不做二次封装。
|
||||
*
|
||||
* ⚠️ searchOrder 为降序排序,值越大越靠前。
|
||||
* ⚠️ 仅用于搜索的列必须 hide + showColumn: false,否则会混进「列显隐」抽屉。
|
||||
*/
|
||||
|
||||
const IMPORT_STATUS_OPTIONS = [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
{ label: '已作废', value: 'voided' },
|
||||
];
|
||||
|
||||
const SETTLEMENT_TYPE_OPTIONS = [
|
||||
{ label: '应收', value: 'receivable' },
|
||||
{ label: '应付', value: 'payable' },
|
||||
];
|
||||
|
||||
const textFormatter = (row, value) =>
|
||||
value === null || value === undefined || value === '' ? '-' : value;
|
||||
|
||||
const pickLabel = (options, value) =>
|
||||
options.find(item => item.value === value)?.label || value || '-';
|
||||
|
||||
const settlementTypeLabel = value => pickLabel(SETTLEMENT_TYPE_OPTIONS, value);
|
||||
|
||||
const billTypeLabel = value => (value === 'pre' ? '预结算单' : value ? '正式结算单' : '-');
|
||||
|
||||
export const importStatusLabel = value => pickLabel(IMPORT_STATUS_OPTIONS, value);
|
||||
|
||||
export const settlementImportOption = () => ({
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
tip: false,
|
||||
searchBtnText: '查询',
|
||||
emptyBtnText: '重置',
|
||||
searchShow: true,
|
||||
searchIcon: true,
|
||||
searchLabelWidth: 88,
|
||||
// 查询/重置紧跟首行 4 个条件(4×5 + 菜单 4 = 24),不再独占下一整行
|
||||
searchMenuSpan: 4,
|
||||
searchIndex: 4,
|
||||
searchMenuPosition: 'right',
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 64,
|
||||
indexFixed: 'left',
|
||||
selection: false,
|
||||
addBtn: false,
|
||||
viewBtn: false,
|
||||
delBtn: false,
|
||||
editBtn: false,
|
||||
menuWidth: 220,
|
||||
menuFixed: 'right',
|
||||
column: [
|
||||
// ---------- 搜索项(前 4 项默认展示,其余折叠;searchOrder 越大越靠前) ----------
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'importStatus',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchSpan: 5,
|
||||
searchOrder: 6,
|
||||
searchPlaceholder: '请选择',
|
||||
dicData: IMPORT_STATUS_OPTIONS,
|
||||
hide: true,
|
||||
showColumn: false,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '导入类型',
|
||||
prop: 'settlementType',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchSpan: 5,
|
||||
searchOrder: 5,
|
||||
searchPlaceholder: '请选择',
|
||||
dicData: SETTLEMENT_TYPE_OPTIONS,
|
||||
hide: true,
|
||||
showColumn: false,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
// ---------- 表格列 ----------
|
||||
{
|
||||
label: '导入批次号',
|
||||
prop: 'batchNo',
|
||||
slot: true,
|
||||
search: true,
|
||||
searchSpan: 5,
|
||||
searchOrder: 7,
|
||||
searchPlaceholder: '请选择',
|
||||
minWidth: 160,
|
||||
},
|
||||
{ label: '状态', prop: 'importStatusName', slot: true, width: 100, align: 'center' },
|
||||
{
|
||||
label: '导入类型',
|
||||
prop: 'settlementTypeName',
|
||||
minWidth: 100,
|
||||
align: 'center',
|
||||
formatter: (row, value) => value || settlementTypeLabel(row.settlementType),
|
||||
},
|
||||
{
|
||||
label: '结算单',
|
||||
prop: 'settlementBillTypeName',
|
||||
minWidth: 110,
|
||||
align: 'center',
|
||||
formatter: (row, value) => value || billTypeLabel(row.settlementBillType),
|
||||
},
|
||||
{
|
||||
label: '项目',
|
||||
prop: 'projectName',
|
||||
search: true,
|
||||
searchSpan: 5,
|
||||
searchOrder: 4,
|
||||
searchPlaceholder: '请输入',
|
||||
minWidth: 140,
|
||||
formatter: textFormatter,
|
||||
},
|
||||
{
|
||||
label: '客户',
|
||||
prop: 'customerName',
|
||||
search: true,
|
||||
searchSpan: 5,
|
||||
searchOrder: 3,
|
||||
searchPlaceholder: '请输入',
|
||||
minWidth: 140,
|
||||
formatter: textFormatter,
|
||||
},
|
||||
{
|
||||
label: '承运商',
|
||||
prop: 'carrierName',
|
||||
search: true,
|
||||
searchSpan: 5,
|
||||
searchOrder: 2,
|
||||
searchPlaceholder: '请输入',
|
||||
minWidth: 140,
|
||||
formatter: textFormatter,
|
||||
},
|
||||
{
|
||||
label: '明细行数',
|
||||
prop: 'detailCount',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
formatter: row => row.detailCount ?? row.waybillCount ?? 0,
|
||||
},
|
||||
{ label: '创建时间', prop: 'createTime', width: 180, align: 'center', formatter: textFormatter },
|
||||
{ label: '创建人', prop: 'createUserName', minWidth: 110, formatter: textFormatter },
|
||||
],
|
||||
});
|
||||
@@ -13,7 +13,7 @@ export const reconciliationStatusOptions = [
|
||||
|
||||
export const transportReconciliationSearchFields = [
|
||||
{ prop: 'reconciliationNo', label: '对账单号', type: 'input' },
|
||||
{ prop: 'preSettlementNos', label: '预结算单号', type: 'input' },
|
||||
{ prop: 'preSettlementNos', label: '预付结算单号', type: 'input' },
|
||||
{ prop: 'projectName', label: '项目', type: 'input' },
|
||||
{ prop: 'deptName', label: '所属组织', type: 'cascader' },
|
||||
{ prop: 'contractNo', label: '合同编号', type: 'input' },
|
||||
|
||||
@@ -14,6 +14,11 @@ const validatePhone = (rule, value, callback) => {
|
||||
callback(new Error('请输入手机号'));
|
||||
return;
|
||||
}
|
||||
// 详情回显为脱敏手机号(含 *)时放行,未修改则提交时剔除该字段
|
||||
if (phone.includes('*')) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (!/^1\d{10}$/.test(phone)) {
|
||||
callback(new Error('请输入正确的手机号'));
|
||||
return;
|
||||
@@ -121,6 +126,62 @@ export default {
|
||||
order: 60,
|
||||
rules: [{ required: true, message: '请选择小程序身份', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '所属组织',
|
||||
prop: 'deptId',
|
||||
formslot: true,
|
||||
hide: true,
|
||||
span: 24,
|
||||
order: 55,
|
||||
// 所属组织可为空:为空表示未分配组织,账号的数据权限为空集
|
||||
},
|
||||
{
|
||||
label: '所属组织',
|
||||
prop: 'deptName',
|
||||
minWidth: 180,
|
||||
overHidden: true,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
order: 54,
|
||||
},
|
||||
{
|
||||
label: '数据权限范围',
|
||||
prop: 'dataScopeRange',
|
||||
type: 'radio',
|
||||
span: 24,
|
||||
hide: true,
|
||||
order: 53,
|
||||
value: 1,
|
||||
dicData: [
|
||||
{ label: '仅所属组织', value: 1 },
|
||||
{ label: '全部组织', value: 2 },
|
||||
{ label: '自定义', value: 3 },
|
||||
],
|
||||
rules: [{ required: true, message: '请选择数据权限范围', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '数据层级范围',
|
||||
prop: 'dataLevelRange',
|
||||
type: 'radio',
|
||||
span: 24,
|
||||
hide: true,
|
||||
order: 52,
|
||||
value: 1,
|
||||
dicData: [
|
||||
{ label: '包含下级', value: 1 },
|
||||
{ label: '仅本级', value: 2 },
|
||||
],
|
||||
rules: [{ required: true, message: '请选择数据层级范围', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '数据权限',
|
||||
prop: 'dataScopeDeptIds',
|
||||
formslot: true,
|
||||
span: 24,
|
||||
hide: true,
|
||||
order: 51,
|
||||
},
|
||||
{
|
||||
label: '性别',
|
||||
prop: 'sex',
|
||||
|
||||
@@ -72,6 +72,7 @@ export const userOption = safe => {
|
||||
},
|
||||
{ label: '岗位', prop: 'postName', overHidden: true, display: false },
|
||||
{ label: '性别', prop: 'sexName', width: 80, display: false },
|
||||
{ label: 'OA登录名', prop: 'oaLoginId', minWidth: 140, overHidden: true, display: false },
|
||||
{ label: 'OA人员id', prop: 'oaPersonId', minWidth: 120, overHidden: true, display: false },
|
||||
{ label: '备注', prop: 'remark', minWidth: 160, overHidden: true, display: false },
|
||||
{
|
||||
@@ -314,29 +315,6 @@ export const userOption = safe => {
|
||||
{ label: '承运商', value: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据权限范围',
|
||||
prop: 'dataScopeRange',
|
||||
type: 'radio',
|
||||
span: 24,
|
||||
value: 1,
|
||||
dicData: [
|
||||
{ label: '仅所属组织', value: 1 },
|
||||
{ label: '全部组织', value: 2 },
|
||||
{ label: '自定义', value: 3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据层级范围',
|
||||
prop: 'dataLevelRange',
|
||||
type: 'radio',
|
||||
span: 24,
|
||||
value: 1,
|
||||
dicData: [
|
||||
{ label: '包含下级', value: 1 },
|
||||
{ label: '仅本级', value: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
prop: 'remark',
|
||||
|
||||
@@ -48,6 +48,7 @@ export const option = {
|
||||
searchOrder: 1,
|
||||
searchslot: true,
|
||||
slot: true,
|
||||
overHidden: true,
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
@@ -55,6 +56,8 @@ export const option = {
|
||||
prop: 'useDepartment',
|
||||
search: true,
|
||||
searchOrder: 3,
|
||||
slot: true,
|
||||
overHidden: true,
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -108,7 +108,7 @@ export default [
|
||||
},
|
||||
{
|
||||
path: '/settlement/pre-settlement/public-view',
|
||||
name: '查看预结算信息',
|
||||
name: '查看预付结算单信息',
|
||||
component: () => import('@/views/settlement/pre-settlement-public-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
@@ -119,7 +119,7 @@ export default [
|
||||
},
|
||||
{
|
||||
path: '/settlement/formal-settlement/public-view',
|
||||
name: '查看正式结算信息',
|
||||
name: '查看结算单信息',
|
||||
component: () => import('@/views/settlement/formal-settlement-public-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
|
||||
@@ -95,7 +95,7 @@ export default [
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: '新增编辑预结算',
|
||||
name: '新增编辑预付结算单',
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
activeMenu: '/settlement/pre-settlement',
|
||||
@@ -110,15 +110,31 @@ export default [
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: '新增编辑正式结算',
|
||||
name: '新增编辑结算单',
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
activeMenu: '/settlement/formal-settlement',
|
||||
// 付款/收款结算单共用表单页,具体高亮菜单由页面按 settlementType 回写
|
||||
activeMenu: '/settlement/payment-settlement',
|
||||
},
|
||||
component: loadView(() => import('@/views/settlement/formal-settlement-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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,
|
||||
|
||||
@@ -367,7 +367,7 @@
|
||||
|
||||
// 4) 弹窗内表单项上下间距:预留绝对定位校验提示(约 14~18px)的可视空间
|
||||
.el-dialog .el-form-item {
|
||||
margin-bottom: 24px;
|
||||
//margin-bottom: 24px;
|
||||
}
|
||||
|
||||
// 校验提示浮在控件下方,提高层级避免被下一行输入框背景遮挡
|
||||
@@ -622,7 +622,7 @@
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
// 合同管理新增/编辑弹窗(business-crud-page 封装 → avue-crud,弹窗带 avue-crud__dialog class)
|
||||
// 合同管理新增/编辑弹窗(avue-crud,弹窗带 avue-crud__dialog class)
|
||||
// 复刻 waybill-manage 方案:接近全屏、压过库内置 .avue-dialog 的 calc(100% - 200px),
|
||||
// 并解开 body 的 200px 上限、抵消全局 footer 的 -30px 上移
|
||||
.contract-manage-dialog.el-dialog {
|
||||
@@ -665,6 +665,15 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// 车辆调度弹窗 label 宽度统一:option 用 labelWidth: 'auto' 时 label 宽度随字数变化
|
||||
// (3 字 64px / 2 字 40px / 4 字带星号 78px),控件可用宽度随之变化,
|
||||
// 于是同一行两列控件宽度不一致(如车牌号 486px vs 使用部门 472px)。
|
||||
// 本弹窗左右两列最长 label 均为 4 个汉字(所属组织 / 使用部门 / 申请时间),故统一按 4em + 24px 取宽。
|
||||
.vehicle-dispatch-dialog .el-form-item__label {
|
||||
flex: 0 0 calc(4em + 24px) !important;
|
||||
width: calc(4em + 24px) !important;
|
||||
}
|
||||
|
||||
// 过程配置新增/编辑/查看弹窗(手写 el-dialog,参照 vehicle/credit-score-quantification):
|
||||
// 灰底弹窗体(全站 .el-dialog__body 已统一)内堆叠 <section-card> 分组白卡,
|
||||
// 卡间 12px 间距由 section-card 自身 margin-bottom 保证,与项目其他弹窗风格统一
|
||||
|
||||
@@ -26,6 +26,35 @@ const uniqueOrgNames = (values = []) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
// 候选司机取数上限:超出后只能靠关键字缩小范围
|
||||
const DRIVER_CANDIDATE_MAX_SIZE = 200;
|
||||
|
||||
/**
|
||||
* 派单候选司机:租户内的司机档案,按岗位等条件过滤。
|
||||
*
|
||||
* 与组织、承运商都无关——司机档案与组织树、承运商之间没有外键,`organization_name`
|
||||
* 只是自由文本,用它做匹配会让小程序自动建档的司机永远进不了候选。
|
||||
*/
|
||||
export const fetchDriverCandidates = async (query = {}) => {
|
||||
const params = { ...query };
|
||||
const size = Math.min(Math.max(Number(params.size) || 50, 50), DRIVER_CANDIDATE_MAX_SIZE);
|
||||
delete params.size;
|
||||
const res = await getDriverList(1, size, params);
|
||||
return extractRecords(res).slice(0, size);
|
||||
};
|
||||
|
||||
/**
|
||||
* 司机候选项的展示文本:姓名(手机尾号后 4 位)。
|
||||
*
|
||||
* 只用于下拉渲染。写回表单的仍是姓名本身,别把带尾号的文本当值用。
|
||||
*/
|
||||
export const driverOptionText = (item = {}) => {
|
||||
const name = normalizeOrgName(item.driverName || item.name);
|
||||
const mobile = String(item.mobile || item.phone || item.driverPhone || '').trim();
|
||||
if (!name) return '';
|
||||
return mobile.length >= 4 ? `${name}(${mobile.slice(-4)})` : name;
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析承运商客商档案上联系人的所属组织(branchName),无联系人时回退客商所属组织。
|
||||
*/
|
||||
@@ -72,13 +101,6 @@ export const resolveCarrierOrganizationNames = async ({ carrierId, carrierName }
|
||||
return orgNames;
|
||||
};
|
||||
|
||||
export const clearCarrierOrganizationCache = (carrier = {}) => {
|
||||
const id = String(carrier.carrierId || '').trim();
|
||||
const name = normalizeOrgName(carrier.carrierName);
|
||||
if (id) orgNameCache.delete(id);
|
||||
if (name) orgNameCache.delete(name);
|
||||
};
|
||||
|
||||
export const matchOrganizationName = (value, orgNames = []) => {
|
||||
const text = normalizeOrgName(value);
|
||||
if (!text || !orgNames.length) return false;
|
||||
@@ -90,34 +112,9 @@ const filterByOrganizations = (records = [], orgNames = []) => {
|
||||
return records.filter(item => matchOrganizationName(item.organizationName, orgNames));
|
||||
};
|
||||
|
||||
/**
|
||||
* 按承运商联系人所属组织筛选司机。
|
||||
* 未选择承运商时返回空列表(需先选承运商)。
|
||||
*/
|
||||
export const fetchDriversByCarrierOrganizations = async (
|
||||
query = {},
|
||||
{ carrierId, carrierName } = {}
|
||||
) => {
|
||||
if (!String(carrierId || '').trim() && !normalizeOrgName(carrierName)) {
|
||||
return [];
|
||||
}
|
||||
const orgNames = await resolveCarrierOrganizationNames({ carrierId, carrierName });
|
||||
if (!orgNames.length) return [];
|
||||
|
||||
const size = Math.max(Number(query.size) || 50, 50);
|
||||
const params = { ...query };
|
||||
delete params.size;
|
||||
// 单组织时用后端模糊条件缩小范围;多组织再前端精确过滤
|
||||
if (orgNames.length === 1) {
|
||||
params.organizationName = orgNames[0];
|
||||
}
|
||||
const res = await getDriverList(1, Math.min(size * 5, 200), params);
|
||||
return filterByOrganizations(extractRecords(res), orgNames).slice(0, size);
|
||||
};
|
||||
|
||||
/**
|
||||
* 按承运商联系人所属组织筛选车辆。
|
||||
* 未选择承运商时返回空列表。
|
||||
* 未选择承运商时返回空列表(需先选承运商)。
|
||||
*/
|
||||
export const fetchVehiclesByCarrierOrganizations = async (
|
||||
query = {},
|
||||
|
||||
@@ -106,6 +106,9 @@ const CERT_SNAPSHOT_GROUPS = {
|
||||
fields: [
|
||||
{ key: 'realName', label: '真实姓名' },
|
||||
{ key: 'idCardNo', label: '身份证号' },
|
||||
{ key: 'birthday', label: '出生年月', type: TYPE_DATE },
|
||||
{ key: 'addressRegion', label: '住址' },
|
||||
{ key: 'address', label: '详细地址' },
|
||||
{ key: 'idCardFrontUrl', label: '身份证正面照', type: TYPE_IMAGE },
|
||||
{ key: 'idCardBackUrl', label: '身份证反面照', type: TYPE_IMAGE },
|
||||
{ key: 'idCardValidStart', label: '身份证有效期起', type: TYPE_DATE },
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'nprogress/nprogress.css';
|
||||
const excelPattern = /\.(xls|xlsx)$/;
|
||||
|
||||
/** 是否为浏览器 Blob(含跨 realm 的 Blob-like) */
|
||||
const isBlobLike = value =>
|
||||
export const isBlobLike = value =>
|
||||
value != null &&
|
||||
typeof value === 'object' &&
|
||||
typeof value.size === 'number' &&
|
||||
@@ -15,14 +15,14 @@ const isBlobLike = value =>
|
||||
(typeof value.text === 'function' || typeof value.arrayBuffer === 'function');
|
||||
|
||||
/** axios 拦截器已把 JSON Blob 解析成的业务对象 */
|
||||
const isBusinessJson = value =>
|
||||
export const isBusinessJson = value =>
|
||||
value != null &&
|
||||
typeof value === 'object' &&
|
||||
!isBlobLike(value) &&
|
||||
!(value instanceof ArrayBuffer) &&
|
||||
('code' in value || 'success' in value || 'msg' in value);
|
||||
|
||||
const isExcelResponse = res => {
|
||||
export const isExcelResponse = res => {
|
||||
// 已是业务 JSON(导入成功/失败提示)时,绝不能当失败明细 Excel 处理
|
||||
if (isBusinessJson(res?.data)) {
|
||||
return false;
|
||||
@@ -54,7 +54,7 @@ const isExcelResponse = res => {
|
||||
* 提前解析成普通对象;此处需同时兼容 Blob / 字符串 / 已解析对象,避免再调
|
||||
* `blob.text()` 时出现「blob.text is not a function」。
|
||||
*/
|
||||
const parseBlobJson = async data => {
|
||||
export const parseBlobJson = async data => {
|
||||
if (data == null) {
|
||||
return {};
|
||||
}
|
||||
@@ -88,7 +88,7 @@ const parseBlobJson = async data => {
|
||||
*
|
||||
* 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。
|
||||
*/
|
||||
const resolveErrorMessage = async error => {
|
||||
export const resolveImportErrorMessage = async error => {
|
||||
const payload = error?.response?.data ?? error?.data;
|
||||
if (isBusinessJson(payload) && payload.msg) {
|
||||
return payload.msg;
|
||||
@@ -109,6 +109,26 @@ const resolveErrorMessage = async error => {
|
||||
return fallback;
|
||||
};
|
||||
|
||||
/**
|
||||
* 统一消费导入接口响应:成功 JSON / 失败 Excel 流。
|
||||
* 兼容 axios 已把 JSON Blob 解析成对象的情况,禁止再对普通对象调 .text()。
|
||||
*/
|
||||
export const consumeImportResponse = async (res, options = {}) => {
|
||||
const { businessName = '导入', dayjsFormat } = options;
|
||||
if (isExcelResponse(res)) {
|
||||
return {
|
||||
kind: 'excel',
|
||||
blob: res?.data,
|
||||
fileName: `${businessName}导入失败明细${dayjsFormat || ''}.xlsx`,
|
||||
};
|
||||
}
|
||||
const result = isBusinessJson(res?.data) ? res.data : await parseBlobJson(res?.data);
|
||||
if (Number(result?.code) !== 200) {
|
||||
throw new Error(result?.msg || '导入失败');
|
||||
}
|
||||
return { kind: 'success', result };
|
||||
};
|
||||
|
||||
/**
|
||||
* 加工后端返回的导入失败明细 Excel。
|
||||
* 若传入 failDetailDecorator(函数),则动态加载 exceljs 读取 blob 流,
|
||||
@@ -235,7 +255,7 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
||||
// 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次,
|
||||
// 只保留一次清晰的中文提示。
|
||||
if (!error?._notified) {
|
||||
vm.$message.error(await resolveErrorMessage(error));
|
||||
vm.$message.error(await resolveImportErrorMessage(error));
|
||||
}
|
||||
notifyUploadError(option, error);
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 金蝶侧发票附件查看/下载公共工具
|
||||
*
|
||||
* 票池发票附件为 {source:'piaozone', path, name}:path 是金蝶侧文件 id(非 http 直链),
|
||||
* 须由后端代理 A801-2 流式回源;OSS 直链附件(url/link/fileUrl)仍由前端直连下载/预览。
|
||||
* 入口共用:票池弹窗、正式结算认领发票行、付款申请发票附件行、收票登记附件表。
|
||||
*/
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { downloadFile } from '@/utils/util';
|
||||
import { downloadInvoicePoolAttachment } from '@/api/payment/invoiceReceipt';
|
||||
|
||||
/**
|
||||
* 金蝶侧附件判定:source=piaozone,或有 path 且非 http(s) 直链
|
||||
* @param {Object|Null} attachment 附件对象(attachmentsJson/attachmentJson 行内首项)
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export const isKingdeePathAttachment = attachment => {
|
||||
if (!attachment) return false;
|
||||
if (attachment.source === 'piaozone') return true;
|
||||
return Boolean(attachment.path) && !/^https?:\/\//i.test(attachment.path);
|
||||
};
|
||||
|
||||
/**
|
||||
* 请求金蝶文件流:成功返回 Blob;业务失败为 HTTP 200 + R.fail JSON(响应通道未写流时后端即失败),
|
||||
* 按 Content-Type 识别并抛出带 msg 的错误;网络级失败拦截器已提示,标记 silent 避免重复提示。
|
||||
* 供查看(弹窗内预览)/下载两类入口复用
|
||||
* @param {String} invoiceNo 发票号码
|
||||
* @returns {Promise<Blob>}
|
||||
*/
|
||||
export const requestKingdeeFile = async invoiceNo => {
|
||||
let res;
|
||||
try {
|
||||
res = await downloadInvoicePoolAttachment(invoiceNo);
|
||||
} catch (error) {
|
||||
throw Object.assign(new Error(error?.message || '附件下载失败'), { silent: true });
|
||||
}
|
||||
const blob = res?.data;
|
||||
if (!(blob instanceof Blob)) {
|
||||
throw new Error('响应不是文件流');
|
||||
}
|
||||
if (blob.type && blob.type.includes('application/json')) {
|
||||
let message = '附件下载失败';
|
||||
try {
|
||||
const parsed = JSON.parse(await blob.text());
|
||||
message = parsed.msg || parsed.message || message;
|
||||
} catch {
|
||||
// 错误体非 JSON 时按默认提示
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
return blob;
|
||||
};
|
||||
|
||||
/**
|
||||
* 下载金蝶侧发票附件(经 A801-2 后端代理),失败统一 ElMessage 提示
|
||||
* @param {String} invoiceNo 发票号码
|
||||
* @param {String} fileName 下载文件名(缺省按 发票号.pdf)
|
||||
*/
|
||||
export const downloadKingdeeInvoiceFile = async (invoiceNo, fileName) => {
|
||||
if (!invoiceNo) {
|
||||
ElMessage.warning('当前发票缺少发票号码,无法下载金蝶侧附件');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const blob = await requestKingdeeFile(invoiceNo);
|
||||
downloadFile(blob, fileName || `${invoiceNo}.pdf`);
|
||||
} catch (error) {
|
||||
if (!error?.silent) ElMessage.error(error?.message || '金蝶附件下载失败');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 新窗口预览金蝶侧发票附件:拉取 Blob 转 objectURL(PDF 可预览;OFD 等浏览器不可预览的格式由浏览器触发下载)
|
||||
* @param {String} invoiceNo 发票号码
|
||||
* @param {String} fileName 预览文件名(缺省按 发票号.pdf)
|
||||
*/
|
||||
export const viewKingdeeInvoiceFile = async (invoiceNo, fileName) => {
|
||||
if (!invoiceNo) {
|
||||
ElMessage.warning('当前发票缺少发票号码,无法预览金蝶侧附件');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const blob = await requestKingdeeFile(invoiceNo);
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, '_blank');
|
||||
// 延迟释放 objectURL:立即回收可能导致新窗口尚未加载完成即失效
|
||||
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
||||
} catch (error) {
|
||||
if (!error?.silent) ElMessage.error(error?.message || '金蝶附件预览失败');
|
||||
}
|
||||
};
|
||||
+66
-14
@@ -23,7 +23,7 @@ export const MK_BIZ = {
|
||||
},
|
||||
'pre-settlement': {
|
||||
dictName: '提交预结算审核流',
|
||||
subjectPrefix: '预结算审批',
|
||||
subjectPrefix: '预付结算审批',
|
||||
rejected: ['returned'],
|
||||
approved: ['approved'],
|
||||
},
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 行政区划展示格式化(仅展示层,不改动后端存储值)。
|
||||
*
|
||||
* 需求:省 / 市 / 区县之间要有空格隔开,避免「上海市浦东新区」「广西壮族自治区南宁市邕宁区」挤成一坨。
|
||||
* 与全站既有规则保持一致(公路地址按「市 区县」展示,省/自治区不重复展示):
|
||||
* 上海市浦东新区 -> 上海市 浦东新区
|
||||
* 广西壮族自治区南宁市邕宁区 -> 南宁市 邕宁区
|
||||
* 梅州市梅县区 -> 梅州市 梅县区
|
||||
*
|
||||
* 非纯行政区划串(站点名「南宁站」「广州南站」、含街道门牌的详细地址等)原样返回,不做任何改动。
|
||||
*/
|
||||
|
||||
// 行政区划后缀:长后缀在前,避免「自治区」被截成「区」、「自治州」被截成「州」
|
||||
const REGION_SUFFIXES = [
|
||||
'特别行政区',
|
||||
'自治区',
|
||||
'自治州',
|
||||
'自治县',
|
||||
'自治旗',
|
||||
'开发区',
|
||||
'新区',
|
||||
'林区',
|
||||
'矿区',
|
||||
'地区',
|
||||
'省',
|
||||
'市',
|
||||
'州',
|
||||
'盟',
|
||||
'区',
|
||||
'县',
|
||||
'旗',
|
||||
];
|
||||
|
||||
// 省级区划:展示时不在区划链首重复(「广西壮族自治区南宁市邕宁区」-> 「南宁市 邕宁区」)
|
||||
const PROVINCE_SUFFIX = /(?:省|自治区|特别行政区)$/;
|
||||
|
||||
/**
|
||||
* 从文本头部匹配第一个行政区划片段。
|
||||
* 特例:「梅州市」州后紧跟市,并入同一片段;「梅县区」县/旗后紧跟区,并入同一片段。
|
||||
* @param {string} text 已去除空白的文本
|
||||
* @returns {string} 片段,未匹配到返回 ''
|
||||
*/
|
||||
function matchRegionToken(text) {
|
||||
for (let end = 0; end < text.length; end += 1) {
|
||||
const prefix = text.slice(0, end + 1);
|
||||
const suffix = REGION_SUFFIXES.find(item => prefix.endsWith(item));
|
||||
if (!suffix) continue;
|
||||
const next = text[end + 1] || '';
|
||||
if ((suffix === '州' || suffix === '盟') && next === '市') continue;
|
||||
if ((suffix === '县' || suffix === '旗') && next === '区') return text.slice(0, end + 2);
|
||||
return prefix;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 按行政区划后缀把文本切成区划片段。
|
||||
* @param {string} value 原值
|
||||
* @returns {string[]|null} 整串都是行政区划时返回片段数组,否则返回 null
|
||||
*/
|
||||
export function splitRegionTokens(value) {
|
||||
const text = String(value ?? '').replace(/\s+/g, '');
|
||||
if (!text) return null;
|
||||
const tokens = [];
|
||||
let rest = text;
|
||||
while (rest) {
|
||||
const token = matchRegionToken(rest);
|
||||
if (!token) return null;
|
||||
tokens.push(token);
|
||||
rest = rest.slice(token.length);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行政区划串按「市 区县」展示,省/自治区不重复展示。
|
||||
* @param {string} value 原值
|
||||
* @returns {string} 含空格的展示值;非纯行政区划串原样返回
|
||||
*/
|
||||
export function formatRegionAddress(value) {
|
||||
const text = String(value ?? '').trim();
|
||||
if (!text) return '';
|
||||
const tokens = splitRegionTokens(text);
|
||||
if (!tokens || !tokens.length) return text;
|
||||
const display = PROVINCE_SUFFIX.test(tokens[0]) ? tokens.slice(1) : tokens;
|
||||
return (display.length ? display : tokens).join(' ');
|
||||
}
|
||||
|
||||
export default formatRegionAddress;
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 路线变更记录内容解析
|
||||
*
|
||||
* 存储格式(单行):
|
||||
* YD-20260904-0022:发货地 A → B;到货地 C → D;YD-20260904-0025:发货地 E → F;到货地 G → H
|
||||
* 路线顺序:A → B → C 调整为 B → A → C
|
||||
*
|
||||
* 解析结果(分组,供变更记录列表按「分组标题 - 字段 - 变更前/后」渲染):
|
||||
* [
|
||||
* {
|
||||
* title: 'YD-20260904-0022',
|
||||
* text: '',
|
||||
* fields: [{ label: '发货地', before: 'A', after: 'B', single: 'B', changed: true, unchanged: false }],
|
||||
* },
|
||||
* { title: '', text: '无法识别的原文', fields: [] },
|
||||
* ]
|
||||
*
|
||||
* 说明:仅用于展示层,不改变已存储的 content 文本(历史数据同样适用)。
|
||||
*/
|
||||
// 长标签必须排在短标签前(「发货地址」优先于「发货地」),否则会被前缀吃掉
|
||||
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;
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
export function isFreightFeeName(name) {
|
||||
const text = String(name || '');
|
||||
return text.includes('运费') || text.includes('运输费');
|
||||
}
|
||||
|
||||
function parseBillingRules(row = {}) {
|
||||
const rule = row.billingRule || {};
|
||||
const nested = Array.isArray(rule.rules) ? rule.rules : [];
|
||||
if (nested.length) return nested;
|
||||
const raw = row.billingRulesJson ?? row.billingRules;
|
||||
if (Array.isArray(raw)) return raw.filter(item => item && typeof item === 'object');
|
||||
if (typeof raw === 'string' && raw.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed.filter(item => item && typeof item === 'object') : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/** 运输单价对应的运费计费类型。 */
|
||||
export function freightBillingType(row = {}) {
|
||||
const rule = row.billingRule || {};
|
||||
const rules = parseBillingRules(row);
|
||||
const freightRule = rules.find(
|
||||
item => isFreightFeeName(item?.feeItem) || isFreightFeeName(item?.feeType)
|
||||
);
|
||||
return freightRule?.billingType || row.billingType || rule.billingType || '';
|
||||
}
|
||||
|
||||
export function isTierUnitPrice(row) {
|
||||
return freightBillingType(row) === '阶梯单价';
|
||||
}
|
||||
|
||||
export function isIntervalBilling(row) {
|
||||
const type = freightBillingType(row);
|
||||
return type === '区间单价' || (type.includes('区间') && type.includes('一口价'));
|
||||
}
|
||||
|
||||
/** 阶梯单价、区间单价、区间一口价的运输单价不开放手工调整。 */
|
||||
export function isUnitPriceAdjustable(row) {
|
||||
const type = freightBillingType(row);
|
||||
if (!type) return true;
|
||||
return !isTierUnitPrice(row) && !isIntervalBilling(row);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/** 审批通过、驳回等审批流转类变更内容,不在结算单变更记录中展示。 */
|
||||
export function isApprovalSettlementChange(content) {
|
||||
const text = String(content || '').trim();
|
||||
if (!text) return false;
|
||||
return text.includes('审批通过') || text.includes('审批驳回') || text === '已驳回';
|
||||
}
|
||||
@@ -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 '';
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/** 结算菜单拆分后的展示名与权限前缀 */
|
||||
|
||||
export const SETTLEMENT_TYPE_PAYABLE = 'payable';
|
||||
export const SETTLEMENT_TYPE_RECEIVABLE = 'receivable';
|
||||
|
||||
export function settlementBillLabel(settlementType) {
|
||||
return settlementType === SETTLEMENT_TYPE_RECEIVABLE ? '收款结算单' : '付款结算单';
|
||||
}
|
||||
|
||||
export function settlementPermissionPrefix(settlementType) {
|
||||
return settlementType === SETTLEMENT_TYPE_RECEIVABLE
|
||||
? 'receipt_settlement'
|
||||
: 'payment_settlement';
|
||||
}
|
||||
|
||||
export function settlementListPath(settlementType) {
|
||||
return settlementType === SETTLEMENT_TYPE_RECEIVABLE
|
||||
? '/settlement/receipt-settlement'
|
||||
: '/settlement/payment-settlement';
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限判断:优先新菜单码,兼容旧 formal_settlement_*。
|
||||
* @param {Object} ctx 组件实例(需含 permission、isAdmin、validData)
|
||||
* @param {string} settlementType payable|receivable
|
||||
* @param {string} action add|edit|view...
|
||||
*/
|
||||
export function hasSettlementPermission(ctx, settlementType, action) {
|
||||
if (ctx?.isAdmin) return true;
|
||||
const prefix = settlementPermissionPrefix(settlementType);
|
||||
const codes = [`${prefix}_${action}`, `formal_settlement_${action}`];
|
||||
return codes.some(code => {
|
||||
const value = ctx?.permission?.[code];
|
||||
if (typeof ctx?.validData === 'function') {
|
||||
return ctx.validData(value, false);
|
||||
}
|
||||
return value === true;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 结算单预付记录单据状态名(预结算编辑器与正式结算编辑器共用,避免两页状态名不一致)
|
||||
*/
|
||||
export function settlementStatusName(status, displayValue) {
|
||||
const labels = {
|
||||
reviewing: '审批中',
|
||||
approved: '审批通过',
|
||||
paid: '已付款',
|
||||
returned: '已驳回',
|
||||
voided: '已作废',
|
||||
draft: '草稿',
|
||||
};
|
||||
const name = labels[status];
|
||||
if (name) return name;
|
||||
return typeof displayValue === 'function' ? displayValue(status) : status || '-';
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一预付付款记录类型名
|
||||
*/
|
||||
export function settlementRecordTypeName(recordType) {
|
||||
return { progress_advance: '进度预付', project_advance: '项目预付核销' }[recordType] || '';
|
||||
}
|
||||
+190
-100
@@ -50,7 +50,7 @@
|
||||
title="角色权限配置"
|
||||
append-to-body
|
||||
v-model="box"
|
||||
width="345px"
|
||||
width="960px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-tabs type="border-card">
|
||||
@@ -100,41 +100,26 @@
|
||||
align="middle"
|
||||
style="margin-bottom: 12px; background: #f5f7fa; padding: 6px 10px; border-radius: 4px"
|
||||
>
|
||||
<span style="display: inline-flex; align-items: center">
|
||||
<el-switch
|
||||
v-model="dataScopeLinked"
|
||||
active-text="节点联动"
|
||||
size="small"
|
||||
@change="handleLinkedChange('treeDataScope')"
|
||||
/>
|
||||
<el-tooltip content="开启后勾选父节点会自动勾选所有子节点,关闭则可独立勾选任意节点" placement="top">
|
||||
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"><el-icon-question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<el-button-group>
|
||||
<el-button
|
||||
size="small"
|
||||
plain
|
||||
@click="handleSelectAll('treeDataScope', dataScopeGrantList)"
|
||||
>全选</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
plain
|
||||
@click="handleInvertSelect('treeDataScope', dataScopeGrantList, dataScopeLinked)"
|
||||
>反选</el-button
|
||||
>
|
||||
</el-button-group>
|
||||
<span style="color: #606266; font-size: 13px"
|
||||
>勾选多个菜单后点击「设置」,可批量配置数据权限;设置后菜单旁展示当前范围</span
|
||||
>
|
||||
<el-button type="primary" size="small" @click="openDataScopeSetting">设置</el-button>
|
||||
</el-row>
|
||||
<el-tree
|
||||
:data="dataScopeGrantList"
|
||||
:data="roleDataScopeMenuTree"
|
||||
show-checkbox
|
||||
:check-strictly="!dataScopeLinked"
|
||||
node-key="id"
|
||||
ref="treeDataScope"
|
||||
:default-checked-keys="dataScopeTreeObj"
|
||||
ref="treeRoleDataScope"
|
||||
:props="props"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<span class="role-data-scope-node">
|
||||
<span>{{ data.title }}</span>
|
||||
<span v-if="getMenuScopeLabel(data.id)" class="role-data-scope-tag"
|
||||
>【{{ getMenuScopeLabel(data.id) }}】</span
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="接口权限">
|
||||
@@ -188,6 +173,45 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
title="数据权限设置"
|
||||
append-to-body
|
||||
v-model="dataScopeSettingBox"
|
||||
width="560px"
|
||||
class="role-data-scope-setting-dialog"
|
||||
>
|
||||
<el-form label-position="right" label-width="auto">
|
||||
<el-form-item label="角色名称">
|
||||
<el-input v-model="dataScopeSetting.roleName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据权限">
|
||||
<el-radio-group v-model="dataScopeSetting.scopeType">
|
||||
<el-radio :value="1">所在部门及下级</el-radio>
|
||||
<el-radio :value="2">全部组织</el-radio>
|
||||
<el-radio :value="3">本人创建</el-radio>
|
||||
<el-radio :value="4">自定义组织</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="Number(dataScopeSetting.scopeType) === 4" label="组织">
|
||||
<el-tree
|
||||
ref="treeCustomDept"
|
||||
:data="deptTree"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
:props="{ label: 'title', children: 'children' }"
|
||||
style="width: 100%; max-height: 320px; overflow: auto; border: 1px solid #ebeef5; padding: 8px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dataScopeSettingBox = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitDataScopeSetting">保存</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
@@ -196,37 +220,44 @@ import {
|
||||
add,
|
||||
getList,
|
||||
getRole,
|
||||
getRoleAlias,
|
||||
getRoleTreeById,
|
||||
grant,
|
||||
grantTree,
|
||||
remove,
|
||||
update,
|
||||
} from '@/api/system/role';
|
||||
import { getRoleMenuDataScopeTree, submitRoleMenuDataScope } from '@/api/system/roleDataScope';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import { mapGetters } from 'vuex';
|
||||
import website from '@/config/website';
|
||||
import { validatenull } from '@/utils/validate';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: {},
|
||||
box: false,
|
||||
dataScopeSettingBox: false,
|
||||
props: {
|
||||
label: 'title',
|
||||
value: 'key',
|
||||
},
|
||||
menuGrantList: [],
|
||||
dataScopeGrantList: [],
|
||||
apiScopeGrantList: [],
|
||||
apiGrantList: [],
|
||||
menuTreeObj: [],
|
||||
dataScopeTreeObj: [],
|
||||
apiScopeTreeObj: [],
|
||||
menuLinked: false,
|
||||
dataScopeLinked: false,
|
||||
apiScopeLinked: false,
|
||||
currentRoleIds: [],
|
||||
currentRoleName: '',
|
||||
roleDataScopeMenuTree: [],
|
||||
roleDataScopeMap: {},
|
||||
deptTree: [],
|
||||
dataScopeSetting: {
|
||||
roleName: '',
|
||||
scopeType: 2,
|
||||
menuIds: [],
|
||||
},
|
||||
selectionList: [],
|
||||
query: {},
|
||||
loading: true,
|
||||
@@ -309,32 +340,19 @@ export default {
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '角色别名',
|
||||
labelTip: '用于权限校验与标注的属性,可手动录入也可点击现有别名快速录入',
|
||||
label: '角色标识',
|
||||
labelTip: '用于权限校验与标注的属性',
|
||||
prop: 'roleAlias',
|
||||
search: true,
|
||||
span: 24,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入角色别名',
|
||||
message: '请输入角色标识',
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '现有别名',
|
||||
labelTip: '当前系统已存在的角色别名,点击可快速录入',
|
||||
prop: 'currentAlias',
|
||||
type: 'radio',
|
||||
dicData: [],
|
||||
props: {
|
||||
label: 'roleName',
|
||||
value: 'roleAlias',
|
||||
},
|
||||
hide: true,
|
||||
span: 24,
|
||||
},
|
||||
{
|
||||
label: '角色排序',
|
||||
prop: 'sort',
|
||||
@@ -378,14 +396,6 @@ export default {
|
||||
return ids;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'form.currentAlias'() {
|
||||
const alias = this.form.currentAlias;
|
||||
if (!validatenull(alias)) {
|
||||
this.form.roleAlias = alias;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
initData(roleId) {
|
||||
getRoleTreeById(roleId).then(res => {
|
||||
@@ -452,9 +462,9 @@ export default {
|
||||
},
|
||||
submit() {
|
||||
const menuList = this.$refs.treeMenu.getCheckedKeys();
|
||||
const dataScopeList = this.$refs.treeDataScope.getCheckedKeys();
|
||||
const apiScopeList = this.$refs.treeApiScope.getCheckedKeys();
|
||||
grant(this.currentRoleIds, menuList, dataScopeList, apiScopeList).then(() => {
|
||||
// 数据权限已改为角色菜单配置;传 null 跳过原生 dataScope 更新,避免误清历史配置
|
||||
grant(this.currentRoleIds, menuList, null, apiScopeList).then(() => {
|
||||
this.box = false;
|
||||
this.$message({
|
||||
type: 'success',
|
||||
@@ -465,6 +475,84 @@ export default {
|
||||
this.onLoad(this.page);
|
||||
});
|
||||
},
|
||||
getMenuScopeLabel(menuId) {
|
||||
const scope = this.roleDataScopeMap[String(menuId)];
|
||||
return scope ? scope.scopeTypeName : '';
|
||||
},
|
||||
loadRoleDataScopeTree(roleId) {
|
||||
return getRoleMenuDataScopeTree(roleId).then(res => {
|
||||
const data = res.data.data || {};
|
||||
this.currentRoleName = data.roleName || '';
|
||||
this.roleDataScopeMenuTree = data.menuTree || [];
|
||||
const scopeMap = {};
|
||||
(data.scopes || []).forEach(item => {
|
||||
scopeMap[String(item.menuId)] = item;
|
||||
});
|
||||
this.roleDataScopeMap = scopeMap;
|
||||
});
|
||||
},
|
||||
openDataScopeSetting() {
|
||||
const tree = this.$refs.treeRoleDataScope;
|
||||
if (!tree) {
|
||||
return;
|
||||
}
|
||||
const menuIds = tree.getCheckedKeys();
|
||||
if (!menuIds || menuIds.length === 0) {
|
||||
this.$message.warning('请先勾选要设置的菜单');
|
||||
return;
|
||||
}
|
||||
this.dataScopeSetting = {
|
||||
roleName: this.currentRoleName,
|
||||
scopeType: 2,
|
||||
menuIds,
|
||||
};
|
||||
const loadDept =
|
||||
this.deptTree && this.deptTree.length
|
||||
? Promise.resolve()
|
||||
: getDeptTree(this.userInfo.tenantId).then(res => {
|
||||
this.deptTree = res.data.data || [];
|
||||
});
|
||||
loadDept.then(() => {
|
||||
this.dataScopeSettingBox = true;
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.treeCustomDept) {
|
||||
this.$refs.treeCustomDept.setCheckedKeys([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
submitDataScopeSetting() {
|
||||
const roleId = this.currentRoleIds && this.currentRoleIds[0];
|
||||
if (!roleId) {
|
||||
this.$message.warning('角色信息异常,请重新打开权限配置');
|
||||
return;
|
||||
}
|
||||
const scopeType = Number(this.dataScopeSetting.scopeType || 2);
|
||||
let deptIds = [];
|
||||
if (scopeType === 4) {
|
||||
deptIds = this.$refs.treeCustomDept ? this.$refs.treeCustomDept.getCheckedKeys() : [];
|
||||
if (!deptIds.length) {
|
||||
this.$message.warning('自定义组织时请勾选组织');
|
||||
return;
|
||||
}
|
||||
}
|
||||
submitRoleMenuDataScope({
|
||||
roleId,
|
||||
menuIds: this.dataScopeSetting.menuIds,
|
||||
scopeType,
|
||||
deptIds,
|
||||
}).then(() => {
|
||||
this.$message.success('数据权限设置成功');
|
||||
this.dataScopeSettingBox = false;
|
||||
this.loadRoleDataScopeTree(roleId).then(() => {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.treeRoleDataScope) {
|
||||
this.$refs.treeRoleDataScope.setCheckedKeys([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
rowSave(row, done, loading) {
|
||||
add(this.withTenantId(row)).then(
|
||||
() => {
|
||||
@@ -547,10 +635,6 @@ export default {
|
||||
this.form.parentId = '';
|
||||
}
|
||||
}
|
||||
getRoleAlias().then(res => {
|
||||
const column = this.findColumn(this.option.column, 'currentAlias');
|
||||
column.dicData = res.data.data;
|
||||
});
|
||||
done();
|
||||
},
|
||||
handleRole() {
|
||||
@@ -563,58 +647,52 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.menuTreeObj = [];
|
||||
this.dataScopeTreeObj = [];
|
||||
this.apiScopeTreeObj = [];
|
||||
// 保存当前要配置权限的角色ID
|
||||
this.currentRoleIds = this.idsArray;
|
||||
grantTree().then(res => {
|
||||
this.menuGrantList = res.data.data.menu;
|
||||
this.dataScopeGrantList = res.data.data.dataScope;
|
||||
this.apiScopeGrantList = res.data.data.apiScope;
|
||||
getRole(this.ids).then(res => {
|
||||
this.menuTreeObj = res.data.data.menu;
|
||||
this.dataScopeTreeObj = res.data.data.dataScope;
|
||||
this.apiScopeTreeObj = res.data.data.apiScope;
|
||||
this.box = true;
|
||||
this.$nextTick(() => {
|
||||
this.applyCheckedKeys('treeMenu', this.menuTreeObj);
|
||||
this.applyCheckedKeys('treeDataScope', this.dataScopeTreeObj);
|
||||
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
|
||||
});
|
||||
this.currentRoleName = (this.selectionList[0] && this.selectionList[0].roleName) || '';
|
||||
const roleId = this.currentRoleIds[0];
|
||||
Promise.all([grantTree(), getRole(this.ids), this.loadRoleDataScopeTree(roleId)]).then(results => {
|
||||
const grantRes = results[0];
|
||||
const roleRes = results[1];
|
||||
this.menuGrantList = grantRes.data.data.menu;
|
||||
this.apiScopeGrantList = grantRes.data.data.apiScope;
|
||||
this.menuTreeObj = roleRes.data.data.menu;
|
||||
this.apiScopeTreeObj = roleRes.data.data.apiScope;
|
||||
this.box = true;
|
||||
this.$nextTick(() => {
|
||||
this.applyCheckedKeys('treeMenu', this.menuTreeObj);
|
||||
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
|
||||
});
|
||||
});
|
||||
},
|
||||
handleRowRole(row) {
|
||||
this.menuTreeObj = [];
|
||||
this.dataScopeTreeObj = [];
|
||||
this.apiScopeTreeObj = [];
|
||||
// 保存当前要配置权限的角色ID(单个行操作)
|
||||
this.currentRoleIds = [row.id];
|
||||
grantTree().then(res => {
|
||||
this.menuGrantList = res.data.data.menu;
|
||||
this.dataScopeGrantList = res.data.data.dataScope;
|
||||
this.apiScopeGrantList = res.data.data.apiScope;
|
||||
getRole(row.id).then(res => {
|
||||
this.menuTreeObj = res.data.data.menu;
|
||||
this.dataScopeTreeObj = res.data.data.dataScope;
|
||||
this.apiScopeTreeObj = res.data.data.apiScope;
|
||||
this.box = true;
|
||||
this.$nextTick(() => {
|
||||
this.applyCheckedKeys('treeMenu', this.menuTreeObj);
|
||||
this.applyCheckedKeys('treeDataScope', this.dataScopeTreeObj);
|
||||
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
|
||||
});
|
||||
this.currentRoleName = row.roleName || '';
|
||||
Promise.all([grantTree(), getRole(row.id), this.loadRoleDataScopeTree(row.id)]).then(results => {
|
||||
const grantRes = results[0];
|
||||
const roleRes = results[1];
|
||||
this.menuGrantList = grantRes.data.data.menu;
|
||||
this.apiScopeGrantList = grantRes.data.data.apiScope;
|
||||
this.menuTreeObj = roleRes.data.data.menu;
|
||||
this.apiScopeTreeObj = roleRes.data.data.apiScope;
|
||||
this.box = true;
|
||||
this.$nextTick(() => {
|
||||
this.applyCheckedKeys('treeMenu', this.menuTreeObj);
|
||||
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
|
||||
});
|
||||
});
|
||||
},
|
||||
handleDialogClose() {
|
||||
// 对话框关闭时清空选择状态和权限树数据
|
||||
this.menuTreeObj = [];
|
||||
this.dataScopeTreeObj = [];
|
||||
this.apiScopeTreeObj = [];
|
||||
this.menuLinked = false;
|
||||
this.dataScopeLinked = false;
|
||||
this.apiScopeLinked = false;
|
||||
this.roleDataScopeMenuTree = [];
|
||||
this.roleDataScopeMap = {};
|
||||
this.currentRoleName = '';
|
||||
this.dataScopeSettingBox = false;
|
||||
this.selectionClear();
|
||||
},
|
||||
handleDelete() {
|
||||
@@ -659,3 +737,15 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-data-scope-node {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.role-data-scope-tag {
|
||||
color: #409eff;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 || [];
|
||||
|
||||
@@ -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;
|
||||
@@ -1721,7 +1722,7 @@ export default {
|
||||
.el-form-item--small.el-form-item,
|
||||
.el-form-item--large.el-form-item,
|
||||
.el-form-item--medium.el-form-item {
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -787,6 +787,6 @@ export default {
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
.el-form-item{
|
||||
margin-bottom: 24px !important;
|
||||
margin-bottom: 22px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -205,7 +205,9 @@ export const defaultContractAttachmentTypeOptions = [
|
||||
{ label: '双章归档文件', value: '双章归档文件' },
|
||||
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
|
||||
];
|
||||
// 特批附件归口附件(佐证材料等)区上传:高风险/黑名单客商合同必传,后端按 fileType=特批附件 校验并挂特批面板(与后端 HcsSyncConstant.ATTACHMENT_TYPE_SPEC_EXPLANATION 对齐)
|
||||
export const defaultContractSupportingAttachmentTypeOptions = [
|
||||
{ label: '特批附件', value: '特批附件' },
|
||||
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
|
||||
];
|
||||
/** @deprecated 兼容旧引用,实际选项由业务字典动态加载 */
|
||||
@@ -299,6 +301,8 @@ export const resolveContractAttachmentType = (
|
||||
item => String(item.label) === aliasedFileType || String(item.label) === rawFileType
|
||||
);
|
||||
if (matchedByLabel) return matchedByLabel.value;
|
||||
// 行已带类型但字典未命中(如历史保存值、字典缺失)时保留原值,避免重开页面时被降级为其他文件
|
||||
if (rawFileType) return aliasedFileType;
|
||||
const fileName = normalizeAttachmentTypeText(attachmentName(row));
|
||||
if (!fileName) return otherType;
|
||||
const matched = list
|
||||
@@ -641,7 +645,7 @@ export default {
|
||||
this.contractAttachmentTypeOptions
|
||||
);
|
||||
} else {
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
this.uploadDialogType = this.uploadDialogType || this.defaultAttachmentType;
|
||||
}
|
||||
this.$message.success('上传成功');
|
||||
},
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<section class="contract-manage-form__section contract-manage-form__section--panel hcs-contract-panel">
|
||||
<div class="contract-manage-form__attachment-head">
|
||||
<div class="dialog-section-title">合规信息</div>
|
||||
<div>
|
||||
<el-button
|
||||
v-if="canApplySpec"
|
||||
type="warning"
|
||||
plain
|
||||
:loading="specApplying"
|
||||
@click="openSpecDialog"
|
||||
>
|
||||
发起特批
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="isSpecProcessing"
|
||||
type="primary"
|
||||
plain
|
||||
@click="confirmSpecResult"
|
||||
>
|
||||
确认特批结果
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-descriptions :column="3" class="contract-manage-detail__descriptions">
|
||||
<el-descriptions-item label="合规同步状态">
|
||||
<el-tooltip
|
||||
v-if="hcsSyncMsg"
|
||||
placement="top"
|
||||
:content="hcsSyncMsg"
|
||||
>
|
||||
<el-tag :type="syncTagType" class="status-text">{{ syncLabel }}</el-tag>
|
||||
</el-tooltip>
|
||||
<el-tag v-else :type="syncTagType" class="status-text">{{ syncLabel }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="合规合同ID">{{
|
||||
contract.hcsFdId || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="客商风险">
|
||||
<template v-if="contract.hcsRiskLevel || contract.hcsBlackStatus">
|
||||
<el-tag
|
||||
v-if="isBlacklist"
|
||||
type="danger"
|
||||
class="status-text"
|
||||
>黑名单</el-tag
|
||||
>
|
||||
<span v-if="contract.hcsRiskLevel" class="hcs-contract-panel__risk">{{
|
||||
`风险等级:${contract.hcsRiskLevel}`
|
||||
}}</span>
|
||||
<span v-if="!contract.hcsRiskLevel && !isBlacklist">-</span>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="特批状态">
|
||||
<el-tag v-if="specLabel" :type="specTagType" class="status-text">{{ specLabel }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="特批说明" :span="2">{{
|
||||
contract.hcsSpecMsg || '-'
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-dialog
|
||||
v-model="specDialogVisible"
|
||||
title="发起合同特批"
|
||||
width="640px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
class="hcs-contract-panel__spec-dialog"
|
||||
@closed="resetSpecDialog"
|
||||
>
|
||||
<el-alert
|
||||
title="高风险客商合同需经集团特批后方可签约推送;请上传特批申请材料及内部审批流程附件。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
class="hcs-contract-panel__spec-alert"
|
||||
/>
|
||||
<el-upload
|
||||
drag
|
||||
multiple
|
||||
:show-file-list="false"
|
||||
:http-request="uploadMaterial"
|
||||
:on-error="() => uploadFailed()"
|
||||
class="hcs-contract-panel__spec-upload"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将特批材料拖到此处,或<em>点击上传到合同系统</em></div>
|
||||
</el-upload>
|
||||
<el-table :data="materials" border class="hcs-contract-panel__spec-table">
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column prop="name" label="文件名" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="fdId" label="合规附件ID" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-link type="danger" @click="materials.splice($index, 1)">删除</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button type="primary" :loading="specApplying" @click="submitSpec">确认发起</el-button>
|
||||
<el-button @click="specDialogVisible = false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { UploadFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
applyContractSpec,
|
||||
confirmContractSpec,
|
||||
uploadAttachment,
|
||||
} from '@/api/business/hcs';
|
||||
|
||||
const SYNC_STATUS_OPTIONS = [
|
||||
{ label: '待推送', value: 'unsynced', type: 'info' },
|
||||
{ label: '已推送', value: 'synced', type: 'success' },
|
||||
{ label: '推送失败', value: 'failed', type: 'danger' },
|
||||
];
|
||||
|
||||
const SPEC_STATUS_OPTIONS = [
|
||||
{ label: '待特批', value: 'need', type: 'warning' },
|
||||
{ label: '特批中', value: 'processing', type: 'warning' },
|
||||
{ label: '特批通过', value: 'approved', type: 'success' },
|
||||
{ label: '特批驳回', value: 'rejected', type: 'danger' },
|
||||
];
|
||||
|
||||
export default {
|
||||
name: 'HcsContractPanel',
|
||||
components: { UploadFilled },
|
||||
props: {
|
||||
// 合同详情行(须含 id/hcsSyncStatus/hcsFdId/hcsRiskLevel/hcsBlackStatus/hcsSpecStatus 等)
|
||||
contract: { type: Object, required: true },
|
||||
},
|
||||
emits: ['refresh'],
|
||||
data() {
|
||||
return {
|
||||
specDialogVisible: false,
|
||||
specApplying: false,
|
||||
materials: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
syncLabel() {
|
||||
return SYNC_STATUS_OPTIONS.find(item => item.value === this.contract.hcsSyncStatus)?.label || '-';
|
||||
},
|
||||
syncTagType() {
|
||||
return SYNC_STATUS_OPTIONS.find(item => item.value === this.contract.hcsSyncStatus)?.type || 'info';
|
||||
},
|
||||
hcsSyncMsg() {
|
||||
return this.contract.hcsSyncMsg || '';
|
||||
},
|
||||
isBlacklist() {
|
||||
return String(this.contract.hcsBlackStatus || '').toLowerCase() === 'yes';
|
||||
},
|
||||
specLabel() {
|
||||
return SPEC_STATUS_OPTIONS.find(item => item.value === this.contract.hcsSpecStatus)?.label || '';
|
||||
},
|
||||
specTagType() {
|
||||
return SPEC_STATUS_OPTIONS.find(item => item.value === this.contract.hcsSpecStatus)?.type || 'info';
|
||||
},
|
||||
isSpecProcessing() {
|
||||
return this.contract.hcsSpecStatus === 'processing';
|
||||
},
|
||||
canApplySpec() {
|
||||
return ['need', 'rejected'].includes(this.contract.hcsSpecStatus);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openSpecDialog() {
|
||||
this.specDialogVisible = true;
|
||||
},
|
||||
resetSpecDialog() {
|
||||
this.materials = [];
|
||||
},
|
||||
// 自定义上传:直传合同合规系统(C103-2),成功记录合规附件ID
|
||||
uploadMaterial(options) {
|
||||
const file = options.file;
|
||||
if (file.size / 1024 / 1024 > 50) {
|
||||
this.$message.warning('单文件大小不能超过 50MB');
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('fdFile', file);
|
||||
uploadAttachment(formData)
|
||||
.then(response => {
|
||||
const fdId = response?.data?.data || response?.data;
|
||||
if (!fdId || typeof fdId !== 'string') {
|
||||
this.$message.error(response?.data?.msg || '上传合同系统失败');
|
||||
return;
|
||||
}
|
||||
this.materials.push({ fdId, name: file.name });
|
||||
this.$message.success(`已上传合同系统:${file.name}`);
|
||||
})
|
||||
.catch(error => {
|
||||
this.$message.error(error?.response?.data?.msg || '上传合同系统失败');
|
||||
});
|
||||
},
|
||||
uploadFailed() {
|
||||
this.$message.error('上传合同系统失败');
|
||||
},
|
||||
submitSpec() {
|
||||
if (!this.materials.length) {
|
||||
this.$message.warning('请先上传特批申请材料');
|
||||
return;
|
||||
}
|
||||
this.specApplying = true;
|
||||
applyContractSpec({
|
||||
contractId: this.contract.id,
|
||||
attachments: this.materials.map(item => ({ fdId: item.fdId, name: item.name })),
|
||||
})
|
||||
.then(response => {
|
||||
if (response?.data?.success === false) {
|
||||
this.$message.error(response?.data?.msg || '特批发起失败');
|
||||
return;
|
||||
}
|
||||
this.$message.success(response?.data?.data || '特批发起成功,等待集团审批');
|
||||
this.specDialogVisible = false;
|
||||
this.$emit('refresh');
|
||||
})
|
||||
.catch(error => {
|
||||
this.$message.error(error?.response?.data?.msg || '特批发起失败');
|
||||
})
|
||||
.finally(() => {
|
||||
this.specApplying = false;
|
||||
});
|
||||
},
|
||||
confirmSpecResult() {
|
||||
this.$prompt('请输入集团特批结果(approved=通过 / rejected=驳回)', '确认特批结果', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /^(approved|rejected)$/,
|
||||
inputErrorMessage: '请输入 approved 或 rejected',
|
||||
}).then(({ value }) => {
|
||||
confirmContractSpec(this.contract.id, value).then(() => {
|
||||
this.$message.success('特批结果已确认');
|
||||
this.$emit('refresh');
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hcs-contract-panel__risk {
|
||||
margin-left: 8px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.hcs-contract-panel__spec-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.hcs-contract-panel__spec-upload {
|
||||
margin-bottom: 12px;
|
||||
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.hcs-contract-panel__spec-table {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -59,7 +59,7 @@
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="route-map"><template v-for="(route, index) in segments" :key="route.segmentNo"><div class="route-map__node"><span :class="['route-badge', index ? 'middle' : 'start']">{{ index ? '经' : '起' }}</span><strong>{{ segmentLocationName(route, 'departure') }}</strong><small>{{ route.departureAddress || '-' }}</small><small class="route-map__progress">{{ index ? `已到达 ${quantity(segments[index - 1].arrivedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(segments[index - 1])}` : `已调度 ${quantity(route.dispatchedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(route)}` }}</small><small v-if="index" class="route-map__progress">已调度 {{ quantity(route.dispatchedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div><div class="route-map__line"></div><div v-if="index === segments.length - 1" class="route-map__node"><span class="route-badge end">终</span><strong>{{ segmentLocationName(route, 'arrival') }}</strong><small>{{ route.arrivalAddress || '-' }}</small><small class="route-map__progress">已到达 {{ quantity(route.arrivedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div></template></div>
|
||||
<div class="route-map"><template v-for="(route, index) in segments" :key="route.segmentNo"><div class="route-map__node"><span :class="['route-badge', index ? 'middle' : 'start']">{{ index ? '经' : '起' }}</span><strong>{{ segmentLocationName(route, 'departure') }}</strong><small>{{ formatRegionAddress(route.departureAddress) || '-' }}</small><small class="route-map__progress">{{ index ? `已到达 ${quantity(segments[index - 1].arrivedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(segments[index - 1])}` : `已调度 ${quantity(route.dispatchedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(route)}` }}</small><small v-if="index" class="route-map__progress">已调度 {{ quantity(route.dispatchedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div><div class="route-map__line"></div><div v-if="index === segments.length - 1" class="route-map__node"><span class="route-badge end">终</span><strong>{{ segmentLocationName(route, 'arrival') }}</strong><small>{{ formatRegionAddress(route.arrivalAddress) || '-' }}</small><small class="route-map__progress">已到达 {{ quantity(route.arrivedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div></template></div>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="documentPreviewVisible" :title="previewFile.name || '附件预览'" append-to-body destroy-on-close width="90%" top="4vh">
|
||||
@@ -110,6 +110,7 @@ import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archiv
|
||||
import { CaretBottom, CaretTop } from '@element-plus/icons-vue';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
import { formatRegionAddress } from '@/utils/region-address';
|
||||
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
|
||||
import '@open-file-viewer/core/style.css';
|
||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||
@@ -164,6 +165,7 @@ export default {
|
||||
} finally { this.loading = false; }
|
||||
},
|
||||
methods: {
|
||||
formatRegionAddress,
|
||||
quantity(value) { const number = Number(value || 0); return Number.isInteger(number) ? number : number.toFixed(3).replace(/\.?0+$/, ''); },
|
||||
segmentNumber(route, index) { return String(route.segmentNo || '').replace(/^段/, '') || index + 1; },
|
||||
isSegmentExpanded(segmentNo) { return Boolean(this.expandedSegments[segmentNo]); },
|
||||
@@ -292,8 +294,8 @@ export default {
|
||||
query: { id: String(customerId), name: '查看客商档案', view: '1' },
|
||||
});
|
||||
},
|
||||
waybillStatusName(value) { return ({ pending: '待执行', waiting: '待执行', running: '进行中', processing: '进行中', completed: '已完成', cancelled: '已取消' })[value] || value || '-'; },
|
||||
waybillStatusType(value) { return ({ pending: 'info', waiting: 'info', running: 'warning', processing: 'warning', completed: 'success', cancelled: 'danger' })[value] || 'info'; },
|
||||
waybillStatusName(value) { return ({ pending: '待执行', waiting: '待执行', running: '进行中', processing: '进行中', pending_return_confirm: '待回单确认', completed: '已完成', cancelled: '已取消' })[value] || value || '-'; },
|
||||
waybillStatusType(value) { return ({ pending: 'info', waiting: 'info', running: 'warning', processing: 'warning', pending_return_confirm: 'warning', completed: 'success', cancelled: 'danger' })[value] || 'info'; },
|
||||
statusName(value) { return ({ waiting_dispatch: '待调度', dispatching: '调度中', completed: '调度完成', closed: '调度关闭' })[value] || value || '-'; },
|
||||
statusType(value) { return ({ waiting_dispatch: 'warning', dispatching: 'success', completed: 'primary', closed: 'danger' })[value] || 'info'; },
|
||||
},
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
<div class="route-overview__node">
|
||||
<span :class="['route-badge', index === 0 ? 'start' : 'middle']">{{ index === 0 ? '起' : '经' }}</span>
|
||||
<el-tooltip :content="route.departureName || '-'" placement="top" :show-after="120">
|
||||
<strong>{{ route.departureName || '-' }}</strong>
|
||||
<strong>{{ dispatchLocationName(route, 'departure') }}</strong>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="route.departureAddress || '-'" placement="top" :show-after="120">
|
||||
<small>{{ route.departureAddress || '-' }}</small>
|
||||
<small>{{ formatRegionAddress(route.departureAddress) || '-' }}</small>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="contactText(route.departureContact, route.departurePhone)" placement="top" :show-after="120">
|
||||
<small>{{ contactText(route.departureContact, route.departurePhone) }}</small>
|
||||
@@ -34,10 +34,10 @@
|
||||
<div v-if="index === routes.length - 1" class="route-overview__node">
|
||||
<span class="route-badge end">终</span>
|
||||
<el-tooltip :content="route.arrivalName || '-'" placement="top" :show-after="120">
|
||||
<strong>{{ route.arrivalName || '-' }}</strong>
|
||||
<strong>{{ dispatchLocationName(route, 'arrival') }}</strong>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="route.arrivalAddress || '-'" placement="top" :show-after="120">
|
||||
<small>{{ route.arrivalAddress || '-' }}</small>
|
||||
<small>{{ formatRegionAddress(route.arrivalAddress) || '-' }}</small>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="contactText(route.arrivalContact, route.arrivalPhone)" placement="top" :show-after="120">
|
||||
<small>{{ contactText(route.arrivalContact, route.arrivalPhone) }}</small>
|
||||
@@ -210,7 +210,11 @@
|
||||
@mousedown="() => handleDriverFocus(route)"
|
||||
@focus="() => handleDriverFocus(route)"
|
||||
@select="handleDriverSuggestionSelect"
|
||||
/></el-form-item
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete></el-form-item
|
||||
></el-col>
|
||||
<el-col :span="6"
|
||||
><el-form-item
|
||||
@@ -239,7 +243,11 @@
|
||||
@mousedown="() => handleDriverFocus(route)"
|
||||
@focus="() => handleDriverFocus(route)"
|
||||
@select="handleEscortSuggestionSelect"
|
||||
/></el-form-item
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete></el-form-item
|
||||
></el-col>
|
||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"
|
||||
><el-form-item label="押运人手机号"
|
||||
@@ -316,7 +324,7 @@
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/business/master-order';
|
||||
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
|
||||
import { fetchDriverCandidates, driverOptionText } from '@/utils/carrier-org-resource';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import {
|
||||
currencyUnit,
|
||||
@@ -324,6 +332,7 @@ import {
|
||||
resolvePriceUnitByQuantityUnit,
|
||||
} from '@/utils/currency';
|
||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||
import { formatRegionAddress } from '@/utils/region-address';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
|
||||
const unwrapRecords = res => {
|
||||
@@ -402,6 +411,7 @@ export default {
|
||||
this.applySelfOperatedCarrierDefaults();
|
||||
},
|
||||
methods: {
|
||||
driverOptionText,
|
||||
loadQuantityUnitOptions() {
|
||||
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
|
||||
this.quantityUnitLoading = true;
|
||||
@@ -851,7 +861,12 @@ export default {
|
||||
const mileage = Number(value);
|
||||
return Number.isFinite(mileage) && mileage >= 0 ? String(Math.trunc(mileage)) : '';
|
||||
},
|
||||
addressText(name, address) { return [name, address].filter(Boolean).join(' / ') || '-'; },
|
||||
formatRegionAddress,
|
||||
addressText(name, address) {
|
||||
// 名称与详细地址都要按「市 区县」展示;两者格式化后相同时只展示一次
|
||||
const values = [this.formatRegionAddress(name), this.formatRegionAddress(address)].filter(Boolean);
|
||||
return [...new Set(values)].join(' / ') || '-';
|
||||
},
|
||||
contactText(name, phone) { return [name, phone].filter(Boolean).join(' - ') || '-'; },
|
||||
dateStartLabel(route) { return route.documentType === '计划单' ? '计划开始日期' : '预计发货日期'; },
|
||||
dateEndLabel(route) { return route.documentType === '计划单' ? '计划结束日期' : '预计完成日期'; },
|
||||
@@ -938,12 +953,6 @@ export default {
|
||||
route.escortPhone = '';
|
||||
this.driverOptions = [];
|
||||
},
|
||||
getRouteCarrierFilter(route = {}) {
|
||||
return {
|
||||
carrierId: route.carrierId || '',
|
||||
carrierName: route.carrierName || '',
|
||||
};
|
||||
},
|
||||
carrierOptionValue(item = {}) {
|
||||
return item.contractId || item.id || item.value || item.carrierName || '';
|
||||
},
|
||||
@@ -1020,36 +1029,22 @@ export default {
|
||||
this.activeSuggestRoute = route;
|
||||
},
|
||||
async loadDriverOptions(route = {}, keyword = '') {
|
||||
const carrier = this.getRouteCarrierFilter(route);
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
this.driverOptions = [];
|
||||
return [];
|
||||
}
|
||||
this.driverLoading = true;
|
||||
try {
|
||||
this.driverOptions = await fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
);
|
||||
this.driverOptions = await fetchDriverCandidates({
|
||||
size: 20,
|
||||
...(keyword ? { driverName: keyword } : {}),
|
||||
posts: '司机',
|
||||
});
|
||||
return this.driverOptions;
|
||||
} finally {
|
||||
this.driverLoading = false;
|
||||
}
|
||||
},
|
||||
fetchDriverSuggestions(queryString, callback) {
|
||||
const route = this.activeSuggestRoute || {};
|
||||
const carrier = this.getRouteCarrierFilter(route);
|
||||
const keyword = String(queryString || '').trim();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
if (!keyword) this.$message.warning('请先选择承运商');
|
||||
return;
|
||||
}
|
||||
this.driverLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
|
||||
.then(records => {
|
||||
this.driverOptions = records;
|
||||
callback(
|
||||
@@ -1074,19 +1069,13 @@ export default {
|
||||
if (drivingVehicle) route.vehicleNo = drivingVehicle;
|
||||
},
|
||||
fetchEscortSuggestions(queryString, callback) {
|
||||
const route = this.activeSuggestRoute || {};
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getRouteCarrierFilter(route);
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
if (!keyword) this.$message.warning('请先选择承运商');
|
||||
return;
|
||||
}
|
||||
this.escortLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({
|
||||
size: 20,
|
||||
...(keyword ? { driverName: keyword } : {}),
|
||||
posts: '押运员',
|
||||
})
|
||||
.then(records => {
|
||||
callback(
|
||||
records.map(item => ({
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="途经地" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly || !route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||
><el-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="isBasicRouteReadonly" placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly" placeholder="请输入详细地址" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人" class="route-contact-form-item"><el-input v-model="route.departureContact" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
|
||||
<el-form-item label="联系方式" class="route-phone-form-item"><div class="route-contact-actions"><el-input v-model="route.departurePhone" :disabled="isBasicRouteReadonly" placeholder="请输入" /><template v-if="!isBasicRouteReadonly"><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></template></div></el-form-item>
|
||||
@@ -111,7 +111,7 @@
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="收货地" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly || !form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||
><el-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="isBasicRouteReadonly" placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly" placeholder="请输入详细地址" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人" class="route-contact-form-item"
|
||||
><el-input v-model="form.arrivalContact" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
|
||||
@@ -714,7 +714,7 @@ export default {
|
||||
'msg',
|
||||
'zip',
|
||||
],
|
||||
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
|
||||
transports: ['铁路运输', '水路运输', '航空运输', '公路运输'],
|
||||
quantityUnitOptions: [],
|
||||
quantityUnitLoading: false,
|
||||
form: {
|
||||
@@ -798,6 +798,7 @@ export default {
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
defaultQuantityUnit() {
|
||||
if (this.quantityUnitOptions.includes('吨')) return '吨';
|
||||
return this.quantityUnitOptions[0] || '吨';
|
||||
},
|
||||
loadQuantityUnitOptions() {
|
||||
@@ -1164,22 +1165,30 @@ export default {
|
||||
const field = this.addressModel(target);
|
||||
if (!field) return false;
|
||||
const siteCode = String(field.model[field.siteCode] || '').trim();
|
||||
// 地址展示模式由实际选择的数据决定:有站点编码表示非公路站点名称,
|
||||
// 没有站点编码则保留省市区级联,避免切换运输方式时改写已有地址。
|
||||
// 展示模式由已选数据决定:有站点编码则显示站点名,否则省市区;暂不按运输方式强制站点限制。
|
||||
return Boolean(siteCode && siteCode !== '/');
|
||||
},
|
||||
hasAddressValue(target) {
|
||||
const field = this.addressModel(target);
|
||||
if (!field) return false;
|
||||
return Boolean(
|
||||
String(field.model[field.address] || '').trim() || String(field.model[field.name] || '').trim()
|
||||
);
|
||||
},
|
||||
tipEmptyAddressDetail(target) {
|
||||
// 替代原「请先选择运输方式」placeholder:空地址点击时 toast,已有值不再提示。
|
||||
if (!this.hasAddressValue(target)) {
|
||||
this.$message.warning('请输入详细地址');
|
||||
}
|
||||
},
|
||||
resolveAddressRegionCode(row = {}) {
|
||||
return row.regionCode || row.districtCode || row.areaCode || row.adcode || '';
|
||||
},
|
||||
openCommonAddress(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
this.tipEmptyAddressDetail(target);
|
||||
this.addressTarget = target;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.stationQuery = {};
|
||||
this.stationPage.current = 1;
|
||||
this.stationDialogVisible = true;
|
||||
return;
|
||||
}
|
||||
// 暂不按运输方式限制站点:统一走常用地址选择。
|
||||
this.addressQuery = {};
|
||||
this.addressPage.current = 1;
|
||||
this.addressDialogVisible = true;
|
||||
@@ -1279,18 +1288,11 @@ export default {
|
||||
},
|
||||
handleAddressDetailClick(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.openCommonAddress(target);
|
||||
return;
|
||||
}
|
||||
this.tipEmptyAddressDetail(target);
|
||||
this.openAddressMap(target);
|
||||
},
|
||||
openAddressMap(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.$message.info('非公路运输地址不能使用地图选择');
|
||||
return;
|
||||
}
|
||||
const field = this.addressModel(target);
|
||||
if (!field) return;
|
||||
this.mapTarget = target;
|
||||
@@ -1690,7 +1692,11 @@ export default {
|
||||
'',
|
||||
cargoTypePath,
|
||||
quantity: row.quantity || '',
|
||||
quantityUnit: row.quantityUnit || this.defaultQuantityUnit(),
|
||||
// 常用货物单位若为空或「个」,新建总单统一默认「吨」
|
||||
quantityUnit:
|
||||
!row.quantityUnit || String(row.quantityUnit).trim() === '个'
|
||||
? this.defaultQuantityUnit()
|
||||
: row.quantityUnit,
|
||||
packageType: row.packageType || row.package || '',
|
||||
brand: row.brand || '',
|
||||
specification: row.specification || row.spec || '',
|
||||
@@ -1775,17 +1781,21 @@ export default {
|
||||
return formatUpdateUserName(row);
|
||||
},
|
||||
selectRoute(route) {
|
||||
// 先写入运输方式(会触发地点清空),再回填线路地址,避免被清空逻辑覆盖。
|
||||
this.form.finalTransportType = '公路运输';
|
||||
if (!this.form.routes.length) this.addRoute();
|
||||
if (this.form.routes[0]) this.form.routes[0].transportType = '公路运输';
|
||||
this.form.departureName = route.departureName || '';
|
||||
this.form.departureAddress = route.departureAddress || '';
|
||||
this.form.departureContact = route.departureContact || '';
|
||||
this.form.departurePhone = route.departurePhone || '';
|
||||
this.form.departureSiteCode = '';
|
||||
this.form.arrivalName = route.arrivalName || '';
|
||||
this.form.arrivalAddress = route.arrivalAddress || '';
|
||||
this.form.arrivalContact = route.arrivalContact || '';
|
||||
this.form.arrivalPhone = route.arrivalPhone || '';
|
||||
this.form.arrivalSiteCode = '';
|
||||
this.syncAddressRegionPaths();
|
||||
this.form.finalTransportType = '公路运输';
|
||||
if (!this.form.routes.length) this.addRoute();
|
||||
this.routeDialogVisible = false;
|
||||
},
|
||||
createRoute(index = this.form.routes.length) {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
:class="{ 'punch-record-photos__grid--edit': editable }"
|
||||
>
|
||||
<div
|
||||
v-for="(item, groupIndex) in group.items"
|
||||
v-for="item in group.items"
|
||||
:key="`${item.photo.url}-${item.index}`"
|
||||
class="punch-record-photos__item"
|
||||
@click="handlePreview(item.index)"
|
||||
@@ -23,8 +23,6 @@
|
||||
:lazy="lazy"
|
||||
fit="cover"
|
||||
/>
|
||||
<!-- 主图 = 该类型的第一张,按上传顺序取;没有独立字段,也不可切换 -->
|
||||
<div v-if="groupIndex === 0" class="punch-record-photos__badge">主图</div>
|
||||
<el-link
|
||||
v-if="editable"
|
||||
class="punch-record-photos__remove"
|
||||
@@ -59,7 +57,7 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
// 打卡照片是每个打卡节点必有的一个照片类型,不由过程配置的凭证类型决定;
|
||||
// 它与各凭证类型各自最多 9 张,同一类型下第一张即界面上的「主图」。
|
||||
// 它与各凭证类型各自最多 9 张。
|
||||
const PUNCH_PHOTO_TYPE = '打卡照片';
|
||||
// 照片没带类型时的兜底名称,与后端落库时的兜底口径一致
|
||||
const PHOTO_TYPE_FALLBACK = '凭证';
|
||||
@@ -252,18 +250,6 @@ const countText = group =>
|
||||
}
|
||||
}
|
||||
|
||||
&__badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0 6px;
|
||||
border-radius: 4px 0 4px 0;
|
||||
background: rgba(64, 158, 255, 0.9);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&__remove {
|
||||
align-self: center;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -153,11 +153,24 @@
|
||||
@click="handleTransportSecondaryAddressInputClick('departure')"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon @click.stop="openTransportMapDialog('departure')">
|
||||
<el-icon
|
||||
class="shipping-template-page__map-suffix"
|
||||
@click.stop="openTransportMapDialog('departure')"
|
||||
>
|
||||
<Location />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-tooltip content="选择常用地址" placement="top">
|
||||
<el-button
|
||||
class="shipping-template-page__address-pick-btn"
|
||||
type="primary"
|
||||
:icon="OfficeBuilding"
|
||||
link
|
||||
:disabled="transportAddressSelectDisabled"
|
||||
@click="openTransportAddressPicker('departure')"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<span>联系人</span>
|
||||
<el-input
|
||||
v-model="form.departureContact"
|
||||
@@ -166,16 +179,6 @@
|
||||
/>
|
||||
<span>联系方式</span>
|
||||
<el-input v-model="form.departurePhone" placeholder="请输入" :disabled="dialogReadonly" />
|
||||
<el-tooltip content="选择常用地址" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:disabled="transportAddressSelectDisabled"
|
||||
@click="openTransportAddressPicker('departure')"
|
||||
>
|
||||
选择
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -197,25 +200,28 @@
|
||||
@click="handleTransportSecondaryAddressInputClick('arrival')"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon @click.stop="openTransportMapDialog('arrival')">
|
||||
<el-icon
|
||||
class="shipping-template-page__map-suffix"
|
||||
@click.stop="openTransportMapDialog('arrival')"
|
||||
>
|
||||
<Location />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-tooltip content="选择常用地址" placement="top">
|
||||
<el-button
|
||||
class="shipping-template-page__address-pick-btn"
|
||||
type="primary"
|
||||
:icon="OfficeBuilding"
|
||||
link
|
||||
:disabled="transportAddressSelectDisabled"
|
||||
@click="openTransportAddressPicker('arrival')"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<span>联系人</span>
|
||||
<el-input v-model="form.arrivalContact" placeholder="请输入" :disabled="dialogReadonly" />
|
||||
<span>联系方式</span>
|
||||
<el-input v-model="form.arrivalPhone" placeholder="请输入" :disabled="dialogReadonly" />
|
||||
<el-tooltip content="选择常用地址" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:disabled="transportAddressSelectDisabled"
|
||||
@click="openTransportAddressPicker('arrival')"
|
||||
>
|
||||
选择
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -382,7 +388,7 @@
|
||||
:disabled="dialogReadonly"
|
||||
@visible-change="visible => visible && loadFeeUnitOptions()"
|
||||
><el-option
|
||||
v-for="unit in feeUnitOptions"
|
||||
v-for="unit in feeUnitSelectOptions"
|
||||
:key="unit.value"
|
||||
:label="unit.label"
|
||||
:value="unit.value" /></el-select
|
||||
@@ -1118,11 +1124,15 @@ import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { packageOptions } from '@/option/business/common';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import {
|
||||
mapFeeUnitOptions,
|
||||
resolvePriceUnitByQuantityUnit,
|
||||
} from '@/utils/currency';
|
||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
import { Location } from '@element-plus/icons-vue';
|
||||
import { Location, OfficeBuilding } from '@element-plus/icons-vue';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
import {
|
||||
@@ -1258,6 +1268,10 @@ const PageAvueForm = {
|
||||
},
|
||||
};
|
||||
|
||||
const shippingTemplateListRoute = '/business/shipping-template';
|
||||
const shippingTemplateFormRoute = '/business/shipping-template/form';
|
||||
const shippingTemplateListRefreshKey = 'shipping-template-list-refresh';
|
||||
|
||||
export default {
|
||||
name: 'ShippingTemplatePage',
|
||||
components: { PageAvueForm, ElImageViewer, OpenFileViewer },
|
||||
@@ -1273,6 +1287,10 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
Location,
|
||||
OfficeBuilding,
|
||||
// 实例所属路由快照,避免 keep-alive 列表实例被全局 $route 误判成表单页。
|
||||
routePathLocked: this.$route.path,
|
||||
isFormPageInstance: this.$route.path === shippingTemplateFormRoute,
|
||||
form: {},
|
||||
query: {},
|
||||
loading: true,
|
||||
@@ -1394,13 +1412,18 @@ export default {
|
||||
isStandaloneBusinessPage() {
|
||||
return (
|
||||
this.standaloneFormPage &&
|
||||
['/business/shipping-template', '/business/shipping-template/form'].includes(
|
||||
this.$route.path
|
||||
)
|
||||
(this.isFormPageInstance ||
|
||||
[shippingTemplateListRoute, shippingTemplateFormRoute].includes(this.routePathLocked) ||
|
||||
[shippingTemplateListRoute, shippingTemplateFormRoute].includes(this.$route.path))
|
||||
);
|
||||
},
|
||||
isStandaloneFormPage() {
|
||||
return this.isStandaloneBusinessPage && ['add', 'edit'].includes(this.$route.query.mode);
|
||||
// 用实例锁定标志,避免列表 keep-alive 实例被全局路由误判成表单页。
|
||||
return (
|
||||
this.standaloneFormPage &&
|
||||
this.isFormPageInstance &&
|
||||
['add', 'edit'].includes(this.$route.query.mode)
|
||||
);
|
||||
},
|
||||
formPageTitle() {
|
||||
return this.isStandaloneFormPage
|
||||
@@ -1457,6 +1480,12 @@ export default {
|
||||
);
|
||||
return this.currencyUnit(option?.label || this.shippingTemplateFreight.currency);
|
||||
},
|
||||
feeUnitSelectOptions() {
|
||||
return mapFeeUnitOptions(
|
||||
this.feeUnitOptions,
|
||||
this.shippingTemplateFreight.currency || 'RMB'
|
||||
);
|
||||
},
|
||||
roadFreightTotal() {
|
||||
const total = this.shippingTemplateFreight.freightItems.reduce(
|
||||
(sum, item) => sum + Number(this.freightAmount(item) || 0),
|
||||
@@ -1523,6 +1552,18 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
// 列表实例:从其他页进入列表时刷新(activated 负责标签切回)。
|
||||
if (!this.isFormPageInstance) {
|
||||
const enteringList =
|
||||
(to.path === this.routePathLocked || to.path === shippingTemplateListRoute) &&
|
||||
from?.path &&
|
||||
from.path !== to.path;
|
||||
if (enteringList) {
|
||||
sessionStorage.removeItem(shippingTemplateListRefreshKey);
|
||||
this.$nextTick(() => this.onLoad(this.page, this.query));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.isStandaloneFormPage &&
|
||||
(to.query.mode !== from?.query?.mode ||
|
||||
@@ -1543,6 +1584,10 @@ export default {
|
||||
this.handleTransportTypeChange(value);
|
||||
this.syncFreightItems();
|
||||
},
|
||||
'form.templateType'() {
|
||||
// 切换到运单模板时立即按货物数量单位生成公路运费明细。
|
||||
this.$nextTick(() => this.syncFreightItems());
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadProjectOptions();
|
||||
@@ -1552,8 +1597,16 @@ export default {
|
||||
this.loadCurrencyOptions();
|
||||
if (this.isStandaloneFormPage) this.$nextTick(() => this.initStandaloneFormPage());
|
||||
},
|
||||
activated() {
|
||||
if (this.isFormPageInstance) return;
|
||||
sessionStorage.removeItem(shippingTemplateListRefreshKey);
|
||||
this.$nextTick(() => this.onLoad(this.page, this.query));
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
markShippingTemplateListNeedRefresh() {
|
||||
sessionStorage.setItem(shippingTemplateListRefreshKey, '1');
|
||||
},
|
||||
loadQuantityUnitOptions() {
|
||||
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
|
||||
this.quantityUnitLoading = true;
|
||||
@@ -1584,12 +1637,8 @@ export default {
|
||||
});
|
||||
}
|
||||
if (this.menuWidth != null) next.menuWidth = this.menuWidth;
|
||||
if (
|
||||
this.standaloneFormPage &&
|
||||
['/business/shipping-template', '/business/shipping-template/form'].includes(
|
||||
this.$route.path
|
||||
)
|
||||
) {
|
||||
// 仅表单页实例套用全屏表单弹窗样式,避免列表页误用后表格区域异常。
|
||||
if (this.standaloneFormPage && this.$route.path === shippingTemplateFormRoute) {
|
||||
next.dialogAppendToBody = false;
|
||||
next.dialogModal = false;
|
||||
next.dialogCustomClass = 'shipping-template-form-page-dialog';
|
||||
@@ -1769,13 +1818,22 @@ export default {
|
||||
this.onLoad(this.page, this.query);
|
||||
},
|
||||
onLoad(page, params = {}) {
|
||||
if (this.isFormPageInstance) return;
|
||||
this.loading = true;
|
||||
const currentPage = page?.currentPage || this.page.currentPage || 1;
|
||||
const pageSize = page?.pageSize || this.page.pageSize || 10;
|
||||
this.api
|
||||
.getList(page.currentPage, page.pageSize, { ...this.query, ...params })
|
||||
.getList(currentPage, pageSize, { ...this.query, ...params })
|
||||
.then(res => {
|
||||
const data = res?.data?.data || {};
|
||||
this.data = extractRecords(res);
|
||||
this.page.total = data.total || this.data.length;
|
||||
const data = res?.data?.data || res?.data || {};
|
||||
const records = extractRecords(res);
|
||||
this.data = records;
|
||||
this.page.total = Number(data.total ?? records.length) || 0;
|
||||
})
|
||||
.catch(error => {
|
||||
window.console.error(error);
|
||||
this.data = [];
|
||||
this.page.total = 0;
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
@@ -1814,9 +1872,10 @@ export default {
|
||||
.submit(submit)
|
||||
.then(() => {
|
||||
this.$message.success('操作成功');
|
||||
this.markShippingTemplateListNeedRefresh();
|
||||
done?.();
|
||||
this.closeCrudDialog();
|
||||
this.onLoad(this.page, this.query);
|
||||
if (!this.isFormPageInstance) this.onLoad(this.page, this.query);
|
||||
})
|
||||
.catch(() => loading?.());
|
||||
},
|
||||
@@ -1830,9 +1889,10 @@ export default {
|
||||
.submit(submit)
|
||||
.then(() => {
|
||||
this.$message.success('操作成功');
|
||||
this.markShippingTemplateListNeedRefresh();
|
||||
done?.();
|
||||
this.closeCrudDialog();
|
||||
this.onLoad(this.page, this.query);
|
||||
if (!this.isFormPageInstance) this.onLoad(this.page, this.query);
|
||||
})
|
||||
.catch(() => loading?.());
|
||||
},
|
||||
@@ -1876,6 +1936,8 @@ export default {
|
||||
this.attachmentRows = [];
|
||||
this.shippingTemplateFreight = defaultFreight();
|
||||
this.loadCurrencyOptions();
|
||||
this.loadFeeUnitOptions();
|
||||
this.$nextTick(() => this.syncFreightItems());
|
||||
if (typeof this.api.nextCode === 'function')
|
||||
this.api
|
||||
.nextCode()
|
||||
@@ -1998,20 +2060,47 @@ export default {
|
||||
return false;
|
||||
}
|
||||
if (this.transportMode === 'road') {
|
||||
const invalid = this.shippingTemplateFreight.freightItems.findIndex(
|
||||
item =>
|
||||
Number(item.unitPrice || 0) < 0 ||
|
||||
!item.priceUnit ||
|
||||
(!this.isFreightAutoCalculable(item) &&
|
||||
(item.freightAmount === '' ||
|
||||
item.freightAmount === null ||
|
||||
item.freightAmount === undefined)) ||
|
||||
Number(item.freightAmount || 0) < 0
|
||||
);
|
||||
if (invalid > -1) {
|
||||
this.$message.warning(`第${invalid + 1}条货物的单价、计价单位或运费无效`);
|
||||
if (!this.shippingTemplateFreight.freightItems.length) {
|
||||
this.$message.warning('请先完善货物数量单位,以便生成运费明细');
|
||||
return false;
|
||||
}
|
||||
for (const [index, item] of this.shippingTemplateFreight.freightItems.entries()) {
|
||||
const label = `第${index + 1}条运费明细`;
|
||||
if (
|
||||
item.unitPrice === '' ||
|
||||
item.unitPrice === null ||
|
||||
item.unitPrice === undefined
|
||||
) {
|
||||
this.$message.warning(`请填写${label}的单价`);
|
||||
return false;
|
||||
}
|
||||
if (Number(item.unitPrice) < 0) {
|
||||
this.$message.warning(`${label}的单价不能小于0`);
|
||||
return false;
|
||||
}
|
||||
if (!String(item.priceUnit || '').trim()) {
|
||||
this.$message.warning(`请选择${label}的计价单位`);
|
||||
return false;
|
||||
}
|
||||
if (this.isFreightAutoCalculable(item)) {
|
||||
if (!item.freightAmountManual) {
|
||||
item.freightAmount = this.freightAmount(item);
|
||||
}
|
||||
} else if (
|
||||
item.freightAmount === '' ||
|
||||
item.freightAmount === null ||
|
||||
item.freightAmount === undefined
|
||||
) {
|
||||
this.$message.warning(
|
||||
`${label}的计价单位与货物数量单位不匹配,无法自动计算运费,请手动填写运费`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (Number(item.freightAmount || 0) < 0) {
|
||||
this.$message.warning(`${label}的运费不能小于0`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (Number(this.shippingTemplateFreight.freightAmount || 0) < 0) {
|
||||
this.$message.warning('运费不能小于0');
|
||||
return false;
|
||||
@@ -2026,8 +2115,11 @@ export default {
|
||||
return true;
|
||||
},
|
||||
closeCrudDialog() {
|
||||
if (this.isStandaloneBusinessPage) {
|
||||
this.$router.push({ path: '/business/shipping-template', query: {} });
|
||||
if (this.isStandaloneBusinessPage || this.isFormPageInstance) {
|
||||
// 关闭独立表单页时标记列表需刷新(含直接点关闭未保存的场景)
|
||||
this.markShippingTemplateListNeedRefresh();
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push({ path: shippingTemplateListRoute, query: {} });
|
||||
return;
|
||||
}
|
||||
this.$refs.crud?.closeDialog?.();
|
||||
@@ -2287,7 +2379,16 @@ export default {
|
||||
},
|
||||
loadFeeUnitOptions() {
|
||||
getDictionary({ code: 'unit_fee' }).then(res => {
|
||||
this.feeUnitOptions = this.normalizeDictOptions(res?.data?.data || []);
|
||||
// 计价单位必须保存可读文案(如 元/吨),才能与货物数量单位匹配并自动计算运费。
|
||||
this.feeUnitOptions = (res?.data?.data || []).map(item => {
|
||||
const label = String(item.dictValue || item.label || item.value || '').trim();
|
||||
const key = String(item.dictKey || item.value || '').trim();
|
||||
return {
|
||||
label: label || key,
|
||||
value: label.includes('/') ? label : key || label,
|
||||
remark: item.remark || '',
|
||||
};
|
||||
});
|
||||
});
|
||||
},
|
||||
loadCurrencyOptions() {
|
||||
@@ -3091,24 +3192,53 @@ export default {
|
||||
: [];
|
||||
const quantityUnits = [];
|
||||
this.transportCargoRows.forEach(row => {
|
||||
const quantityUnit = row.quantityUnit || '';
|
||||
const quantityUnit = String(row.quantityUnit || '').trim();
|
||||
if (!quantityUnit) return;
|
||||
if (!quantityUnits.some(unit => unit === quantityUnit)) quantityUnits.push(quantityUnit);
|
||||
});
|
||||
const currency = this.shippingTemplateFreight.currency || 'RMB';
|
||||
this.shippingTemplateFreight.freightItems = quantityUnits.map((quantityUnit, index) => {
|
||||
const matched =
|
||||
previousItems.find(item => String(item.quantityUnit || '') === String(quantityUnit)) ||
|
||||
previousItems[index] ||
|
||||
{};
|
||||
const defaultPriceUnit = resolvePriceUnitByQuantityUnit(quantityUnit, currency);
|
||||
const priceUnit = this.normalizeFreightPriceUnit(
|
||||
matched.priceUnit || defaultPriceUnit || '',
|
||||
quantityUnit,
|
||||
currency
|
||||
);
|
||||
return {
|
||||
quantityUnit,
|
||||
unitPrice: matched.unitPrice || '',
|
||||
priceUnit: matched.priceUnit || '',
|
||||
priceUnit,
|
||||
freightAmount: matched.freightAmount ?? '',
|
||||
freightAmountManual:
|
||||
matched.freightAmountManual === true || matched.manualFreightAmount === true,
|
||||
};
|
||||
});
|
||||
},
|
||||
normalizeFreightPriceUnit(priceUnit, quantityUnit, currency = 'RMB') {
|
||||
const text = this.resolvePriceUnitText(priceUnit);
|
||||
if (text && this.priceUnitMatchesQuantity(text, quantityUnit)) return text;
|
||||
return (
|
||||
resolvePriceUnitByQuantityUnit(quantityUnit, currency) ||
|
||||
text ||
|
||||
String(priceUnit || '').trim()
|
||||
);
|
||||
},
|
||||
resolvePriceUnitText(priceUnit) {
|
||||
const raw = String(priceUnit || '').trim();
|
||||
if (!raw) return '';
|
||||
if (raw.includes('/')) return raw;
|
||||
const options = this.feeUnitSelectOptions.length
|
||||
? this.feeUnitSelectOptions
|
||||
: this.feeUnitOptions;
|
||||
const matched = options.find(
|
||||
item => String(item.value) === raw || String(item.label) === raw
|
||||
);
|
||||
return matched?.label || raw;
|
||||
},
|
||||
quantityUnits() {
|
||||
return [
|
||||
...new Set(
|
||||
@@ -3124,7 +3254,7 @@ export default {
|
||||
.replace(/\s+/g, '')
|
||||
.replace(/立方米|立方公尺|方/g, '立方');
|
||||
const unit = normalizeUnit(quantityUnit).replace(/[·*.]/g, '');
|
||||
const value = String(priceUnit || '')
|
||||
const value = String(this.resolvePriceUnitText(priceUnit) || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '');
|
||||
@@ -3156,6 +3286,10 @@ export default {
|
||||
return this.formatFreightNumber(item.freightAmount);
|
||||
}
|
||||
if (!this.isFreightAutoCalculable(item)) {
|
||||
// 无法自动计算且未手工填写时返回空,避免把空值显示成 0 造成“已填写”的错觉。
|
||||
if (item.freightAmount === '' || item.freightAmount === null || item.freightAmount === undefined) {
|
||||
return '';
|
||||
}
|
||||
return this.formatFreightNumber(item.freightAmount);
|
||||
}
|
||||
return this.formatFreightNumber(
|
||||
@@ -3421,10 +3555,25 @@ export default {
|
||||
.shipping-template-page__route-row {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(160px, 1fr) minmax(220px, 2fr) auto minmax(140px, 1fr) auto minmax(160px, 1fr)
|
||||
auto;
|
||||
minmax(160px, 1fr) minmax(220px, 2fr) 40px auto minmax(140px, 1fr) auto
|
||||
minmax(160px, 1fr);
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.shipping-template-page__address-pick-btn {
|
||||
:deep(.el-icon) {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.shipping-template-page__map-suffix {
|
||||
cursor: pointer;
|
||||
color: #909399;
|
||||
font-size: 18px;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
.shipping-template-page__address-map-input {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1677,7 +1677,7 @@
|
||||
{{
|
||||
isDispatchPlanQuantityUnlimited(row)
|
||||
? '-'
|
||||
: formatDispatchQuantity(dispatchItemRemainingQuantity(row))
|
||||
: formatDispatchQuantity(dispatchItemDisplayRemainingQuantity(row))
|
||||
}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -1861,7 +1861,7 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="司机" :required="dispatchItemForm.carrierType !== '承运商'">
|
||||
<el-form-item label="司机" required>
|
||||
<el-autocomplete
|
||||
v-model="dispatchItemForm.driverName"
|
||||
placeholder="请输入"
|
||||
@@ -1870,11 +1870,15 @@
|
||||
:fetch-suggestions="fetchTaskDriverSuggestions"
|
||||
:loading="taskDriverLoading"
|
||||
@select="item => handleTaskDriverSelect('dispatch', item)"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="dispatchItemForm.carrierType === '承运商' ? '手机号' : '司机手机号'"
|
||||
:required="dispatchItemForm.carrierType !== '承运商'"
|
||||
required
|
||||
>
|
||||
<el-input v-model="dispatchItemForm.driverPhone" placeholder="请输入" />
|
||||
</el-form-item>
|
||||
@@ -1893,7 +1897,11 @@
|
||||
:fetch-suggestions="fetchTaskEscortSuggestions"
|
||||
:loading="taskEscortLoading"
|
||||
@select="handleTaskEscortSelect"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dispatchItemForm.carrierType !== '承运商'" label="押运人手机号">
|
||||
<el-input v-model="dispatchItemForm.escortPhone" placeholder="请输入" />
|
||||
@@ -1955,7 +1963,7 @@
|
||||
class="transport-plan-page__dispatch-quantity-hint"
|
||||
>
|
||||
剩余数量:{{
|
||||
formatDispatchQuantity(dispatchItemHintRemainingQuantity(dispatchItemForm))
|
||||
formatDispatchQuantity(dispatchItemDisplayRemainingQuantity(dispatchItemForm))
|
||||
}}
|
||||
{{ dispatchItemForm.quantityUnit || '吨' }}
|
||||
</div>
|
||||
@@ -2096,11 +2104,7 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="dispatchIsRoadTransport"
|
||||
label="司机"
|
||||
:required="dispatchItemForm.carrierType === '自运'"
|
||||
>
|
||||
<el-form-item v-if="dispatchIsRoadTransport" label="司机" required>
|
||||
<el-autocomplete
|
||||
v-model="dispatchItemForm.driverName"
|
||||
placeholder="请输入"
|
||||
@@ -2111,11 +2115,7 @@
|
||||
@select="item => handleTaskDriverSelect('dispatch', item)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="dispatchIsRoadTransport"
|
||||
label="手机号"
|
||||
:required="dispatchItemForm.carrierType === '自运'"
|
||||
>
|
||||
<el-form-item v-if="dispatchIsRoadTransport" label="手机号" required>
|
||||
<el-input v-model="dispatchItemForm.driverPhone" placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
@@ -2768,7 +2768,7 @@ import {
|
||||
getList as getProjectList,
|
||||
} from '@/api/business/project-apply';
|
||||
import { getList as getCommonRouteList } from '@/api/business/common-route';
|
||||
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
|
||||
import { fetchDriverCandidates, driverOptionText } from '@/utils/carrier-org-resource';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { addressTypeOptions } from '@/option/base/common-address';
|
||||
@@ -2807,8 +2807,11 @@ import 'nprogress/nprogress.css';
|
||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||
let amapLoader;
|
||||
const transportPlanListRoute = '/business/transport-plan';
|
||||
const transportPlanFormRoute = '/business/transport-plan/form';
|
||||
const transportPlanListRefreshKey = 'transport-plan-list-refresh';
|
||||
const standaloneTransportPlanFormRoutes = {
|
||||
'/business/transport-plan': '/business/transport-plan/form',
|
||||
[transportPlanListRoute]: transportPlanFormRoute,
|
||||
};
|
||||
const standaloneTransportPlanFormPaths = [
|
||||
...Object.keys(standaloneTransportPlanFormRoutes),
|
||||
@@ -3072,6 +3075,10 @@ export default {
|
||||
// 必须放 data:Options API 的顺序是 data → computed → watch(immediate) → created,
|
||||
// 放 created 的话首个 immediate watcher 会读到空值,把首次加载也挡掉。
|
||||
routePathLocked: this.$route.path,
|
||||
// 表单页实例在创建时锁定,避免 keep-alive 列表实例随全局 $route 误切到表单态。
|
||||
isFormPageInstance: Object.values(standaloneTransportPlanFormRoutes).includes(
|
||||
this.$route.path
|
||||
),
|
||||
form: {},
|
||||
suppressTransportTypeClear: false,
|
||||
query: {},
|
||||
@@ -3112,10 +3119,11 @@ export default {
|
||||
option: (() => {
|
||||
const opt = this.cloneOption(this.crudOption, estimateMenuButtonCount(this.config));
|
||||
if (this.menuWidth != null) opt.menuWidth = this.menuWidth;
|
||||
if (
|
||||
this.standaloneFormPage &&
|
||||
standaloneTransportPlanFormPaths.includes(this.$route.path)
|
||||
) {
|
||||
// 仅表单页实例套用全屏表单弹窗样式,避免列表页误用后表格区域异常。
|
||||
const isFormInstance = Object.values(standaloneTransportPlanFormRoutes).includes(
|
||||
this.$route.path
|
||||
);
|
||||
if (this.standaloneFormPage && isFormInstance) {
|
||||
opt.dialogAppendToBody = false;
|
||||
opt.dialogModal = false;
|
||||
opt.dialogCustomClass = 'transport-plan-form-page-dialog';
|
||||
@@ -3150,6 +3158,8 @@ export default {
|
||||
dispatchItemIndex: -1,
|
||||
dispatchItemForm: defaultDispatchRow(),
|
||||
dispatchItemCargoRows: [],
|
||||
// 本次数量变更版本号:驱动同表其他行「剩余数量」同步重算
|
||||
dispatchCargoQuantityVersion: 0,
|
||||
dispatchItemAttachmentRows: [],
|
||||
selectedDispatchItemAttachmentRows: [],
|
||||
attachmentRows: [],
|
||||
@@ -3289,6 +3299,12 @@ export default {
|
||||
}
|
||||
this.loadTaskQuantityUnitOptions();
|
||||
},
|
||||
// keep-alive 重新激活列表时强制刷新,避免缓存脏数据;标记位用于兼容旧跳转逻辑。
|
||||
activated() {
|
||||
if (this.isFormPageInstance) return;
|
||||
sessionStorage.removeItem(transportPlanListRefreshKey);
|
||||
this.$nextTick(() => this.onLoad(this.page, this.query));
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
permissionList() {
|
||||
@@ -3300,10 +3316,20 @@ export default {
|
||||
return this.config.detailSections || [];
|
||||
},
|
||||
isStandaloneBusinessPage() {
|
||||
return this.standaloneFormPage && standaloneTransportPlanFormPaths.includes(this.$route.path);
|
||||
return (
|
||||
this.standaloneFormPage &&
|
||||
(this.isFormPageInstance ||
|
||||
standaloneTransportPlanFormPaths.includes(this.routePathLocked) ||
|
||||
standaloneTransportPlanFormPaths.includes(this.$route.path))
|
||||
);
|
||||
},
|
||||
isStandaloneFormPage() {
|
||||
return this.isStandaloneBusinessPage && ['add', 'edit'].includes(this.$route.query.mode);
|
||||
// 用实例锁定标志,避免列表 keep-alive 实例被全局路由误判成表单页。
|
||||
return (
|
||||
this.standaloneFormPage &&
|
||||
this.isFormPageInstance &&
|
||||
['add', 'edit'].includes(this.$route.query.mode)
|
||||
);
|
||||
},
|
||||
formPageTitle() {
|
||||
if (this.isStandaloneFormPage) {
|
||||
@@ -3752,6 +3778,18 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
// 列表实例:从其他页进入列表时刷新(activated 负责标签切回)。
|
||||
if (!this.isFormPageInstance) {
|
||||
const enteringList =
|
||||
(to.path === this.routePathLocked || to.path === transportPlanListRoute) &&
|
||||
from?.path &&
|
||||
from.path !== to.path;
|
||||
if (enteringList) {
|
||||
sessionStorage.removeItem(transportPlanListRefreshKey);
|
||||
this.$nextTick(() => this.onLoad(this.page, this.query));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!this.isStandaloneFormPage) return;
|
||||
if (
|
||||
to.query.mode !== from?.query?.mode ||
|
||||
@@ -3779,6 +3817,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
driverOptionText,
|
||||
initStandaloneFormPage() {
|
||||
if (!this.isStandaloneFormPage) return;
|
||||
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
||||
@@ -4148,6 +4187,7 @@ export default {
|
||||
waiting: ['待执行', 'is-pending'],
|
||||
processing: ['进行中', 'is-processing'],
|
||||
running: ['进行中', 'is-processing'],
|
||||
pending_return_confirm: ['待回单确认', 'is-pending'],
|
||||
completed: ['已完成', 'is-completed'],
|
||||
cancelled: ['已取消', 'is-cancelled'],
|
||||
};
|
||||
@@ -4484,6 +4524,9 @@ export default {
|
||||
loading();
|
||||
}
|
||||
},
|
||||
markTransportPlanListNeedRefresh() {
|
||||
sessionStorage.setItem(transportPlanListRefreshKey, '1');
|
||||
},
|
||||
rowSave(row, done, loading) {
|
||||
const submitRow = this.normalizeRow(row);
|
||||
if (!submitRow) {
|
||||
@@ -4492,6 +4535,7 @@ export default {
|
||||
}
|
||||
this.getSaveRequest()(submitRow).then(
|
||||
() => {
|
||||
this.markTransportPlanListNeedRefresh();
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '操作成功!' });
|
||||
done();
|
||||
@@ -4511,6 +4555,7 @@ export default {
|
||||
}
|
||||
this.getSaveRequest()(submitRow).then(
|
||||
() => {
|
||||
this.markTransportPlanListNeedRefresh();
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '操作成功!' });
|
||||
done();
|
||||
@@ -4540,7 +4585,7 @@ export default {
|
||||
.submit(submitRow)
|
||||
.then(() => {
|
||||
this.$message.success(action === 'draft' ? '暂存成功' : '创建成功');
|
||||
this.onLoad(this.page, this.query);
|
||||
this.markTransportPlanListNeedRefresh();
|
||||
this.closeCrudDialog();
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -4548,12 +4593,14 @@ export default {
|
||||
});
|
||||
},
|
||||
closeCrudDialog() {
|
||||
if (this.isStandaloneBusinessPage) {
|
||||
if (this.isStandaloneBusinessPage || this.isFormPageInstance) {
|
||||
// 关闭独立表单页时标记列表需刷新(含直接点关闭未保存的场景)
|
||||
this.markTransportPlanListNeedRefresh();
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
const listPath =
|
||||
Object.entries(standaloneTransportPlanFormRoutes).find(
|
||||
([, formPath]) => formPath === this.$route.path
|
||||
)?.[0] || this.$route.path;
|
||||
([, formPath]) => formPath === this.$route.path || formPath === this.routePathLocked
|
||||
)?.[0] || transportPlanListRoute;
|
||||
this.$router.push({ path: listPath, query: {} });
|
||||
return;
|
||||
}
|
||||
@@ -4951,16 +4998,8 @@ export default {
|
||||
},
|
||||
fetchTaskDriverSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getDispatchCarrierFilter();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
this.taskDriverLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
|
||||
.then(records => {
|
||||
this.taskDriverOptions = records;
|
||||
callback(
|
||||
@@ -4980,16 +5019,12 @@ export default {
|
||||
},
|
||||
fetchTaskEscortSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getDispatchCarrierFilter();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
this.taskEscortLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({
|
||||
size: 20,
|
||||
...(keyword ? { driverName: keyword } : {}),
|
||||
posts: '押运员',
|
||||
})
|
||||
.then(records => {
|
||||
this.taskEscortOptions = records;
|
||||
callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' })));
|
||||
@@ -6634,20 +6669,33 @@ export default {
|
||||
return query;
|
||||
},
|
||||
onLoad(page, params = {}) {
|
||||
if (this.isFormPageInstance) return;
|
||||
this.loading = true;
|
||||
const currentPage = page?.currentPage || this.page.currentPage || 1;
|
||||
const pageSize = page?.pageSize || this.page.pageSize || 10;
|
||||
const query = this.normalizeQueryParams({
|
||||
...params,
|
||||
...this.query,
|
||||
allDept: 0,
|
||||
});
|
||||
this.api
|
||||
.getList(page.currentPage, page.pageSize, query)
|
||||
.getList(currentPage, pageSize, query)
|
||||
.then(res => {
|
||||
const result = res.data.data;
|
||||
this.page.total = result.total;
|
||||
this.data = result.records;
|
||||
const result = res?.data?.data || res?.data || {};
|
||||
const records = Array.isArray(result.records)
|
||||
? result.records
|
||||
: Array.isArray(result)
|
||||
? result
|
||||
: [];
|
||||
this.page.total = Number(result.total ?? records.length) || 0;
|
||||
this.data = records;
|
||||
this.selectionClear();
|
||||
})
|
||||
.catch(error => {
|
||||
window.console.error(error);
|
||||
this.data = [];
|
||||
this.page.total = 0;
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
@@ -7652,6 +7700,7 @@ export default {
|
||||
row.cargoName = '';
|
||||
row.specification = '';
|
||||
row.model = '';
|
||||
this.dispatchCargoQuantityVersion += 1;
|
||||
},
|
||||
handleDispatchConfiguredCargoNameChange(row, value) {
|
||||
const nextValue = String(value ?? row.cargoName ?? '').trim();
|
||||
@@ -7660,6 +7709,7 @@ export default {
|
||||
);
|
||||
if (!option) {
|
||||
row.cargoName = nextValue;
|
||||
this.dispatchCargoQuantityVersion += 1;
|
||||
return;
|
||||
}
|
||||
const previousType = String(row.cargoType || '').trim();
|
||||
@@ -7674,6 +7724,7 @@ export default {
|
||||
}
|
||||
row.specification = option.specification || row.specification || '';
|
||||
row.model = option.model || row.model || '';
|
||||
this.dispatchCargoQuantityVersion += 1;
|
||||
},
|
||||
openDispatchItemDialog(index = -1, row = defaultDispatchRow()) {
|
||||
const defaultSummaryItem =
|
||||
@@ -7991,17 +8042,12 @@ export default {
|
||||
this.taskDriverOptions = [];
|
||||
this.taskEscortOptions = [];
|
||||
},
|
||||
getDispatchCarrierFilter() {
|
||||
return {
|
||||
carrierId: this.dispatchItemForm.carrierId || '',
|
||||
carrierName: this.dispatchItemForm.carrierName || '',
|
||||
};
|
||||
},
|
||||
handleDispatchCargoQuantityInput(row, value) {
|
||||
const text = String(value || '').replace(/[^\d.]/g, '');
|
||||
const parts = text.split('.');
|
||||
row.quantity =
|
||||
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 3)}` : parts[0];
|
||||
this.dispatchCargoQuantityVersion += 1;
|
||||
},
|
||||
handleDispatchCargoNumberInput(row, prop, value) {
|
||||
this.handleFreightNumberInput(row, prop, value);
|
||||
@@ -8021,12 +8067,14 @@ export default {
|
||||
const nextRow = this.normalizeTransportCargoRow({ priceUnit: this.defaultPriceUnit });
|
||||
if (index > -1) this.dispatchItemCargoRows.splice(index + 1, 0, nextRow);
|
||||
else this.dispatchItemCargoRows.push(nextRow);
|
||||
this.dispatchCargoQuantityVersion += 1;
|
||||
},
|
||||
removeDispatchCargoRow(index) {
|
||||
this.dispatchItemCargoRows.splice(index, 1);
|
||||
if (!this.dispatchItemCargoRows.length) {
|
||||
this.dispatchItemCargoRows.push(this.normalizeTransportCargoRow());
|
||||
}
|
||||
this.dispatchCargoQuantityVersion += 1;
|
||||
},
|
||||
dispatchCargoFreightAmount(cargo = {}) {
|
||||
if (cargo.freightAmount !== undefined && cargo.freightAmount !== null) {
|
||||
@@ -8176,11 +8224,50 @@ export default {
|
||||
}, 0)
|
||||
);
|
||||
},
|
||||
/** 精简录入提示:可调度余量扣减当前本次数量后的剩余 */
|
||||
dispatchItemHintRemainingQuantity(row = {}) {
|
||||
/**
|
||||
* 当前编辑表单中,与指定行同货物(有身份)或同单位(无身份)已占用的本次数量合计。
|
||||
* 用于剩余数量展示联动,不参与提交校验(校验仍按整单合计对比计划余量)。
|
||||
*/
|
||||
getDispatchFormOccupiedQuantity(row = {}) {
|
||||
const goodsRows =
|
||||
this.dispatchItemForm?.taskEntryMode === 'full'
|
||||
? this.dispatchItemCargoRows || []
|
||||
: [this.dispatchItemForm || row];
|
||||
const hasCargoIdentity = Boolean(
|
||||
String(row.cargoName || row.goodsName || row.name || '').trim() ||
|
||||
String(row.cargoType || row.secondCargoTypeName || row.goodsType || row.type || '').trim()
|
||||
);
|
||||
const unit = this.getDispatchQuantityUnit(row);
|
||||
const cargoKey = this.getDispatchCargoIdentity(row);
|
||||
return goodsRows.reduce((sum, goods) => {
|
||||
if (!goods) return sum;
|
||||
if (hasCargoIdentity) {
|
||||
const goodsHasIdentity = Boolean(
|
||||
String(goods.cargoName || goods.goodsName || goods.name || '').trim() ||
|
||||
String(
|
||||
goods.cargoType || goods.secondCargoTypeName || goods.goodsType || goods.type || ''
|
||||
).trim()
|
||||
);
|
||||
if (!goodsHasIdentity || this.getDispatchCargoIdentity(goods) !== cargoKey) {
|
||||
return sum;
|
||||
}
|
||||
} else if (this.getDispatchQuantityUnit(goods) !== unit) {
|
||||
return sum;
|
||||
}
|
||||
return sum + this.parseDispatchQuantity(goods.quantity);
|
||||
}, 0);
|
||||
},
|
||||
/** 展示用剩余数量:计划可调度余量扣减本单已填本次数量(含当前行) */
|
||||
dispatchItemDisplayRemainingQuantity(row = {}) {
|
||||
// 依赖版本号,确保一行本次数量变化后其他行剩余数量一并刷新
|
||||
void this.dispatchCargoQuantityVersion;
|
||||
const available = this.dispatchItemRemainingQuantity(row);
|
||||
const current = this.parseDispatchQuantity(row.quantity);
|
||||
return Math.max(available - current, 0);
|
||||
const occupied = this.getDispatchFormOccupiedQuantity(row);
|
||||
return Math.max(available - occupied, 0);
|
||||
},
|
||||
/** 精简录入提示:与完整录入剩余数量展示规则一致 */
|
||||
dispatchItemHintRemainingQuantity(row = {}) {
|
||||
return this.dispatchItemDisplayRemainingQuantity(row);
|
||||
},
|
||||
pruneDispatchPendingRows(rows = []) {
|
||||
const planQuantities = new Map();
|
||||
@@ -8278,13 +8365,17 @@ export default {
|
||||
const fullEntry = row.taskEntryMode === 'full';
|
||||
const roadTransport = this.resolveTransportMode(row.transportType) === 'road';
|
||||
if (fullEntry) {
|
||||
if (roadTransport && row.carrierType === '自运') {
|
||||
// 公路运输时司机/手机号一律必填(含承运类型为承运商)
|
||||
if (roadTransport && row.carrierType) {
|
||||
requiredFields.push(['driverName', '司机']);
|
||||
requiredFields.push(['driverPhone', '手机号']);
|
||||
}
|
||||
} else if (row.carrierType && row.carrierType !== '承运商') {
|
||||
} else if (row.carrierType) {
|
||||
requiredFields.push(['driverName', '司机']);
|
||||
requiredFields.push(['driverPhone', '司机手机号']);
|
||||
requiredFields.push([
|
||||
'driverPhone',
|
||||
row.carrierType === '承运商' ? '手机号' : '司机手机号',
|
||||
]);
|
||||
}
|
||||
const emptyField = requiredFields.find(([prop]) => this.isBillingFieldEmpty(row[prop]));
|
||||
if (emptyField) return requiredMessage(emptyField[1]);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="voucher-folder-page voucher-folder-page--embedded">
|
||||
<section class="voucher-folder-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="160px">
|
||||
<el-form :model="query" label-position="right">
|
||||
<div class="voucher-folder-page__search-grid">
|
||||
<el-form-item label="车牌号">
|
||||
<el-input v-model="query.plateNo" clearable placeholder="请输入" />
|
||||
@@ -196,7 +196,7 @@ watch(
|
||||
}
|
||||
&__search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 360px 1fr;
|
||||
grid-template-columns: 220px 220px 1fr;
|
||||
gap: 8px 24px;
|
||||
}
|
||||
&__search-actions {
|
||||
|
||||
@@ -96,22 +96,12 @@
|
||||
prop="carrierName"
|
||||
label="承运商"
|
||||
min-width="140"
|
||||
/><el-table-column
|
||||
v-if="columnVisible.carrierType"
|
||||
prop="carrierType"
|
||||
label="承运类型"
|
||||
min-width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="columnVisible.waybillCount"
|
||||
prop="waybillCount"
|
||||
label="运单数"
|
||||
width="90"
|
||||
/> <el-table-column
|
||||
v-if="columnVisible.importTypeName"
|
||||
prop="importTypeName"
|
||||
label="导入方式"
|
||||
width="100"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="columnVisible.statusName"
|
||||
@@ -119,6 +109,12 @@
|
||||
label="状态"
|
||||
width="100"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="columnVisible.settlementStatusName"
|
||||
prop="settlementStatusName"
|
||||
label="结算状态"
|
||||
width="110"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="columnVisible.createUserName"
|
||||
prop="createUserName"
|
||||
@@ -159,7 +155,7 @@
|
||||
>编辑</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="row.importStatus === 'draft'"
|
||||
v-if="canRemoveBatch(row)"
|
||||
type="danger"
|
||||
@click="removeBatch(row)"
|
||||
>删除</el-link
|
||||
@@ -641,10 +637,9 @@ const columnOptions = [
|
||||
{ prop: 'batchNo', label: '运单批次号' },
|
||||
{ prop: 'projectName', label: '项目名称' },
|
||||
{ prop: 'carrierName', label: '承运商' },
|
||||
{ prop: 'carrierType', label: '承运类型' },
|
||||
{ prop: 'waybillCount', label: '运单数' },
|
||||
{ prop: 'importTypeName', label: '导入方式' },
|
||||
{ prop: 'statusName', label: '状态' },
|
||||
{ prop: 'settlementStatusName', label: '结算状态' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
{ prop: 'updateTime', label: '更新时间' },
|
||||
@@ -887,11 +882,14 @@ const syncCarrierOptions = () => {
|
||||
carrierOptions.value = [];
|
||||
return;
|
||||
}
|
||||
const carrierId =
|
||||
const carrierId = resolveCarrierArchiveId(
|
||||
customerContract?.partyBId ||
|
||||
customerContract?.partyBUserId ||
|
||||
customerContract?.customerId ||
|
||||
'';
|
||||
customerContract?.partyBUserId ||
|
||||
customerContract?.contractPartyBId ||
|
||||
customerContract?.customerId ||
|
||||
'',
|
||||
carrierName
|
||||
);
|
||||
carrierOptions.value = [{ id: carrierId, carrierName, label: carrierName, value: carrierName }];
|
||||
form.carrierId = carrierId;
|
||||
form.carrierIds = carrierId ? [carrierId] : [];
|
||||
@@ -1263,9 +1261,47 @@ const fillEditForm = async row => {
|
||||
if (carrierValue) carrierChange(carrierValue);
|
||||
await loadBatchRows(snapshot.id);
|
||||
};
|
||||
const removeBatches = () =>
|
||||
api.removeImportBatches(selected.value.map(item => item.id).join(',')).then(loadBatches);
|
||||
const removeBatch = row => api.removeImportBatches(row.id).then(loadBatches);
|
||||
/** 与后端一致:仅未结算批次可整批删除 */
|
||||
const canRemoveBatch = row => String(row?.settlementStatus || 'unsettled') === 'unsettled';
|
||||
const removeBatches = async () => {
|
||||
if (!selected.value.length) {
|
||||
ElMessage.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
// 选中含部分结算/已结算时整批拦截,不静默跳过
|
||||
if (selected.value.some(row => !canRemoveBatch(row))) {
|
||||
ElMessage.warning('仅未结算的运单批次可以删除');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除选中的运单批次?删除后不可恢复。', '提示', {
|
||||
type: 'warning',
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
await api.removeImportBatches(selected.value.map(item => item.id).join(','));
|
||||
ElMessage.success('删除成功');
|
||||
return loadBatches();
|
||||
};
|
||||
const removeBatch = async row => {
|
||||
if (!canRemoveBatch(row)) {
|
||||
ElMessage.warning('仅未结算的运单批次可以删除');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认删除运单批次「${row.batchNo || ''}」?删除后不可恢复。`,
|
||||
'提示',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
await api.removeImportBatches(row.id);
|
||||
ElMessage.success('删除成功');
|
||||
return loadBatches();
|
||||
};
|
||||
const projectChange = async id => {
|
||||
const requestId = ++projectRequestId;
|
||||
const item = projects.value.find(row => String(row.id) === String(id));
|
||||
@@ -1317,12 +1353,20 @@ const contractChange = id => {
|
||||
const carrierTypeChange = () => {
|
||||
syncCarrierOptions();
|
||||
};
|
||||
const resolveCarrierArchiveId = (carrierId, carrierName) => {
|
||||
if (carrierId) return carrierId;
|
||||
const name = String(carrierName || '').trim();
|
||||
if (!name) return '';
|
||||
const matched = queryCarriers.value.find(item => String(item.name || '').trim() === name);
|
||||
return matched?.id || '';
|
||||
};
|
||||
const carrierChange = value => {
|
||||
const item = carrierOptions.value.find(option => String(option.value) === String(value || ''));
|
||||
clearCarrier();
|
||||
if (!item) return;
|
||||
form.carrierId = item.id || '';
|
||||
form.carrierIds = item.id ? [item.id] : [];
|
||||
const carrierId = resolveCarrierArchiveId(item.id, item.carrierName);
|
||||
form.carrierId = carrierId;
|
||||
form.carrierIds = carrierId ? [carrierId] : [];
|
||||
form.carrierName = item.carrierName || '';
|
||||
if (!isSelfOperated.value) form.carrierContractId = item.carrierContractId || '';
|
||||
};
|
||||
@@ -1337,11 +1381,23 @@ const formatOptionalNumber = value => {
|
||||
const normalized = normalizeOptionalImportNumber(value);
|
||||
return normalized === null || normalized === undefined ? '' : normalized;
|
||||
};
|
||||
// 明细接口返回运单实体字段 startDate/endDate/estimatedStartTime/estimatedEndTime,
|
||||
// 而明细表与编辑回填按导入列字段名 actualStartDate 等读取,重载后在此对齐,否则实际/预计时间列为空。
|
||||
const detailDateAliases = {
|
||||
actualStartDate: 'startDate',
|
||||
actualEndDate: 'endDate',
|
||||
planStartDate: 'estimatedStartTime',
|
||||
planEndDate: 'estimatedEndTime',
|
||||
};
|
||||
const normalizeDetailRow = row => {
|
||||
const next = { ...(row || {}) };
|
||||
OPTIONAL_NUMBER_PROPS.forEach(prop => {
|
||||
next[prop] = formatOptionalNumber(next[prop]);
|
||||
});
|
||||
Object.entries(detailDateAliases).forEach(([prop, source]) => {
|
||||
if (String(next[prop] ?? '').trim()) return;
|
||||
if (String(next[source] ?? '').trim()) next[prop] = next[source];
|
||||
});
|
||||
return next;
|
||||
};
|
||||
const normalizeImportRow = row => ({
|
||||
@@ -1738,11 +1794,23 @@ const normalizeOptionalPlanId = value => {
|
||||
const buildImportPayload = (status = form.status, rowList = rows.value, extra = {}) => {
|
||||
const { file, ...batch } = form;
|
||||
const planId = normalizeOptionalPlanId(batch.planId);
|
||||
const carrierId = resolveCarrierArchiveId(
|
||||
batch.carrierId || batch.carrierIds?.[0] || '',
|
||||
batch.carrierName
|
||||
);
|
||||
const carrierIds =
|
||||
Array.isArray(batch.carrierIds) && batch.carrierIds.length
|
||||
? batch.carrierIds
|
||||
: carrierId
|
||||
? [carrierId]
|
||||
: [];
|
||||
return {
|
||||
...batch,
|
||||
planId,
|
||||
// 未选计划时不带占位名称,避免落库/回显 -1
|
||||
planName: planId ? batch.planName || undefined : null,
|
||||
carrierId: carrierId || undefined,
|
||||
carrierIds,
|
||||
status,
|
||||
rows: rowList.map(normalizeImportRow).map(stripInternalRowFields),
|
||||
...extra,
|
||||
|
||||
@@ -1200,7 +1200,11 @@
|
||||
:loading="taskDriverLoading"
|
||||
:disabled="dialogReadonly"
|
||||
@select="handleTaskDriverSelect"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="isRoadTransport"
|
||||
@@ -1279,7 +1283,11 @@
|
||||
:loading="taskEscortLoading"
|
||||
:disabled="dialogReadonly"
|
||||
@select="handleTaskEscortSelect"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showRoadEscortFields" label="押运人手机号">
|
||||
<el-input
|
||||
@@ -1334,7 +1342,7 @@
|
||||
:disabled="dialogReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="型号" required>
|
||||
<el-form-item label="型号">
|
||||
<el-input
|
||||
v-model="form.model"
|
||||
placeholder="选择货物自动回填"
|
||||
@@ -1594,6 +1602,13 @@
|
||||
<el-link type="primary" v-if="canComplete(row)" @click="handleAction('complete', row)">
|
||||
{{ completeActionLabel }}
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="canConfirmReturn(row)"
|
||||
@click="handleAction('confirmReturn', row)"
|
||||
>
|
||||
确认回单
|
||||
</el-link>
|
||||
<el-link type="primary" v-if="canMaintainMileage(row)" @click="openMileageDialog(row)">
|
||||
维护里程
|
||||
</el-link>
|
||||
@@ -1653,7 +1668,11 @@
|
||||
style="width: 100%"
|
||||
@select="handleReassignDriverSelect"
|
||||
@change="handleReassignDriverChange"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="手机号"
|
||||
@@ -2252,6 +2271,13 @@
|
||||
<span v-if="record.volume">体积 {{ record.volume }}方</span>
|
||||
<span v-if="record.quantity">数量 {{ record.quantity }}</span>
|
||||
</div>
|
||||
<!-- 里程只挂在末位节点上(needMileage 由后端下发),显示的是运单里程 -->
|
||||
<div
|
||||
v-if="record.needMileage && Number(record.mileage) > 0"
|
||||
class="waybill-manage-page__punch-record-line"
|
||||
>
|
||||
里程 {{ record.mileage }} 公里
|
||||
</div>
|
||||
<div
|
||||
v-if="record.remark"
|
||||
class="waybill-manage-page__punch-record-line"
|
||||
@@ -2848,6 +2874,20 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!-- 里程只在末位打卡节点出现:后端按「末位 + 计费规则含里程」下发 needMileage,前端不自己推 -->
|
||||
<el-col v-if="waybillPunchEditMileageVisible" :span="8">
|
||||
<el-form-item label="里程">
|
||||
<el-input
|
||||
v-model="waybillPunchEditForm.mileage"
|
||||
maxlength="10"
|
||||
:disabled="waybillPunchEditMileageLocked"
|
||||
placeholder="请输入"
|
||||
@input="handleWaybillPunchMileageInput"
|
||||
>
|
||||
<template #append>公里</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
@@ -3020,10 +3060,56 @@
|
||||
<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">
|
||||
{{ row.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>
|
||||
@@ -3637,6 +3723,7 @@ import {
|
||||
getVoucherImages as getProcessConfigVoucherImages,
|
||||
} from '@/api/business/process-config';
|
||||
import {
|
||||
getList as getWaybillList,
|
||||
getPunchRecords as getWaybillPunchRecords,
|
||||
locateVehicle,
|
||||
savePunchRecord,
|
||||
@@ -3646,9 +3733,7 @@ import VoucherExecutionDetail from './voucher-execution-detail.vue';
|
||||
import PunchRecordPhotos from './punch-record-photos.vue';
|
||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||
import {
|
||||
fetchDriversByCarrierOrganizations,
|
||||
} from '@/utils/carrier-org-resource';
|
||||
import { fetchDriverCandidates, driverOptionText } from '@/utils/carrier-org-resource';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { addressTypeOptions } from '@/option/base/common-address';
|
||||
@@ -3666,6 +3751,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 { parseRouteChangeContent } from '@/utils/route-change';
|
||||
import { applyTableMenuWidth } from '@/utils/table-menu';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
@@ -3964,6 +4050,7 @@ export default {
|
||||
weight: '',
|
||||
volume: '',
|
||||
quantity: '',
|
||||
mileage: '',
|
||||
remark: '',
|
||||
photos: [],
|
||||
},
|
||||
@@ -4323,9 +4410,15 @@ export default {
|
||||
addBtn: this.canCreate,
|
||||
};
|
||||
},
|
||||
// 打卡记录编辑入口与运单编辑同权限:能改运单的人才能订正它的打卡流水
|
||||
// 打卡记录编辑入口与运单编辑同权限:能改运单的人才能订正它的打卡流水。
|
||||
// 时间轴的「编辑打卡 / 补卡」链接是本页唯一写入入口,提交固定 punched=true。
|
||||
// 取消打卡不在本页;待回单确认的运单不显示该链接。
|
||||
canEditWaybillPunchRecord() {
|
||||
return this.hasPermission(`${this.config.permission}_edit`) && !this.readonlyScope;
|
||||
return (
|
||||
this.hasPermission(`${this.config.permission}_edit`) &&
|
||||
!this.readonlyScope &&
|
||||
this.statusValue(this.detailRow) !== 'pending_return_confirm'
|
||||
);
|
||||
},
|
||||
waybillPunchEditNode() {
|
||||
return this.findWaybillPunchEditNodeConfig(this.waybillPunchEditRecord);
|
||||
@@ -4360,6 +4453,15 @@ export default {
|
||||
waybillPunchEditPunchDisabled() {
|
||||
return this.waybillPunchEditNode?.punch === false;
|
||||
},
|
||||
// 里程:后端下发 needMileage 才出现(末位打卡节点 + 计费规则含里程);在途打卡不采集里程
|
||||
waybillPunchEditMileageVisible() {
|
||||
const record = this.waybillPunchEditRecord || {};
|
||||
return record.needMileage === true && record.type !== 'enroute';
|
||||
},
|
||||
// 已生成结算单的运单里程冻结:输入框置灰,改动由后端拒绝
|
||||
waybillPunchEditMileageLocked() {
|
||||
return this.waybillPunchEditRecord?.mileageLocked === true;
|
||||
},
|
||||
waybillPunchEditRules() {
|
||||
return { punchTime: [{ required: true, message: '请选择打卡时间', trigger: 'change' }] };
|
||||
},
|
||||
@@ -4753,6 +4855,8 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
driverOptionText,
|
||||
parseRouteChangeContent,
|
||||
// 页面被 keep-alive 缓存/卸载时调用,关闭本页所有 append-to-body 的弹窗,
|
||||
// 避免 Teleport 出去的 DOM 残留在 body 上盖住后续页面(详见 data 里的说明)。
|
||||
closeInnerDialogs() {
|
||||
@@ -5051,7 +5155,8 @@ export default {
|
||||
return false;
|
||||
}
|
||||
const status = this.statusValue(row);
|
||||
return status === 'pending';
|
||||
// 待回单确认可取消,取消不生成应收应付
|
||||
return status === 'pending' || status === 'pending_return_confirm';
|
||||
},
|
||||
canComplete(row) {
|
||||
if (
|
||||
@@ -5063,7 +5168,17 @@ export default {
|
||||
return false;
|
||||
}
|
||||
const status = this.statusValue(row);
|
||||
return status === 'processing' || status === 'running';
|
||||
// 待回单确认也可直接完成,不经确认回单
|
||||
return status === 'processing' || status === 'running' || status === 'pending_return_confirm';
|
||||
},
|
||||
canConfirmReturn(row) {
|
||||
return (
|
||||
this.hasAction('confirmReturn') &&
|
||||
this.hasPermission(`${this.config.permission}_complete`) &&
|
||||
!this.readonlyScope &&
|
||||
!row.readonly &&
|
||||
this.statusValue(row) === 'pending_return_confirm'
|
||||
);
|
||||
},
|
||||
canMaintainMileage(row) {
|
||||
return (
|
||||
@@ -5218,10 +5333,11 @@ export default {
|
||||
return true;
|
||||
},
|
||||
showDetailButton(row) {
|
||||
// 草稿与其它状态统一走「详情」(独立详情页 / 详情弹窗),
|
||||
// 不再对草稿单独降级为 avue 的「查看」表单弹窗,避免同一列出现两种弹窗形态。
|
||||
if (!this.config.detailButton || !this.hasPermission(`${this.config.permission}_view`))
|
||||
return false;
|
||||
const status = this.statusValue(row);
|
||||
return status !== 'draft';
|
||||
return true;
|
||||
},
|
||||
formatDetailValue(row, prop) {
|
||||
if (!row) return '-';
|
||||
@@ -5877,6 +5993,8 @@ export default {
|
||||
weight: record.weight || '',
|
||||
volume: record.volume || '',
|
||||
quantity: record.quantity || '',
|
||||
// 里程只有正整数口径,后端若仍下发 "200.00" 会过不了提交时的正整数校验
|
||||
mileage: Number(record.mileage) > 0 ? String(Number(record.mileage)) : '',
|
||||
remark: record.remark || '',
|
||||
photos: (record.photos || [])
|
||||
.filter(photo => photo?.url)
|
||||
@@ -5952,10 +6070,22 @@ export default {
|
||||
photos.splice(index, 1);
|
||||
this.waybillPunchEditForm.photos = photos;
|
||||
},
|
||||
// 里程只收整数:非数字直接丢掉,长度与「维护里程」一致(≤10 位)
|
||||
handleWaybillPunchMileageInput(value) {
|
||||
this.waybillPunchEditForm.mileage = String(value || '')
|
||||
.replace(/\D/g, '')
|
||||
.slice(0, 10);
|
||||
},
|
||||
async submitWaybillPunchRecordEdit() {
|
||||
await this.$refs.waybillPunchRecordEditFormRef.validate();
|
||||
const record = this.waybillPunchEditRecord;
|
||||
const form = this.waybillPunchEditForm;
|
||||
// 管理端里程选填:留空表示不改动运单里程,所以只校验填了值的情况
|
||||
const mileageText = String(form.mileage || '').trim();
|
||||
if (this.waybillPunchEditMileageVisible && mileageText && !/^[1-9]\d{0,9}$/.test(mileageText)) {
|
||||
this.$message.warning('里程必须为不超过10位的正整数');
|
||||
return;
|
||||
}
|
||||
const submit =
|
||||
typeof this.api?.savePunchRecord === 'function' ? this.api.savePunchRecord : savePunchRecord;
|
||||
this.waybillPunchRecordEditDialog.submitting = true;
|
||||
@@ -5974,6 +6104,8 @@ export default {
|
||||
weight: form.weight,
|
||||
volume: form.volume,
|
||||
quantity: form.quantity,
|
||||
// 不传代表不改;后端按 null 处理
|
||||
mileage: mileageText ? Number(mileageText) : undefined,
|
||||
remark: form.remark,
|
||||
photos: form.photos.map(photo => ({ url: photo.url, type: photo.type })),
|
||||
});
|
||||
@@ -6494,6 +6626,9 @@ export default {
|
||||
if (status === 'completed') {
|
||||
return 'primary';
|
||||
}
|
||||
if (status === 'pending_return_confirm') {
|
||||
return 'warning';
|
||||
}
|
||||
return 'warning';
|
||||
},
|
||||
normalizeRow(row, saveAsDraft = false) {
|
||||
@@ -6504,17 +6639,10 @@ export default {
|
||||
if (isAddMode) {
|
||||
delete submitRow.id;
|
||||
delete submitRow.waybillNo;
|
||||
// 复制提交:状态列未进表单,Avue 可能不带回;显式保留原运单业务状态(含已完成)
|
||||
// 复制新建:业务状态固定为草稿,不沿用原运单状态
|
||||
const copyId = this.$route.query.copyId || '';
|
||||
if (copyId && !saveAsDraft) {
|
||||
const copiedStatus =
|
||||
this.form?.businessStatus ||
|
||||
this.copySourceBusinessStatus ||
|
||||
submitRow.businessStatus ||
|
||||
'';
|
||||
if (copiedStatus) {
|
||||
submitRow.businessStatus = copiedStatus;
|
||||
}
|
||||
if (copyId) {
|
||||
submitRow.businessStatus = this.config.draftStatus || 'draft';
|
||||
}
|
||||
}
|
||||
const skipValidation = saveAsDraft && this.config.skipDraftValidation === true;
|
||||
@@ -6537,14 +6665,16 @@ export default {
|
||||
if (this.config.enableAttachmentTable) {
|
||||
submitRow.attachmentsJson = JSON.stringify(this.attachmentRows);
|
||||
}
|
||||
const isCopyAdd = Boolean(this.$route.query.copyId) && isAddMode;
|
||||
if (this.config.saveAsDraft && saveAsDraft) {
|
||||
submitRow[this.config.draftStatusProp || 'approvalStatus'] =
|
||||
this.config.draftStatus || 'draft';
|
||||
} else if (
|
||||
!isCopyAdd &&
|
||||
this.config.draftStatusProp === 'businessStatus' &&
|
||||
submitRow.businessStatus === (this.config.draftStatus || 'draft')
|
||||
) {
|
||||
// 正式提交:清空草稿态,由后端按过程配置校正为待执行/进行中
|
||||
// 正式提交:清空草稿态,由后端按过程配置校正为待执行/进行中(复制新建除外,保持草稿)
|
||||
submitRow.businessStatus = '';
|
||||
}
|
||||
delete submitRow.basicInfoTitle;
|
||||
@@ -6807,12 +6937,18 @@ export default {
|
||||
}
|
||||
},
|
||||
rowSave(row, done, loading) {
|
||||
const submitRow = this.normalizeRow(row);
|
||||
// 复制页提交走草稿保存,避免 submit 接口把 draft 校正为待执行/进行中
|
||||
const isCopySubmit = Boolean(this.$route.query.copyId);
|
||||
const submitRow = this.normalizeRow(row, isCopySubmit);
|
||||
if (!submitRow) {
|
||||
this.stopSubmitLoading(loading);
|
||||
return;
|
||||
}
|
||||
this.getSaveRequest()(submitRow).then(
|
||||
const request =
|
||||
isCopySubmit && typeof this.api.saveDraft === 'function'
|
||||
? this.api.saveDraft
|
||||
: this.getSaveRequest();
|
||||
request(submitRow).then(
|
||||
res => {
|
||||
this.markWaybillListNeedRefresh();
|
||||
this.onLoad(this.page);
|
||||
@@ -6941,13 +7077,14 @@ export default {
|
||||
if (type === 'add' && copyId) {
|
||||
this.api.getDetail(copyId).then(res => {
|
||||
const sourceData = res.data.data || {};
|
||||
// 原运单业务状态(如 completed)提交时需原样带回并生成应收应付
|
||||
this.copySourceBusinessStatus = sourceData.businessStatus || '';
|
||||
// 清除不应该复制的字段(业务状态与原运单保持一致)
|
||||
this.copySourceBusinessStatus = '';
|
||||
// 清除不应该复制的字段;复制新建固定为草稿
|
||||
delete sourceData.id;
|
||||
delete sourceData.code;
|
||||
delete sourceData.waybillNo;
|
||||
delete sourceData.waybillStatus;
|
||||
delete sourceData.businessStatus;
|
||||
delete sourceData.businessStatusName;
|
||||
delete sourceData.driverAcceptStatus;
|
||||
delete sourceData.driverAcceptTime;
|
||||
delete sourceData.driverAcceptDriverId;
|
||||
@@ -6964,11 +7101,8 @@ export default {
|
||||
delete sourceData.updateUserName;
|
||||
|
||||
this.applyFormDetail(sourceData);
|
||||
// 标记为手工创建;业务状态沿用原运单(含已完成)
|
||||
this.form.dataSource = '手工创建';
|
||||
if (this.copySourceBusinessStatus) {
|
||||
this.form.businessStatus = this.copySourceBusinessStatus;
|
||||
}
|
||||
this.form.businessStatus = this.config.draftStatus || 'draft';
|
||||
done();
|
||||
});
|
||||
return;
|
||||
@@ -7894,19 +8028,10 @@ export default {
|
||||
this.taskDriverOptions = [];
|
||||
this.taskEscortOptions = [];
|
||||
},
|
||||
getTaskCarrierFilter() {
|
||||
return {
|
||||
carrierId: this.form.carrierId || '',
|
||||
carrierName: this.form.carrierName || '',
|
||||
};
|
||||
},
|
||||
loadTaskDriverOptions() {
|
||||
if (!this.taskInfoFormEnabled || this.taskDriverLoading) return Promise.resolve([]);
|
||||
this.taskDriverLoading = true;
|
||||
return fetchDriversByCarrierOrganizations(
|
||||
{ size: 9999, posts: '司机' },
|
||||
this.getTaskCarrierFilter()
|
||||
)
|
||||
return fetchDriverCandidates({ size: 9999, posts: '司机' })
|
||||
.then(records => {
|
||||
this.taskDriverOptions = records;
|
||||
return this.taskDriverOptions;
|
||||
@@ -7917,16 +8042,8 @@ export default {
|
||||
},
|
||||
fetchTaskDriverSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getTaskCarrierFilter();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
this.taskDriverLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
|
||||
.then(records => {
|
||||
this.taskDriverOptions = records;
|
||||
callback(
|
||||
@@ -7946,16 +8063,12 @@ export default {
|
||||
},
|
||||
fetchTaskEscortSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getTaskCarrierFilter();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
this.taskEscortLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({
|
||||
size: 20,
|
||||
...(keyword ? { driverName: keyword } : {}),
|
||||
posts: '押运员',
|
||||
})
|
||||
.then(records => {
|
||||
this.taskEscortOptions = records;
|
||||
callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' })));
|
||||
@@ -10102,8 +10215,14 @@ export default {
|
||||
cancel: '取消',
|
||||
complete: this.completeActionLabel,
|
||||
reassign: '重新派单',
|
||||
confirmReturn: '确认回单',
|
||||
}[action];
|
||||
this.$confirm(`确定${actionName}该${this.config.title}?`, '提示', {
|
||||
// 确认回单是终态动作(本轮不做撤销),把后果写在提示里,别让人以为还能退回
|
||||
const confirmText =
|
||||
action === 'confirmReturn'
|
||||
? '确认回单后该运单将置为已完成,且不可撤销。是否继续?'
|
||||
: `确定${actionName}该${this.config.title}?`;
|
||||
this.$confirm(confirmText, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
@@ -10156,20 +10275,8 @@ export default {
|
||||
},
|
||||
fetchReassignDriverSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim();
|
||||
const row = this.reassignDrawer.row || {};
|
||||
const carrier = {
|
||||
carrierId: row.carrierId || '',
|
||||
carrierName: row.carrierName || '',
|
||||
};
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
this.reassignDrawer.driverLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
|
||||
.then(records => {
|
||||
this.reassignDriverOptions = records;
|
||||
callback(
|
||||
@@ -10308,7 +10415,14 @@ export default {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
this.$confirm('确定完成选中的运单?', '提示', {
|
||||
const pendingReturnCount = this.selectionList.filter(
|
||||
row => this.statusValue(row) === 'pending_return_confirm'
|
||||
).length;
|
||||
const tip =
|
||||
pendingReturnCount > 0
|
||||
? `确定完成选中的运单?其中 ${pendingReturnCount} 条为待回单确认,确认后将转为已完成。`
|
||||
: '确定完成选中的运单?';
|
||||
this.$confirm(tip, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
@@ -10322,25 +10436,38 @@ export default {
|
||||
this.onLoad(this.page, this.query);
|
||||
});
|
||||
},
|
||||
handleRoadLoading() {
|
||||
async handleRoadLoading() {
|
||||
if (!this.selectionList.length) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
const nonRoadWaybills = this.selectionList.filter(row => !this.waybillIsRoadTransport(row));
|
||||
if (nonRoadWaybills.length) {
|
||||
this.$message.warning('公路配载仅支持运输方式为“公路运输”的运单');
|
||||
const selectedIds = this.selectionList.map(row => row.id).filter(Boolean);
|
||||
if (!selectedIds.length) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
const unavailableWaybills = this.selectionList.filter(
|
||||
row =>
|
||||
!['processing', 'running'].includes(this.statusValue(row)) ||
|
||||
row.loadingId ||
|
||||
row.loadingManageId ||
|
||||
row.loadingNo
|
||||
);
|
||||
if (unavailableWaybills.length) {
|
||||
this.$message.warning('公路配载仅支持进行中且未配载的运单');
|
||||
// 可配载与否只看后端查询结果
|
||||
let loadableIds;
|
||||
try {
|
||||
const res = await getWaybillList(1, selectedIds.length, {
|
||||
loadableOnly: 1,
|
||||
ids: selectedIds.join(','),
|
||||
});
|
||||
loadableIds = new Set(extractRecords(res).map(row => String(row.id)));
|
||||
} catch (error) {
|
||||
this.$message.error('校验运单可配载状态失败,请重试');
|
||||
return;
|
||||
}
|
||||
const rejectedRows = this.selectionList.filter(row => !loadableIds.has(String(row.id)));
|
||||
if (rejectedRows.length) {
|
||||
const rejectedText = rejectedRows
|
||||
.map(row => {
|
||||
const reason = this.roadLoadingRejectReasonText(row);
|
||||
const no = row.waybillNo || row.id || '';
|
||||
return `${no}(${reason})`;
|
||||
})
|
||||
.join('、');
|
||||
this.$message.warning(`${rejectedText};公路配载仅支持公路、待执行且未配载的运单`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -10363,6 +10490,13 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
// 只给后端判定为不可配载的运单生成可读原因,本身不参与准入判定
|
||||
roadLoadingRejectReasonText(row = {}) {
|
||||
if (!this.waybillIsRoadTransport(row)) return '非公路运输';
|
||||
if (String(row.businessStatus || '') !== 'pending') return '状态不是待执行';
|
||||
if (row.loadingNo) return '已关联配载单';
|
||||
return '运单数据可能已变化,请刷新后重试';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -11843,7 +11977,65 @@ export default {
|
||||
}
|
||||
|
||||
&__route-change-record-content {
|
||||
white-space: pre-wrap;
|
||||
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 {
|
||||
|
||||
@@ -8,11 +8,17 @@
|
||||
<el-form-item label="变更类型" class="contract-basic-section__change-type">
|
||||
<el-radio-group v-model="form.changeType">
|
||||
<el-radio label="合同信息变更" />
|
||||
<el-radio label="结算规则变更" />
|
||||
<el-radio label="终止合同" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="签约类型" prop="signType">
|
||||
<el-select v-model="form.signType" placeholder="请选择签约类型" clearable>
|
||||
<el-select
|
||||
v-model="form.signType"
|
||||
placeholder="请选择签约类型"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in signTypeOptions"
|
||||
:key="item.value"
|
||||
@@ -21,8 +27,14 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同编号">
|
||||
<el-input v-model="form.contractNo" disabled />
|
||||
<el-form-item label="合同编号" prop="contractNo">
|
||||
<el-input
|
||||
v-model="form.contractNo"
|
||||
maxlength="100"
|
||||
clearable
|
||||
placeholder="请输入合同编号"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同名称" prop="contractName">
|
||||
<el-input
|
||||
@@ -30,6 +42,7 @@
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
placeholder="请输入合同名称"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同类型">
|
||||
@@ -80,6 +93,7 @@
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
<span>至</span>
|
||||
<el-date-picker
|
||||
@@ -88,6 +102,7 @@
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -95,6 +110,7 @@
|
||||
<el-input
|
||||
v-model="form.paymentDays"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
@input="value => positiveIntegerInput('paymentDays', value)"
|
||||
>
|
||||
<template #suffix>天</template>
|
||||
@@ -105,22 +121,36 @@
|
||||
:model-value="contractAmountDisplay"
|
||||
placeholder="请输入"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
@input="handleContractAmountInput"
|
||||
@blur="formatContractAmountField"
|
||||
@clear="form.contractAmount = null"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否范本">
|
||||
<el-select v-model="form.templateFlag" placeholder="请选择">
|
||||
<el-select
|
||||
v-model="form.templateFlag"
|
||||
placeholder="请选择"
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option label="否" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="原件合同编号">
|
||||
<el-input v-model="form.originalContractNo" maxlength="100" placeholder="请输入" />
|
||||
<el-input
|
||||
v-model="form.originalContractNo"
|
||||
maxlength="100"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否电子章">
|
||||
<el-select v-model="form.electronicSealFlag" placeholder="请选择">
|
||||
<el-select
|
||||
v-model="form.electronicSealFlag"
|
||||
placeholder="请选择"
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option label="否" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
@@ -129,13 +159,19 @@
|
||||
<el-input
|
||||
v-model="form.invoiceCycle"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
@input="value => positiveIntegerInput('invoiceCycle', value)"
|
||||
>
|
||||
<template #suffix>天</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="结算币种" prop="settlementCurrency">
|
||||
<el-select v-model="form.settlementCurrency" placeholder="请选择结算币种" clearable>
|
||||
<el-select
|
||||
v-model="form.settlementCurrency"
|
||||
placeholder="请选择结算币种"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in settlementCurrencyOptions"
|
||||
:key="item.value"
|
||||
@@ -145,7 +181,12 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同格式">
|
||||
<el-select v-model="form.contractFormat" placeholder="请选择合同格式" clearable>
|
||||
<el-select
|
||||
v-model="form.contractFormat"
|
||||
placeholder="请选择合同格式"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in contractFormatOptions"
|
||||
:key="item.value"
|
||||
@@ -158,19 +199,30 @@
|
||||
<el-input
|
||||
v-model="form.copyCount"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
@input="value => positiveIntegerInput('copyCount', value)"
|
||||
>
|
||||
<template #suffix>份</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否需要加盖法人章">
|
||||
<el-select v-model="form.legalSealFlag" placeholder="请选择" clearable>
|
||||
<el-select
|
||||
v-model="form.legalSealFlag"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option label="否" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="结算方式">
|
||||
<el-select v-model="form.settlementMode" placeholder="请选择结算方式" clearable>
|
||||
<el-select
|
||||
v-model="form.settlementMode"
|
||||
placeholder="请选择结算方式"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in settlementModeOptions"
|
||||
:key="item.value"
|
||||
@@ -191,6 +243,7 @@
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入合同主要内容"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="其他事项说明" class="contract-basic-section__remark">
|
||||
@@ -201,14 +254,21 @@
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入其他事项说明"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 风险客商提示:合同已推送合规并留痕高风险/黑名单时展示,变更提交同样要求特批附件 -->
|
||||
<div v-if="riskTipText" class="contract-manage-form__risk-tip">
|
||||
<div class="contract-manage-form__risk-tip-title">风险客商提示:</div>
|
||||
<div class="contract-manage-form__risk-tip-content">{{ riskTipText }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<contract-attachment-section
|
||||
title="用印材料"
|
||||
description
|
||||
attachment-type
|
||||
:readonly="isSettlementRuleChange"
|
||||
:attachment-location-options="changeAttachmentLocationOptions"
|
||||
:rows="contractFileRows"
|
||||
:preview="previewAttachment"
|
||||
@@ -217,9 +277,38 @@
|
||||
/>
|
||||
|
||||
<section class="change-section">
|
||||
<div class="section-head"><div class="dialog-section-title">计费信息</div><el-button type="primary" plain @click="addPlan">添加</el-button></div>
|
||||
<el-radio-group v-model="feeGenerationMode"><el-radio label="system">系统生成</el-radio><el-radio label="manual">账单导入生成</el-radio></el-radio-group>
|
||||
<el-table :data="plans" border class="change-table"><el-table-column type="index" label="序号" width="70" /><el-table-column prop="planName" label="方案名称" min-width="220" /><el-table-column label="默认方案" min-width="120"><template #default="{ row }">{{ row.defaultPlan ? '是' : '否' }}</template></el-table-column><el-table-column prop="remark" label="备注" min-width="260" /><el-table-column label="操作" width="180"><template #default="{ row, $index }"><el-link type="primary" @click="editPlan(row, $index)">编辑</el-link><el-link type="danger" @click="plans.splice($index, 1)">删除</el-link></template></el-table-column></el-table>
|
||||
<div class="section-head">
|
||||
<div class="dialog-section-title">计费信息</div>
|
||||
<el-button
|
||||
v-if="feeGenerationMode !== 'manual'"
|
||||
type="primary"
|
||||
plain
|
||||
@click="addPlan"
|
||||
>添加</el-button
|
||||
>
|
||||
</div>
|
||||
<el-radio-group v-model="feeGenerationMode"
|
||||
><el-radio label="system">系统生成</el-radio
|
||||
><el-radio label="manual">账单导入生成</el-radio></el-radio-group
|
||||
>
|
||||
<el-table :data="plans" border class="change-table"
|
||||
><el-table-column type="index" label="序号" width="70" /><el-table-column
|
||||
prop="planName"
|
||||
label="方案名称"
|
||||
min-width="220"
|
||||
/><el-table-column label="默认方案" min-width="120"
|
||||
><template #default="{ row }">{{
|
||||
row.defaultPlan ? '是' : '否'
|
||||
}}</template></el-table-column
|
||||
><el-table-column prop="remark" label="备注" min-width="260" /><el-table-column
|
||||
label="操作"
|
||||
width="180"
|
||||
><template #default="{ row, $index }"
|
||||
><el-link type="primary" @click="editPlan(row, $index)">编辑</el-link
|
||||
><el-link type="danger" @click="plans.splice($index, 1)">删除</el-link></template
|
||||
></el-table-column
|
||||
></el-table
|
||||
>
|
||||
</section>
|
||||
|
||||
<section class="change-section">
|
||||
@@ -238,19 +327,106 @@
|
||||
<el-radio :label="0">关闭</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<el-form v-if="settlementRule.autoGenerate === 1" :model="settlementRule" label-position="right" label-width="auto" class="settlement-form">
|
||||
<el-form-item label="账单起始日" required><el-date-picker v-model="settlementRule.billStartDate" type="date" placeholder="请选择账单起始日" format="YYYY-MM-DD" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="结算类型" required><el-select v-model="settlementRule.settlementType" placeholder="请选择结算类型" @change="handleSettlementTypeChange"><el-option v-for="item in settlementTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementBillCycleType" label="结算周期" required><el-select v-model="settlementRule.billCycleType" placeholder="请选择结算周期" @change="handleCycleTypeChange"><el-option v-for="item in billCycleTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日" required><el-select v-model="settlementRule.billCutoffDay" placeholder="请选择账单截单日"><el-option v-for="item in billCutoffDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementCycleDays" label="周期天数" required><el-select v-model="settlementRule.cycleDays" placeholder="请选择周期天数"><el-option v-for="item in cycleDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
|
||||
<el-form
|
||||
v-if="settlementRule.autoGenerate === 1"
|
||||
:model="settlementRule"
|
||||
label-position="right"
|
||||
label-width="auto"
|
||||
class="settlement-form"
|
||||
>
|
||||
<el-form-item label="账单起始日" required
|
||||
><el-date-picker
|
||||
v-model="settlementRule.billStartDate"
|
||||
type="date"
|
||||
placeholder="请选择账单起始日"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
/></el-form-item>
|
||||
<el-form-item label="结算类型" required
|
||||
><el-select
|
||||
v-model="settlementRule.settlementType"
|
||||
placeholder="请选择结算类型"
|
||||
@change="handleSettlementTypeChange"
|
||||
><el-option
|
||||
v-for="item in settlementTypeOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementBillCycleType" label="结算周期" required
|
||||
><el-select
|
||||
v-model="settlementRule.billCycleType"
|
||||
placeholder="请选择结算周期"
|
||||
@change="handleCycleTypeChange"
|
||||
><el-option
|
||||
v-for="item in billCycleTypeOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日" required
|
||||
><el-select v-model="settlementRule.billCutoffDay" placeholder="请选择账单截单日"
|
||||
><el-option
|
||||
v-for="item in billCutoffDayOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementCycleDays" label="周期天数" required
|
||||
><el-select v-model="settlementRule.cycleDays" placeholder="请选择周期天数"
|
||||
><el-option
|
||||
v-for="item in cycleDayOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/></el-select></el-form-item>
|
||||
</el-form>
|
||||
<el-table v-if="settlementRule.autoGenerate === 1 && showSettlementCustomPeriods" :data="settlementRule.customPeriods || []" border class="change-table custom-periods-table">
|
||||
<el-table
|
||||
v-if="settlementRule.autoGenerate === 1 && showSettlementCustomPeriods"
|
||||
:data="settlementRule.customPeriods || []"
|
||||
border
|
||||
class="change-table custom-periods-table"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="70" />
|
||||
<el-table-column label="运单区间-开始日" min-width="180"><template #default="{ row, $index }"><el-select :model-value="row.startDay" placeholder="请选择" clearable @update:model-value="value => handleCustomPeriodStartDayChange($index, value)"><el-option v-for="item in billCutoffDayOptions" :key="`start-${item.value}`" :label="item.label" :value="item.value" /></el-select></template></el-table-column>
|
||||
<el-table-column label="运单区间-结束日" min-width="200"><template #default="{ row, $index }"><el-select :model-value="row.endDay" placeholder="请选择" clearable @update:model-value="value => handleCustomPeriodEndDayChange($index, value)"><el-option v-for="item in customPeriodEndDayOptions(row)" :key="item.value" :label="item.label" :value="item.value" /></el-select></template></el-table-column>
|
||||
<el-table-column label="操作" width="100"><template #default="{ $index }"><el-link v-if="$index === 0" type="primary" @click="addCustomPeriodRow">添加</el-link><el-link v-else type="danger" @click="removeCustomPeriodRow($index)">删除</el-link></template></el-table-column>
|
||||
<template #empty><el-link type="primary" @click="addCustomPeriodRow">添加</el-link></template>
|
||||
<el-table-column label="运单区间-开始日" min-width="180"
|
||||
><template #default="{ row, $index }"
|
||||
><el-select
|
||||
:model-value="row.startDay"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
@update:model-value="value => handleCustomPeriodStartDayChange($index, value)"
|
||||
><el-option
|
||||
v-for="item in billCutoffDayOptions"
|
||||
:key="`start-${item.value}`"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/></el-select></template
|
||||
></el-table-column>
|
||||
<el-table-column label="运单区间-结束日" min-width="200"
|
||||
><template #default="{ row, $index }"
|
||||
><el-select
|
||||
:model-value="row.endDay"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
@update:model-value="value => handleCustomPeriodEndDayChange($index, value)"
|
||||
><el-option
|
||||
v-for="item in customPeriodEndDayOptions(row)"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/></el-select></template
|
||||
></el-table-column>
|
||||
<el-table-column label="操作" width="100"
|
||||
><template #default="{ $index }"
|
||||
><el-link v-if="$index === 0" type="primary" @click="addCustomPeriodRow">添加</el-link
|
||||
><el-link v-else type="danger" @click="removeCustomPeriodRow($index)"
|
||||
>删除</el-link
|
||||
></template
|
||||
></el-table-column
|
||||
>
|
||||
<template #empty
|
||||
><el-link type="primary" @click="addCustomPeriodRow">添加</el-link></template
|
||||
>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
@@ -258,17 +434,48 @@
|
||||
<div class="dialog-section-title">
|
||||
付款比例设置
|
||||
<el-tooltip content="非必填;配置付款比例时,合计必须等于100%。" placement="top">
|
||||
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"><el-icon-question-filled /></el-icon>
|
||||
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"
|
||||
><el-icon-question-filled
|
||||
/></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-table :data="paymentRatioRows" border class="change-table">
|
||||
<el-table-column type="index" label="序号" width="70" />
|
||||
<el-table-column prop="paymentTerm" label="付款笔数" min-width="180" />
|
||||
<el-table-column label="付款比例上限(%)" min-width="220"><template #default="{ row }"><el-input-number v-model="row.ratioLimit" :min="0" :max="100" :precision="2" controls-position="right" /></template></el-table-column>
|
||||
<el-table-column label="备注" min-width="220"><template #default="{ row }"><el-input v-model="row.remark" maxlength="200" /></template></el-table-column>
|
||||
<el-table-column label="操作" width="100"><template #default="{ $index }"><el-link type="danger" @click="paymentRatioRows.splice($index, 1)">删除</el-link></template></el-table-column>
|
||||
<el-table-column label="付款比例上限(%)" min-width="220"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-model="row.ratioLimit"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/></template
|
||||
></el-table-column>
|
||||
<el-table-column label="备注" min-width="220"
|
||||
><template #default="{ row }"
|
||||
><el-input
|
||||
v-model="row.remark"
|
||||
maxlength="200"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/></template
|
||||
></el-table-column>
|
||||
<el-table-column v-if="!isSettlementRuleChange" label="操作" width="100"
|
||||
><template #default="{ $index }"
|
||||
><el-link type="danger" @click="paymentRatioRows.splice($index, 1)"
|
||||
>删除</el-link
|
||||
></template
|
||||
></el-table-column
|
||||
>
|
||||
</el-table>
|
||||
<el-button type="primary" plain @click="addPaymentRatioRow">添加</el-button>
|
||||
<el-button
|
||||
v-if="!isSettlementRuleChange"
|
||||
type="primary"
|
||||
plain
|
||||
@click="addPaymentRatioRow"
|
||||
>添加</el-button
|
||||
>
|
||||
</section>
|
||||
|
||||
<billing-plan-editor
|
||||
@@ -285,6 +492,7 @@
|
||||
attachment-type
|
||||
attachment-location="附件(佐证材料等)"
|
||||
attachment-type-dict-code="contract_supporting_attachment_type"
|
||||
:readonly="isSettlementRuleChange"
|
||||
:attachment-location-options="changeAttachmentLocationOptions"
|
||||
:rows="attachments"
|
||||
:preview="previewAttachment"
|
||||
@@ -332,7 +540,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,
|
||||
@@ -440,7 +648,7 @@ export default {
|
||||
},
|
||||
preSettlementConfig: {},
|
||||
formalSettlementConfig: {},
|
||||
settlementTypeOptions: ['月结', '日结', '周结', '半月结', '固定天数周期结算'],
|
||||
settlementTypeOptions: ['月结', '日结', '周结', '固定天数周期结算'],
|
||||
billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'],
|
||||
billCutoffDayOptions: Array.from({ length: 31 }, (_, index) => ({
|
||||
label: `${index + 1}日`,
|
||||
@@ -465,6 +673,24 @@ export default {
|
||||
'按数量',
|
||||
],
|
||||
rules: {
|
||||
contractNo: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (this.isSettlementRuleChange) {
|
||||
return callback();
|
||||
}
|
||||
const contractNo = String(value || '').trim();
|
||||
if (!contractNo) {
|
||||
return callback(new Error('请输入合同编号'));
|
||||
}
|
||||
if (contractNo.length > 100) {
|
||||
return callback(new Error('合同编号不能超过100个字符'));
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
|
||||
contractAmount: [
|
||||
{ required: true, message: '请输入合同金额', trigger: 'blur' },
|
||||
@@ -489,7 +715,24 @@ export default {
|
||||
pageTitle() {
|
||||
return this.$route.query.name || '合同变更';
|
||||
},
|
||||
// 高风险/黑名单客商提示:按合同推送合规留痕 hcs_risk_level/hcs_black_status 判定(与合同详情口径一致)
|
||||
riskTipText() {
|
||||
const riskLevel = String(this.form.hcsRiskLevel || '').trim();
|
||||
const blacklist = String(this.form.hcsBlackStatus || '').toLowerCase() === 'yes';
|
||||
if (riskLevel !== '2' && !blacklist) return '';
|
||||
const parties = [this.form.partyA, this.form.partyB]
|
||||
.map(name => String(name || '').trim())
|
||||
.filter(Boolean)
|
||||
.join('、');
|
||||
return `【${parties || '相对方'}】:被列入外部黑名单;涉贪污受贿裁判相关提及方(3年以上及其他);被执行人信息;买卖合同纠纷;劳动纠纷;重大纠纷。提交变更前须上传「特批附件」类型附件`;
|
||||
},
|
||||
isSettlementRuleChange() {
|
||||
return this.form.changeType === '结算规则变更';
|
||||
},
|
||||
changeAttachmentLocationOptions() {
|
||||
if (this.isSettlementRuleChange) {
|
||||
return ['变更材料'];
|
||||
}
|
||||
return ['用印材料', '附件(佐证材料等)', '变更材料'];
|
||||
},
|
||||
contractAmountDisplay() {
|
||||
@@ -715,6 +958,12 @@ export default {
|
||||
previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; },
|
||||
handleAttachmentUploadToLocation({ location, rows = [] }) {
|
||||
if (!rows.length) return;
|
||||
if (this.isSettlementRuleChange) {
|
||||
if (location === '变更材料') {
|
||||
this.changeMaterials = [...(this.changeMaterials || []), ...rows];
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isContractSealMaterialLocation(location)) {
|
||||
this.contractFileRows = [...(this.contractFileRows || []), ...rows];
|
||||
return;
|
||||
@@ -738,7 +987,91 @@ 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() {
|
||||
if (this.form.changeType === '终止合同') {
|
||||
await api.terminate(this.form.id, this.form.changeReason);
|
||||
this.$message.success('合同已终止');
|
||||
this.$router.back();
|
||||
return;
|
||||
}
|
||||
await this.$refs.formRef.validate();
|
||||
if (!this.isSettlementRuleChange) {
|
||||
const contractNo = String(this.form.contractNo || '').trim();
|
||||
if (!contractNo) {
|
||||
this.$message.warning('请输入合同编号');
|
||||
return;
|
||||
}
|
||||
if (contractNo.length > 100) {
|
||||
this.$message.warning('合同编号不能超过100个字符');
|
||||
return;
|
||||
}
|
||||
this.form.contractNo = contractNo;
|
||||
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,
|
||||
};
|
||||
const payload = this.isSettlementRuleChange
|
||||
? {
|
||||
id: this.form.id,
|
||||
changeType: this.form.changeType,
|
||||
changeContent: this.form.changeContent,
|
||||
changeReason: this.form.changeReason,
|
||||
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),
|
||||
changeAttachmentsJson: JSON.stringify(this.changeMaterials),
|
||||
}
|
||||
: {
|
||||
...this.form,
|
||||
changeType: this.form.changeType || '合同信息变更',
|
||||
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 api.submitChange(payload);
|
||||
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>
|
||||
@@ -754,7 +1087,7 @@ export default {
|
||||
.contract-basic-section__change-type { grid-column: 1 / -1; }
|
||||
.contract-basic-section__remark {
|
||||
width: 100%;
|
||||
margin-bottom: 24px !important;
|
||||
margin-bottom: 23px !important;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
@@ -762,8 +1095,12 @@ export default {
|
||||
}
|
||||
.contract-basic-section__date-range { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 8px; width: 360px; max-width: 100%; }
|
||||
.contract-basic-section__date-range span { color: #606266; }
|
||||
// 风险客商提示条(基本信息备注后):橙黄背景 + 红色文字,与合同新增页一致
|
||||
.contract-manage-form__risk-tip { margin: 0 0 18px; padding: 10px 16px; background: #faecd8; border-radius: 4px; }
|
||||
.contract-manage-form__risk-tip-title { color: #f56c6c; font-weight: 600; line-height: 22px; }
|
||||
.contract-manage-form__risk-tip-content { color: #f56c6c; line-height: 22px; }
|
||||
.contract-basic-section__date-range :deep(.el-date-editor) { width: auto; min-width: 0; }
|
||||
.contract-basic-section :deep(.el-form-item) { align-items: center; margin-bottom: 24px; }
|
||||
.contract-basic-section :deep(.el-form-item) { align-items: center; }
|
||||
.contract-basic-section :deep(.el-form-item__label) { flex: 0 0 96px !important; width: 96px !important; height: auto; padding-right: 12px; white-space: normal; word-break: break-all; line-height: 18px; }
|
||||
.contract-basic-section :deep(.el-form-item__content) { min-width: 0; }
|
||||
.contract-basic-section :deep(.el-input),
|
||||
|
||||
@@ -32,6 +32,14 @@
|
||||
>
|
||||
批量导出
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
:loading="hcsRepushingId === 'batch'"
|
||||
@click="handleHcsBatchRepush"
|
||||
>
|
||||
<el-icon class="el-icon--left"><Position /></el-icon>批量推送合规
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template #header>
|
||||
@@ -72,6 +80,18 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<template #hcsSyncStatus="{ row }">
|
||||
<el-tooltip v-if="row.hcsSyncMsg" placement="top" :content="row.hcsSyncMsg">
|
||||
<el-tag :type="hcsSyncTagType(row.hcsSyncStatus)" class="status-text">
|
||||
{{ hcsSyncLabel(row.hcsSyncStatus) }}
|
||||
</el-tag>
|
||||
</el-tooltip>
|
||||
<el-tag v-else-if="row.hcsSyncStatus" :type="hcsSyncTagType(row.hcsSyncStatus)" class="status-text">
|
||||
{{ hcsSyncLabel(row.hcsSyncStatus) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template #menu="{ row }">
|
||||
<el-link
|
||||
v-if="hasPermission(`${config.permission}_view`)"
|
||||
@@ -92,6 +112,9 @@
|
||||
>
|
||||
{{ operation.label }}
|
||||
</el-link>
|
||||
<el-link v-if="canHcsRepush(row)" type="warning" @click="handleHcsRepush(row)">
|
||||
{{ hcsRepushingId === row.id ? '推送中…' : '推送合规' }}
|
||||
</el-link>
|
||||
<el-link v-if="canDelete(row)" type="danger" @click="removeRow(row)">删除</el-link>
|
||||
</template>
|
||||
</avue-crud>
|
||||
@@ -180,9 +203,18 @@
|
||||
<el-descriptions-item label="其他事项说明" :span="3">{{
|
||||
detailValue('remark')
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="detailRiskTipText" label="风险客商提示" :span="3">
|
||||
<span style="color: #f56c6c">{{ detailRiskTipText }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</section>
|
||||
|
||||
<hcs-contract-panel
|
||||
v-if="!isPublicViewPage"
|
||||
:contract="detailRow"
|
||||
@refresh="initDetailPage"
|
||||
/>
|
||||
|
||||
<contract-attachment-section
|
||||
title="用印材料"
|
||||
description
|
||||
@@ -302,6 +334,7 @@
|
||||
<contract-attachment-section
|
||||
title="附件(佐证材料等)"
|
||||
description
|
||||
attachment-type
|
||||
readonly
|
||||
:rows="detailAttachmentRows"
|
||||
:preview="previewAttachment"
|
||||
@@ -380,17 +413,15 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同编号">
|
||||
<el-form-item label="合同编号" prop="contractNo">
|
||||
<el-input
|
||||
v-model="form.contractNo"
|
||||
disabled
|
||||
maxlength="100"
|
||||
clearable
|
||||
:placeholder="
|
||||
formMode === 'add'
|
||||
? form.organizationId
|
||||
? '系统按部门规则自动生成'
|
||||
: '请先选择合同负责部门'
|
||||
: ''
|
||||
formMode === 'add' ? '不填写则按部门规则自动生成' : '请输入合同编号'
|
||||
"
|
||||
@input="handleContractNoInput"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同名称" prop="contractName">
|
||||
@@ -448,6 +479,7 @@
|
||||
clearable
|
||||
:loading="contractPartyLoading"
|
||||
@visible-change="visible => visible && loadContractPartyOptions()"
|
||||
@change="queryPartyRisk('partyA')"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in contractPartyAOptions"
|
||||
@@ -465,6 +497,7 @@
|
||||
clearable
|
||||
:loading="contractPartyLoading"
|
||||
@visible-change="visible => visible && loadContractPartyOptions()"
|
||||
@change="queryPartyRisk('partyB')"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in contractPartyBOptions"
|
||||
@@ -613,12 +646,17 @@
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="200"
|
||||
:rows="4"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入其他事项说明"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 风险客商提示(基本信息备注后):外部客商高风险/黑名单时展示,文案由后端 riskTip 拼装 -->
|
||||
<div v-if="partyRiskText" class="contract-manage-form__risk-tip">
|
||||
<div class="contract-manage-form__risk-tip-title">风险客商提示:</div>
|
||||
<div class="contract-manage-form__risk-tip-content">{{ partyRiskText }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<contract-attachment-section
|
||||
@@ -647,7 +685,12 @@
|
||||
<el-radio label="manual">账单导入生成</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<el-button type="primary" @click="openBillingPlan()">添加</el-button>
|
||||
<el-button
|
||||
v-if="form.feeGenerationMode !== 'manual'"
|
||||
type="primary"
|
||||
@click="openBillingPlan()"
|
||||
>添加</el-button
|
||||
>
|
||||
</div>
|
||||
<el-table :data="billingPlanRows" border>
|
||||
<el-table-column type="index" label="序号" width="80" align="center" />
|
||||
@@ -1046,6 +1089,8 @@ import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import * as api from '@/api/business/contract-manage';
|
||||
import { repush as hcsRepush, repushBatch as hcsRepushBatch, getRisk } from '@/api/business/hcs';
|
||||
import { Position } from '@element-plus/icons-vue';
|
||||
import { getMkPublicDetail } from '@/api/mk-process';
|
||||
import { getList as getProjectList } from '@/api/business/project-apply';
|
||||
import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive';
|
||||
@@ -1066,6 +1111,7 @@ import ContractAttachmentSection, {
|
||||
isContractSupportingAttachmentLocation,
|
||||
normalizeContractFileRows,
|
||||
} from './components/contract-attachment-section.vue';
|
||||
import HcsContractPanel from './components/hcs-contract-panel.vue';
|
||||
import PdfPreview from '@/components/pdf-preview/main.vue';
|
||||
import { ElImageViewer, ElCascader } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
@@ -1171,6 +1217,11 @@ const createCopiedDetail = (detail = {}) => {
|
||||
Object.keys(form).forEach(prop => {
|
||||
if (Object.prototype.hasOwnProperty.call(detail, prop)) form[prop] = detail[prop];
|
||||
});
|
||||
// 特批附件属于原合同的特批佐证,复制后须对新合同重新上传,否则会绕过高风险/黑名单校验
|
||||
const stripSpecAttachments = value => {
|
||||
const rows = parseArray(value).filter(item => item?.fileType !== '特批附件');
|
||||
return rows.length ? JSON.stringify(rows) : '';
|
||||
};
|
||||
return {
|
||||
...form,
|
||||
contractNo: '',
|
||||
@@ -1178,8 +1229,8 @@ const createCopiedDetail = (detail = {}) => {
|
||||
archiveStatus: '未归档',
|
||||
feeGenerationMode: detail.feeGenerationMode || '',
|
||||
billingEnabled: detail.billingEnabled,
|
||||
contractFileJson: detail.contractFileJson || '',
|
||||
attachmentsJson: detail.attachmentsJson || '',
|
||||
contractFileJson: stripSpecAttachments(detail.contractFileJson),
|
||||
attachmentsJson: stripSpecAttachments(detail.attachmentsJson),
|
||||
billingPlanJson: detail.billingPlanJson || '',
|
||||
paymentRatioJson: detail.paymentRatioJson || '',
|
||||
settlementRuleJson: detail.settlementRuleJson || '',
|
||||
@@ -1238,9 +1289,11 @@ export default {
|
||||
ContractAttachmentSection,
|
||||
BillingPlanEditor,
|
||||
ChangeRecordDetailDialog,
|
||||
HcsContractPanel,
|
||||
ElImageViewer,
|
||||
OpenFileViewer,
|
||||
PdfPreview,
|
||||
Position,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -1263,7 +1316,23 @@ export default {
|
||||
{ label: '已到期', value: 'expired', statKey: 'expired' },
|
||||
],
|
||||
form: defaultForm(),
|
||||
contractNoManual: false,
|
||||
formRules: {
|
||||
contractNo: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
const contractNo = String(value || '').trim();
|
||||
if (this.formMode === 'edit' && !contractNo) {
|
||||
return callback(new Error('请输入合同编号'));
|
||||
}
|
||||
if (contractNo.length > 100) {
|
||||
return callback(new Error('合同编号不能超过100个字符'));
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
|
||||
contractCategory: [{ required: true, message: '请选择合同类别', trigger: 'change' }],
|
||||
signType: [{ required: true, message: '请选择签约类型', trigger: 'change' }],
|
||||
@@ -1316,6 +1385,9 @@ export default {
|
||||
contractCategoryDictOptions: [],
|
||||
contractPartyAOptions: [],
|
||||
contractPartyBOptions: [],
|
||||
// 合规客商风险(C101-1 展示用):甲方/乙方选择后即时查询
|
||||
contractPartyCodeMap: {},
|
||||
partyRisk: {},
|
||||
contractPartyLoading: false,
|
||||
contractPartyRequestId: 0,
|
||||
organizationOptions: [],
|
||||
@@ -1354,6 +1426,8 @@ export default {
|
||||
billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'],
|
||||
detailLoading: false,
|
||||
detailRow: {},
|
||||
// 合规(HCS)推送/对账状态
|
||||
hcsRepushingId: null,
|
||||
detailContractFileRows: [],
|
||||
detailAttachmentRows: [],
|
||||
detailBillingPlanRows: [],
|
||||
@@ -1530,6 +1604,32 @@ export default {
|
||||
const endDate = this.detailRow.endDate || '-';
|
||||
return `${startDate} 至 ${endDate}`;
|
||||
},
|
||||
// 高风险/黑名单客商签约提示(C101-1,签约前提示;强制拦截在后端提交校验与推送链路)
|
||||
partyRiskText() {
|
||||
const parts = [];
|
||||
['partyA', 'partyB'].forEach(prop => {
|
||||
const risk = this.partyRisk[prop];
|
||||
if (!risk?.risky) return;
|
||||
// riskTip 由后端按风险项拼装(【公司名称】:黑名单;涉诉;被执行人等),缺失时兜底通用提示
|
||||
parts.push(
|
||||
risk.riskTip ||
|
||||
`${prop === 'partyA' ? '甲方' : '乙方'}[${this.form[prop] || ''}]为高风险/黑名单客商`
|
||||
);
|
||||
});
|
||||
if (!parts.length) return '';
|
||||
return `${parts.join(';')},按集团规定不可直接签约,提交前须上传「特批附件」类型附件,审批通过后将自动发起合同特批`;
|
||||
},
|
||||
// 详情风险客商提示(基本信息备注后展示):按推送留痕 hcs_risk_level/hcs_black_status 判定(甲乙取高/任一),文案与后端 riskTip 一致
|
||||
detailRiskTipText() {
|
||||
const riskLevel = String(this.detailRow.hcsRiskLevel || '').trim();
|
||||
const blacklist = String(this.detailRow.hcsBlackStatus || '').toLowerCase() === 'yes';
|
||||
if (riskLevel !== '2' && !blacklist) return '';
|
||||
const parties = [this.detailRow.partyA, this.detailRow.partyB]
|
||||
.map(name => String(name || '').trim())
|
||||
.filter(Boolean)
|
||||
.join('、');
|
||||
return `【${parties || '相对方'}】:被列入外部黑名单;涉贪污受贿裁判相关提及方(3年以上及其他);被执行人信息;买卖合同纠纷;劳动纠纷;重大纠纷`;
|
||||
},
|
||||
detailLegalSealText() {
|
||||
const value = this.detailRow.legalSealFlag;
|
||||
if (value === undefined || value === null || value === '') return '-';
|
||||
@@ -1651,6 +1751,92 @@ export default {
|
||||
if (stage === 'terminated') return 'danger';
|
||||
return 'info';
|
||||
},
|
||||
// ==================== 合规(HCS)推送/对账(照“同步金蝶”按钮模式) ====================
|
||||
hcsSyncLabel(value) {
|
||||
return { unsynced: '待推送', synced: '已推送', failed: '推送失败' }[value] || value;
|
||||
},
|
||||
hcsSyncTagType(value) {
|
||||
return { unsynced: 'info', synced: 'success', failed: 'danger' }[value] || 'info';
|
||||
},
|
||||
canHcsRepush(row) {
|
||||
return (
|
||||
['approved', 'change_approved'].includes(row.approvalStatus) &&
|
||||
row.hcsSyncStatus !== 'synced'
|
||||
);
|
||||
},
|
||||
handleHcsRepush(row) {
|
||||
if (this.hcsRepushingId) return;
|
||||
this.$confirm('确定推送该数据到合同合规系统?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
// 同步推送合规耗时较长,加遮罩防重复操作(照审批通过遮罩)
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在推送合规,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
this.hcsRepushingId = row.id;
|
||||
hcsRepush('contract', row.id)
|
||||
.then(res => {
|
||||
if (res.data?.success === false) {
|
||||
this.$message.error(res.data?.msg || '推送合规失败');
|
||||
return;
|
||||
}
|
||||
this.$message.success(res.data?.data || '推送合规成功');
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.catch(() => {
|
||||
// 失败原因由 axios 拦截器统一弹出后端 msg,此处不再重复提示
|
||||
})
|
||||
.finally(() => {
|
||||
this.hcsRepushingId = null;
|
||||
loading.close();
|
||||
});
|
||||
});
|
||||
},
|
||||
handleHcsBatchRepush() {
|
||||
if (this.hcsRepushingId) return;
|
||||
if (!this.selectionList.length) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
const invalidCount = this.selectionList.filter(item => !this.canHcsRepush(item)).length;
|
||||
if (invalidCount) {
|
||||
this.$message.warning(`选中数据中有${invalidCount}条不符合推送条件(仅审批通过且未同步的可推送),请重新选择`);
|
||||
return;
|
||||
}
|
||||
this.$confirm(`确定将选中的${this.selectionList.length}条数据推送到合同合规系统?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
// 同步推送合规耗时较长,加遮罩防重复操作(照审批通过遮罩)
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在批量推送合规,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
this.hcsRepushingId = 'batch';
|
||||
hcsRepushBatch('contract', this.selectionList.map(item => item.id))
|
||||
.then(res => {
|
||||
if (res.data?.success === false) {
|
||||
this.$message.error(res.data?.msg || '批量推送合规失败');
|
||||
return;
|
||||
}
|
||||
this.$message.success(res.data?.data || '批量推送合规完成');
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.catch(() => {
|
||||
// 失败原因由 axios 拦截器统一弹出后端 msg,此处不再重复提示
|
||||
})
|
||||
.finally(() => {
|
||||
this.hcsRepushingId = null;
|
||||
loading.close();
|
||||
});
|
||||
});
|
||||
},
|
||||
canEdit(row) {
|
||||
return (
|
||||
this.hasPermission(`${this.config.permission}_edit`) &&
|
||||
@@ -1777,6 +1963,7 @@ export default {
|
||||
this.api.getDetail(id).then(res => this.applyDetail(res.data?.data || {}));
|
||||
},
|
||||
resetFormState() {
|
||||
this.contractNoManual = false;
|
||||
this.form = {
|
||||
...defaultForm(),
|
||||
handlerUserName:
|
||||
@@ -1852,7 +2039,20 @@ export default {
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push(contractManageListRoute);
|
||||
},
|
||||
validateContractNo() {
|
||||
const contractNo = String(this.form.contractNo || '').trim();
|
||||
if (this.formMode === 'edit' && !contractNo) {
|
||||
this.$message.warning('请输入合同编号');
|
||||
return false;
|
||||
}
|
||||
if (contractNo.length > 100) {
|
||||
this.$message.warning('合同编号不能超过100个字符');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
validateBase(action) {
|
||||
if (!this.validateContractNo()) return false;
|
||||
if (action === 'temporary') {
|
||||
const required = [
|
||||
['signType', '签约类型'],
|
||||
@@ -1938,6 +2138,7 @@ export default {
|
||||
const projectName = projectId ? normalizeProjectValue(this.form.projectName) : '';
|
||||
return {
|
||||
...this.form,
|
||||
contractNo: String(this.form.contractNo || '').trim(),
|
||||
projectId,
|
||||
projectName,
|
||||
copyCount: normalizeOptionalInteger(this.form.copyCount),
|
||||
@@ -1986,7 +2187,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 +2228,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();
|
||||
@@ -2209,8 +2413,21 @@ export default {
|
||||
])
|
||||
.then(responses => {
|
||||
if (requestId !== this.contractPartyRequestId) return;
|
||||
const names = responses
|
||||
.flatMap(extractRecords)
|
||||
const records = responses.flatMap(extractRecords);
|
||||
// 名称→客商编码映射,供 C101-1 风险查询使用(§9-U6 编码体系联调确认)
|
||||
const codeMap = {};
|
||||
records.forEach(item => {
|
||||
const code = item.customerCode;
|
||||
if (!code) return;
|
||||
[item.fullName, item.shortName, item.customerName]
|
||||
.map(name => String(name || '').trim())
|
||||
.filter(Boolean)
|
||||
.forEach(name => {
|
||||
codeMap[name] = code;
|
||||
});
|
||||
});
|
||||
this.contractPartyCodeMap = codeMap;
|
||||
const names = records
|
||||
.map(item => item.fullName || item.shortName || item.customerName || item.customerCode)
|
||||
.map(name => String(name || '').trim())
|
||||
.filter(Boolean);
|
||||
@@ -2218,11 +2435,40 @@ export default {
|
||||
const options = [...new Set(names)].map(name => ({ label: name, value: name }));
|
||||
this.contractPartyAOptions = options;
|
||||
this.contractPartyBOptions = options;
|
||||
// 复制/编辑回填的甲乙方不会触发下拉 change,加载选项后主动查一次 C101-1 风险
|
||||
['partyA', 'partyB'].forEach(prop => {
|
||||
if (this.form[prop]) this.queryPartyRisk(prop);
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
if (requestId === this.contractPartyRequestId) this.contractPartyLoading = false;
|
||||
});
|
||||
},
|
||||
// C101-1 客商风险查询(展示用,强校验在后端提交/推送链路):失败静默不阻塞填写
|
||||
queryPartyRisk(prop) {
|
||||
const name = this.form[prop];
|
||||
const code = name ? this.contractPartyCodeMap[name] : null;
|
||||
if (!code) {
|
||||
this.partyRisk = { ...this.partyRisk, [prop]: null };
|
||||
return;
|
||||
}
|
||||
getRisk(code)
|
||||
.then(res => {
|
||||
const risk = res?.data?.data ?? res?.data ?? {};
|
||||
this.partyRisk = {
|
||||
...this.partyRisk,
|
||||
[prop]: {
|
||||
customerKind: risk.customerKind || '',
|
||||
// risky 由后端统一判定:外部客商高风险(等级=2)或黑名单;内部组织客商默认无风险恒为 false
|
||||
risky: risk.risky === true,
|
||||
riskTip: risk.riskTip || '',
|
||||
},
|
||||
};
|
||||
})
|
||||
.catch(() => {
|
||||
this.partyRisk = { ...this.partyRisk, [prop]: null };
|
||||
});
|
||||
},
|
||||
loadOrganizationOptions() {
|
||||
if (this.organizationLoading) return;
|
||||
this.organizationLoading = true;
|
||||
@@ -2306,11 +2552,14 @@ export default {
|
||||
this.refreshPreviewContractNo();
|
||||
}
|
||||
},
|
||||
handleContractNoInput(value) {
|
||||
this.contractNoManual = String(value || '').trim() !== '';
|
||||
},
|
||||
refreshPreviewContractNo() {
|
||||
if (this.formMode !== 'add') return;
|
||||
const organizationId = this.form.organizationId;
|
||||
if (!organizationId) {
|
||||
this.form.contractNo = '';
|
||||
if (!this.contractNoManual) this.form.contractNo = '';
|
||||
return;
|
||||
}
|
||||
const requestId = (this.contractNoPreviewRequestId =
|
||||
@@ -2318,11 +2567,11 @@ export default {
|
||||
api
|
||||
.nextContractNo(organizationId)
|
||||
.then(res => {
|
||||
if (requestId !== this.contractNoPreviewRequestId) return;
|
||||
if (requestId !== this.contractNoPreviewRequestId || this.contractNoManual) return;
|
||||
this.form.contractNo = res.data?.data || '';
|
||||
})
|
||||
.catch(() => {
|
||||
if (requestId !== this.contractNoPreviewRequestId) return;
|
||||
if (requestId !== this.contractNoPreviewRequestId || this.contractNoManual) return;
|
||||
this.form.contractNo = '';
|
||||
});
|
||||
},
|
||||
@@ -2755,6 +3004,7 @@ export default {
|
||||
},
|
||||
getContractChangeFieldLabel(field) {
|
||||
const labels = {
|
||||
contractNo: '合同编号',
|
||||
contractName: '合同名称',
|
||||
partyB: '乙方',
|
||||
startDate: '开始日期',
|
||||
@@ -3001,13 +3251,30 @@ export default {
|
||||
bizId: row.id,
|
||||
subjectName: row.contractName || '',
|
||||
approvalStatus: row.approvalStatus || '',
|
||||
mkEnabled: this.mkEnabled,
|
||||
})
|
||||
)
|
||||
: request();
|
||||
afterMk.then(() => {
|
||||
this.$message.success(operation.successMessage || `${operation.label}成功`);
|
||||
this.onLoad(this.page, this.query);
|
||||
});
|
||||
// 合同通过会同步等待附件转传 + 合规推送,加遮罩防重复点击
|
||||
const loading =
|
||||
operation.action === 'approve'
|
||||
? this.$loading({
|
||||
lock: true,
|
||||
text: '正在推送合规,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
})
|
||||
: null;
|
||||
afterMk
|
||||
.then(res => {
|
||||
// data=false 表示审批已通过但合规推送失败,msg 带具体原因(照正式结算单审批)
|
||||
if (operation.action === 'approve' && res?.data?.data === false) {
|
||||
this.$message.warning(res.data.msg);
|
||||
} else {
|
||||
this.$message.success(operation.successMessage || `${operation.label}成功`);
|
||||
}
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.finally(() => loading && loading.close());
|
||||
};
|
||||
if (operation.prompt) {
|
||||
this.$prompt(operation.prompt, operation.label, {
|
||||
@@ -3104,6 +3371,29 @@ export default {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.contract-manage-form__risk-alert {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// 风险客商提示条(基本信息备注后):橙黄背景 + 红色文字(设计稿 2026-10)
|
||||
.contract-manage-form__risk-tip {
|
||||
margin: 0 0 18px;
|
||||
padding: 10px 16px;
|
||||
background: #faecd8;
|
||||
border-radius: 4px;
|
||||
|
||||
.contract-manage-form__risk-tip-title {
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.contract-manage-form__risk-tip-content {
|
||||
color: #f56c6c;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.contract-manage-page {
|
||||
&__expiry-search {
|
||||
display: flex;
|
||||
@@ -3338,7 +3628,7 @@ export default {
|
||||
|
||||
:deep(.el-form-item) {
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
//margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<basic-container class="hcs-master-data-page">
|
||||
<div class="hcs-master-data-page__head">
|
||||
<div class="dialog-section-title">合规主数据(HCS)</div>
|
||||
<el-button type="primary" :loading="refreshing" @click="handleRefresh">刷新缓存</el-button>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="合同模板" name="template" />
|
||||
</el-tabs>
|
||||
|
||||
<el-table v-loading="loading" :data="templates" border>
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column prop="fdId" label="模板ID(fdTemplateId)" min-width="320" show-overflow-tooltip />
|
||||
<el-table-column prop="fdName" label="模板名称" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="fdStatus" label="状态" width="100" align="center" />
|
||||
<el-table-column prop="fdContractCharacter" label="合同性质" min-width="140" align="center" />
|
||||
<el-table-column label="创建时间" min-width="170" align="center">
|
||||
<template #default="{ row }">{{ formatTime(row.fdCreateTime) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-alert
|
||||
class="hcs-master-data-page__tip"
|
||||
title="合同类别与合规模板的对应关系由 Nacos 配置 thirdParty.hcs.contract.template-mapping 维护;此列表用于核对映射的模板ID 是否存在。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
/>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getTemplates, refreshMasterData } from '@/api/business/hcs';
|
||||
|
||||
export default {
|
||||
name: 'HcsMasterData',
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'template',
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
templates: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.loadTemplates();
|
||||
},
|
||||
methods: {
|
||||
loadTemplates() {
|
||||
this.loading = true;
|
||||
getTemplates()
|
||||
.then(response => {
|
||||
const data = response?.data?.data ?? response?.data;
|
||||
this.templates = Array.isArray(data) ? data : [];
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('合规模板列表加载失败');
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
handleRefresh() {
|
||||
this.refreshing = true;
|
||||
refreshMasterData()
|
||||
.then(response => {
|
||||
this.$message.success(response?.data?.data || '合规主数据缓存已刷新');
|
||||
this.loadTemplates();
|
||||
})
|
||||
.catch(error => {
|
||||
this.$message.error(error?.response?.data?.msg || '刷新失败');
|
||||
})
|
||||
.finally(() => {
|
||||
this.refreshing = false;
|
||||
});
|
||||
},
|
||||
formatTime(value) {
|
||||
if (!value) return '-';
|
||||
return this.$dayjs(Number(value)).format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hcs-master-data-page__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.dialog-section-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hcs-master-data-page__tip {
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user