Compare commits
136 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9558426da1 | |||
| 3f019c9ac5 | |||
| e7c76c803f | |||
| 90edc4b864 | |||
| 6a21b4ac8d | |||
| d05f8fd636 | |||
| d968c418a8 | |||
| d100e7a271 | |||
| 1c798046fd | |||
| 8d25e186b1 | |||
| c1863d0b6a | |||
| 4a0af452e8 | |||
| 447b2d9d76 | |||
| 603038d5ff | |||
| 673c47771b | |||
| 3168d365d0 | |||
| 5c97d3b5c5 | |||
| 09ce6472b0 | |||
| 5b435bef04 | |||
| 0c4bb7cd82 | |||
| 4c9bbf1eb7 | |||
| 31794bf26a | |||
| 0e57de5c71 | |||
| 5cb7eb16d5 | |||
| ccaf944782 | |||
| aee661772b | |||
| bbe71e340a | |||
| 1ac741f785 | |||
| 912c1169a6 | |||
| 6596722601 | |||
| 6f2383a7e5 | |||
| 8db52ef168 | |||
| 38ca250242 | |||
| 73c12cc1c7 | |||
| f4542cd0b8 | |||
| 4b165471e3 | |||
| d1e779bbcc | |||
| acfbb4c54e | |||
| f440343830 | |||
| c1bc55e133 | |||
| baec42f876 | |||
| 4d3b6cdaed | |||
| 96b675b3be | |||
| 6067d24e3b | |||
| e051327f2d | |||
| f7759fd9e7 | |||
| 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 | |||
| 1181aea3b9 | |||
| 68f4b2c35e | |||
| 6296c8f6f4 | |||
| 6955784e93 | |||
| bc7755a160 | |||
| 18a02a1969 | |||
| dcaa3127a0 | |||
| 959c1821c1 | |||
| a4c2987c5f | |||
| d8f4f22b9c | |||
| 2377a2e8d4 | |||
| 9e5f53702f | |||
| 8640238cbe | |||
| b077ba5be4 | |||
| f455003014 | |||
| ea797356ce | |||
| eaece0209d | |||
| cd92c36dab | |||
| ef1a43f947 | |||
| a1da644c49 | |||
| 2500540933 | |||
| 570fbeb537 | |||
| d1f804056e | |||
| 3320155416 | |||
| 8ad53894b9 | |||
| 360147e46a | |||
| b00e12c9bb | |||
| 7543a8c06b | |||
| 1699ae873f | |||
| 462f9bbea2 | |||
| d62d3da60e | |||
| 50b8abf85c | |||
| 509f5e163c | |||
| 0e853ea751 | |||
| d67240bc3b | |||
| d0770c8e83 | |||
| cc0b889fef | |||
| 3d27e981e1 | |||
| 2994539de4 | |||
| 785ad08b7e | |||
| 8de51e568d | |||
| d22fe4777d | |||
| 7751a735c5 | |||
| 069d0e92ff | |||
| ce54d21344 | |||
| 715c0dc00c | |||
| b8d74de4a4 | |||
| 71a6f7c4f8 | |||
| 7c6ac2e5db |
@@ -0,0 +1,18 @@
|
||||
# 2026-10-13
|
||||
|
||||
## 车辆调度弹窗:使用部门级联选择器宽度对齐
|
||||
|
||||
- 现象:`/transportCapacity/vehicle-dispatch` 新增/编辑弹窗中,使用部门 `el-cascader` 明显窄于同行车牌号 `el-input`。
|
||||
- 根因:`el-cascader` 是 `display: inline-block` 宽度按内容收缩(不像 `el-autocomplete` / `el-input` 有 `--el-input-width: 100%` 兜底);而页面里写的 `.vehicle-dispatch-page :deep(.vehicle-dispatch-form-control){width:100%}` 命中不到 —— avue-crud 弹窗默认 `append-to-body: true`(`append-to-body": validData(crud.tableOption.dialogAppendToBody, true)`),DOM Teleport 到 body,已不在 `.vehicle-dispatch-page` 之下。
|
||||
- 修法(走项目既有约定):
|
||||
1. `src/option/transportCapacity/vehicle-dispatch.js` 增加 `dialogCustomClass: 'vehicle-dispatch-dialog'`;
|
||||
2. `src/styles/element-ui.scss` 增加全局规则 `.vehicle-dispatch-dialog .el-form-item__content > .el-cascader { width:100%; max-width:100% }`。
|
||||
- 自检:dev(2889) `curl /src/styles/element-ui.scss` 与 `/src/option/transportCapacity/vehicle-dispatch.js` 均 200,编译产物含新规则。
|
||||
- 遗留观察:同文件里 `.vehicle-dispatch-page :deep(.avue--detail ...)` 那一批查看态禁用样式,同样因 Teleport 属死规则(未改,待确认是否需要迁到 `.vehicle-dispatch-dialog` 作用域)。
|
||||
|
||||
## 追加修正(同日 20:xx):第一次改完用户反馈「还是没对齐」
|
||||
|
||||
- 原因一:选择器写成了 `.el-form-item__content > .el-cascader`(直接子级)。**avue 表单项在 `el-form-item__content` 与控件之间还有两层包裹 div**(`packages/element-plus/form/index3.js`:content > div > [div|elTooltip] > 控件),必须用后代选择器。已改成 `.vehicle-dispatch-dialog .el-form-item__content .el-cascader`。
|
||||
- 原因二(更隐蔽):option 用 `labelWidth: 'auto'`,**EP 的 auto 是逐项计算**(实测:3 字 54/64px、2 字 40px、4 字无星号 68px、4 字带星号 78px),label 越宽控件可用宽度越小 → 车牌号 input 486px、使用部门 cascader 472px,仍差 14px。已加 `.vehicle-dispatch-dialog .el-form-item__label { flex: 0 0 calc(4em + 24px) !important; width: calc(4em + 24px) !important }`(本弹窗左右两列最长 label 均为 4 汉字)。
|
||||
- 验证方式(可复用):临时在项目根建 `cascader-width-check.html`,Vite 直接服务 → `import './src/option/...'` 用真实 option + `import './src/styles/element-ui.scss'`,挂 avue-crud 并 `mounted(){ this.$refs.crud.rowAdd() }` 弹出真实弹窗;用 agent-browser(`/Users/gxwebsoft/.workbuddy/binaries/node/workspace/node_modules/.bin/agent-browser`)`open` + `eval` 读 DOM 链路与 `getBoundingClientRect`。修完已删除该临时文件。
|
||||
- 结果:全部 label 80px,车牌号 input 与使用部门 cascader 均为 left=732 / width=470 / right=1202,完全一致。
|
||||
@@ -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。
|
||||
@@ -12,16 +12,84 @@
|
||||
|
||||
**坑二:宿主 view 直绑全局 `$route.query.*` 给子组件 prop。** 缓存实例被灌入当前页 id → 拿错 id 查后端(批量弹「运单管理不存在」),live computed 还会误 push 标签。修法:`routePathLocked` 锁路径 + watcher / push 前判 `isOwnedRouteActive` + 宿主绑定加路径守卫。⚠️ `routePathLocked` **必须放 `data()`**(Options API 顺序 data → computed → watch(immediate) → created;放 created 会读到空值、挡掉首次加载)。已修 waybill-manage-page / transport-plan-page + 三个宿主 view。
|
||||
|
||||
**坑四:avue-crud 弹窗 `append-to-body` 默认 true(`dialog-form3.js`:`validData(tableOption.dialogAppendToBody, true)`),DOM Teleport 到 body。** 于是页面里以页面容器为祖先的 scoped 选择器(`.xxx-page :deep(.yyy)`)**命中不到弹窗内容**,尤其 `el-cascader`(inline-block、宽度按内容收缩,不像 `el-input`/`el-autocomplete` 有 `--el-input-width:100%` 兜底)会明显比同行 input 窄。修法:option 里配 `dialogCustomClass: 'xxx-dialog'`,规则写到 `src/styles/element-ui.scss` 的 `.xxx-dialog ...`(项目既有约定,10+ 处同款);或把基准类放在 **Teleport 内部元素**上(如 ship.vue 的 `.ship-form` 挂在 el-form 上,自身带 scope 属性,照样生效)。
|
||||
|
||||
**坑五:avue 表单项控件不是 `.el-form-item__content` 的直接子级。** `form/index3.js` 渲染为 `content > div > [div|elTooltip] > 控件`,所以 `.el-form-item__content > .el-cascader` 这类**直接子级选择器对 avue 表单全部失效**(手写 `<el-form-item>` 才生效),一律用后代选择器。
|
||||
|
||||
**坑六:`labelWidth: 'auto'` 在 Element Plus 里逐项计算,不是全表单共享。** 实测同一弹窗:3 字 54/64px、2 字 40px、4 字 68px、4 字带必填星号 78px;label 越宽 → 控件可用宽度越小,同级控件宽度就不一致(车牌号 input 486 vs 使用部门 cascader 472)。要让同一行控件等宽必须统一 label 宽度:`.xxx-dialog .el-form-item__label { flex: 0 0 calc(Nem + 24px) !important; width: calc(Nem + 24px) !important }`(N=本列最长汉字数,与 ship.vue、`.dialog-form--carded` 同款)。
|
||||
|
||||
**验证 avue 弹窗 DOM/宽度的可复用办法:** 项目根建临时 html(Vite 直接服务)→ 引真实 option 与 `src/styles/element-ui.scss`,挂 avue-crud 并 `mounted(){ this.$refs.crud.rowAdd() }` 弹出真弹窗;用 `agent-browser`(`/Users/gxwebsoft/.workbuddy/binaries/node/workspace/node_modules/.bin/agent-browser`)`open` + `eval` 读 DOM 链路与 `getBoundingClientRect`,用完删文件。
|
||||
|
||||
**坑三:路由表重复定义 → 内容区整页空白(2026-09-18)。** `/business/contract-manage/detail` 在 `router/views/index.js` 定义两次、子路由同名 `合同详情`。重名 `addRoute` 会 `removeRoute(name)` 移除旧子记录,但**不从父记录 children 里摘除**,第一条 Layout 壳记录留在 matcher 且靠前 → `resolve()` 命中它 → `$route.matched.length === 1` → 二级 router-view 无匹配 → 侧栏/标签正常但内容空白。修法:删重复定义。排查:扫「顶层 path + 首个子路由 name」重复项。
|
||||
|
||||
## 列表页右上角按钮组(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"`),改表格列时先看这两条。
|
||||
|
||||
Generated
+347
-58
@@ -13,31 +13,31 @@ importers:
|
||||
dependencies:
|
||||
'@element-plus/icons-vue':
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.2(vue@3.5.34)
|
||||
version: 2.3.2(vue@3.5.34(typescript@7.0.2))
|
||||
'@open-file-viewer/core':
|
||||
specifier: ^0.1.27
|
||||
version: 0.1.27(pdfjs-dist@6.1.200)
|
||||
'@open-file-viewer/vue':
|
||||
specifier: ^0.1.27
|
||||
version: 0.1.27(@open-file-viewer/core@0.1.27(pdfjs-dist@6.1.200))(vue@3.5.34)
|
||||
version: 0.1.27(@open-file-viewer/core@0.1.27(pdfjs-dist@6.1.200))(vue@3.5.34(typescript@7.0.2))
|
||||
'@saber/nf-design-base-elp':
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
'@saber/nf-form-design-elp':
|
||||
specifier: ^1.6.1
|
||||
version: 1.6.1(vue@3.5.34)
|
||||
version: 1.6.1(vue@3.5.34(typescript@7.0.2))
|
||||
'@saber/nf-form-elp':
|
||||
specifier: ^1.6.3
|
||||
version: 1.7.0
|
||||
'@smallwei/avue':
|
||||
specifier: ~3.9.2
|
||||
version: 3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)
|
||||
version: 3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34(typescript@7.0.2)))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34(typescript@7.0.2)))(vue@3.5.34(typescript@7.0.2))
|
||||
animate.css:
|
||||
specifier: ^4.1.1
|
||||
version: 4.1.1
|
||||
avue-plugin-ueditor:
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4(axios@1.16.0(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34)
|
||||
version: 1.0.4(axios@1.16.0(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34(typescript@7.0.2))
|
||||
axios:
|
||||
specifier: ^1.13.5
|
||||
version: 1.16.0(debug@4.4.3(supports-color@7.2.0))
|
||||
@@ -58,7 +58,7 @@ importers:
|
||||
version: 6.1.0
|
||||
element-plus:
|
||||
specifier: ^2.14.2
|
||||
version: 2.14.2(vue@3.5.34)
|
||||
version: 2.14.2(vue@3.5.34(typescript@7.0.2))
|
||||
exceljs:
|
||||
specifier: ^4.4.0
|
||||
version: 4.4.0
|
||||
@@ -85,29 +85,29 @@ importers:
|
||||
version: 0.4.0
|
||||
vue:
|
||||
specifier: ^3.5.13
|
||||
version: 3.5.34
|
||||
version: 3.5.34(typescript@7.0.2)
|
||||
vue-i18n:
|
||||
specifier: ^11.4.2
|
||||
version: 11.4.2(vue@3.5.34)
|
||||
version: 11.4.2(vue@3.5.34(typescript@7.0.2))
|
||||
vue-pdf-embed:
|
||||
specifier: ^2.1.6
|
||||
version: 2.1.6(vue@3.5.34)
|
||||
version: 2.1.6(vue@3.5.34(typescript@7.0.2))
|
||||
vue-router:
|
||||
specifier: ^4.3.2
|
||||
version: 4.6.4(vue@3.5.34)
|
||||
version: 4.6.4(vue@3.5.34(typescript@7.0.2))
|
||||
vue3-clipboard:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0(vue@3.5.34)
|
||||
version: 1.0.0(vue@3.5.34(typescript@7.0.2))
|
||||
vuex:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(vue@3.5.34)
|
||||
version: 4.1.0(vue@3.5.34(typescript@7.0.2))
|
||||
xlsx:
|
||||
specifier: 0.18.5
|
||||
version: 0.18.5
|
||||
devDependencies:
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^5.2.4
|
||||
version: 5.2.4(vite@5.4.21(sass@1.99.0)(terser@5.46.2))(vue@3.5.34)
|
||||
version: 5.2.4(vite@5.4.21(sass@1.99.0)(terser@5.46.2))(vue@3.5.34(typescript@7.0.2))
|
||||
'@vue/compiler-sfc':
|
||||
specifier: ^3.4.27
|
||||
version: 3.5.34
|
||||
@@ -120,9 +120,12 @@ importers:
|
||||
terser:
|
||||
specifier: ^5.31.1
|
||||
version: 5.46.2
|
||||
typescript:
|
||||
specifier: ^7.0.2
|
||||
version: 7.0.2
|
||||
unplugin-auto-import:
|
||||
specifier: ^0.11.2
|
||||
version: 0.11.5(@vueuse/core@14.3.0(vue@3.5.34))(rollup@4.60.3)
|
||||
version: 0.11.5(@vueuse/core@14.3.0(vue@3.5.34(typescript@7.0.2)))(rollup@4.60.3)
|
||||
vite:
|
||||
specifier: ^5.4.19
|
||||
version: 5.4.21(sass@1.99.0)(terser@5.46.2)
|
||||
@@ -132,6 +135,9 @@ importers:
|
||||
vite-plugin-vue-setup-extend:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(vite@5.4.21(sass@1.99.0)(terser@5.46.2))
|
||||
vue-tsc:
|
||||
specifier: ^3.3.11
|
||||
version: 3.3.13(typescript@7.0.2)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -765,6 +771,126 @@ packages:
|
||||
'@types/web-bluetooth@0.0.21':
|
||||
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
||||
|
||||
'@typescript/typescript-aix-ppc64@7.0.2':
|
||||
resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@typescript/typescript-darwin-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@typescript/typescript-darwin-x64@7.0.2':
|
||||
resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@typescript/typescript-freebsd-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@typescript/typescript-freebsd-x64@7.0.2':
|
||||
resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@typescript/typescript-linux-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-arm@7.0.2':
|
||||
resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-loong64@7.0.2':
|
||||
resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-mips64el@7.0.2':
|
||||
resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-ppc64@7.0.2':
|
||||
resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-riscv64@7.0.2':
|
||||
resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-s390x@7.0.2':
|
||||
resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-x64@7.0.2':
|
||||
resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-netbsd-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@typescript/typescript-netbsd-x64@7.0.2':
|
||||
resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@typescript/typescript-openbsd-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@typescript/typescript-openbsd-x64@7.0.2':
|
||||
resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@typescript/typescript-sunos-x64@7.0.2':
|
||||
resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@typescript/typescript-win32-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@typescript/typescript-win32-x64@7.0.2':
|
||||
resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@uppy/companion-client@2.2.2':
|
||||
resolution: {integrity: sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==}
|
||||
|
||||
@@ -789,6 +915,20 @@ packages:
|
||||
vite: ^5.0.0 || ^6.0.0
|
||||
vue: ^3.2.25
|
||||
|
||||
'@volar/language-core@2.4.28':
|
||||
resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
|
||||
|
||||
'@volar/source-map@2.4.28':
|
||||
resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==}
|
||||
|
||||
'@volar/typescript@2.4.28':
|
||||
resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@vue/compiler-core@3.5.34':
|
||||
resolution: {integrity: sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==}
|
||||
|
||||
@@ -804,6 +944,9 @@ packages:
|
||||
'@vue/devtools-api@6.6.4':
|
||||
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
|
||||
|
||||
'@vue/language-core@3.3.13':
|
||||
resolution: {integrity: sha512-axA0Xemn5LuUKuKWob0/kVq7xQ2mJJM6VqA9Exvt35nM9yRC31goWVve/G/Zb2ZCLsXC8FHYjjfXlnshknSCew==}
|
||||
|
||||
'@vue/reactivity@3.5.34':
|
||||
resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==}
|
||||
|
||||
@@ -1017,6 +1160,9 @@ packages:
|
||||
ag-psd@30.2.0:
|
||||
resolution: {integrity: sha512-tSAWfNzLl5brFqKEer7egxASZ1a0LwHnReM/D5VVpvrgdwdsa8OVfCQpysK7tGaOuE2tPEqp3mAuH7aIvPhcig==}
|
||||
|
||||
alien-signals@3.2.1:
|
||||
resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
|
||||
|
||||
animate.css@4.1.1:
|
||||
resolution: {integrity: sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==}
|
||||
|
||||
@@ -1728,6 +1874,9 @@ packages:
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
muggle-string@0.4.1:
|
||||
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
||||
|
||||
namespace-emitter@2.0.1:
|
||||
resolution: {integrity: sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==}
|
||||
|
||||
@@ -1778,6 +1927,9 @@ packages:
|
||||
parsedbf@2.0.0:
|
||||
resolution: {integrity: sha512-WNjKn/cwgGBkXqQLif+2VMEahcRHkBRU0/RfBWZ7Vj7snRNNW63yW1mVuuHRDyXTRxuGCzAHHBcr/Fn+U/bXjQ==}
|
||||
|
||||
path-browserify@1.0.1:
|
||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||
|
||||
path-intersection@2.2.1:
|
||||
resolution: {integrity: sha512-9u8xvMcSfuOiStv9bPdnRJQhGQXLKurew94n4GPQCdH1nj9QKC9ObbNoIpiRq8skiOBxKkt277PgOoFgAt3/rA==}
|
||||
|
||||
@@ -2042,6 +2194,11 @@ packages:
|
||||
typedarray@0.0.6:
|
||||
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
||||
|
||||
typescript@7.0.2:
|
||||
resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
hasBin: true
|
||||
|
||||
ufo@1.6.4:
|
||||
resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
|
||||
|
||||
@@ -2122,6 +2279,9 @@ packages:
|
||||
terser:
|
||||
optional: true
|
||||
|
||||
vscode-uri@3.2.0:
|
||||
resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==}
|
||||
|
||||
vue-component-type-helpers@3.3.6:
|
||||
resolution: {integrity: sha512-FkljacAwJ9BUoSUdpFe3VDy0sGigNlTH9+2zcXUWmZOjN8swiCkl3t48wOJun0OsUd2cEIda1l04tsxMiKIIrQ==}
|
||||
|
||||
@@ -2141,6 +2301,12 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
vue-tsc@3.3.13:
|
||||
resolution: {integrity: sha512-jE4/bXdOMSHVxEGgQCqNllJy+9N3Gx6tfSAsg0yhFK/MAN6eXqISihupnDEKSCt12VVnjN2gI3AqLYcqoh5x/A==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: '>=5.0.0'
|
||||
|
||||
vue3-clipboard@1.0.0:
|
||||
resolution: {integrity: sha512-GUqKh1oO79xDpq0z+cCv/NDVTpcJGNDzeNgT3PmTdTp/WJh3gcTrDqIYKycKhzMFOtIFJ7hO/+usgyWtT+fNhA==}
|
||||
peerDependencies:
|
||||
@@ -2242,9 +2408,9 @@ snapshots:
|
||||
|
||||
'@ctrl/tinycolor@4.2.0': {}
|
||||
|
||||
'@element-plus/icons-vue@2.3.2(vue@3.5.34)':
|
||||
'@element-plus/icons-vue@2.3.2(vue@3.5.34(typescript@7.0.2))':
|
||||
dependencies:
|
||||
vue: 3.5.34
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
|
||||
'@esbuild/aix-ppc64@0.21.5':
|
||||
optional: true
|
||||
@@ -2485,10 +2651,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- geotiff
|
||||
|
||||
'@open-file-viewer/vue@0.1.27(@open-file-viewer/core@0.1.27(pdfjs-dist@6.1.200))(vue@3.5.34)':
|
||||
'@open-file-viewer/vue@0.1.27(@open-file-viewer/core@0.1.27(pdfjs-dist@6.1.200))(vue@3.5.34(typescript@7.0.2))':
|
||||
dependencies:
|
||||
'@open-file-viewer/core': 0.1.27(pdfjs-dist@6.1.200)
|
||||
vue: 3.5.34
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.6':
|
||||
optional: true
|
||||
@@ -2640,9 +2806,9 @@ snapshots:
|
||||
codemirror: 5.65.21
|
||||
randomcolor: 0.6.2
|
||||
|
||||
'@saber/nf-form-design-elp@1.6.1(vue@3.5.34)':
|
||||
'@saber/nf-form-design-elp@1.6.1(vue@3.5.34(typescript@7.0.2))':
|
||||
dependencies:
|
||||
vuedraggable: 4.1.0(vue@3.5.34)
|
||||
vuedraggable: 4.1.0(vue@3.5.34(typescript@7.0.2))
|
||||
transitivePeerDependencies:
|
||||
- vue
|
||||
|
||||
@@ -2654,16 +2820,16 @@ snapshots:
|
||||
countup.js: 2.10.0
|
||||
signature_pad: 5.1.3
|
||||
|
||||
'@smallwei/avue@3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)':
|
||||
'@smallwei/avue@3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34(typescript@7.0.2)))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34(typescript@7.0.2)))(vue@3.5.34(typescript@7.0.2))':
|
||||
dependencies:
|
||||
'@element-plus/icons-vue': 2.3.2(vue@3.5.34)
|
||||
'@element-plus/icons-vue': 2.3.2(vue@3.5.34(typescript@7.0.2))
|
||||
axios: 0.21.4(debug@4.4.3(supports-color@7.2.0))
|
||||
countup.js: 1.9.3
|
||||
dayjs: 1.11.20
|
||||
element-plus: 2.14.2(vue@3.5.34)
|
||||
element-plus: 2.14.2(vue@3.5.34(typescript@7.0.2))
|
||||
lodash: 4.18.1
|
||||
lodash-es: 4.18.1
|
||||
vue: 3.5.34
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
@@ -2690,6 +2856,66 @@ snapshots:
|
||||
|
||||
'@types/web-bluetooth@0.0.21': {}
|
||||
|
||||
'@typescript/typescript-aix-ppc64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-darwin-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-darwin-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-freebsd-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-freebsd-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-arm@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-loong64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-mips64el@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-ppc64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-riscv64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-s390x@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-netbsd-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-netbsd-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-openbsd-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-openbsd-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-sunos-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-win32-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-win32-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@uppy/companion-client@2.2.2':
|
||||
dependencies:
|
||||
'@uppy/utils': 4.1.3
|
||||
@@ -2719,10 +2945,24 @@ snapshots:
|
||||
'@uppy/utils': 4.1.3
|
||||
nanoid: 3.3.12
|
||||
|
||||
'@vitejs/plugin-vue@5.2.4(vite@5.4.21(sass@1.99.0)(terser@5.46.2))(vue@3.5.34)':
|
||||
'@vitejs/plugin-vue@5.2.4(vite@5.4.21(sass@1.99.0)(terser@5.46.2))(vue@3.5.34(typescript@7.0.2))':
|
||||
dependencies:
|
||||
vite: 5.4.21(sass@1.99.0)(terser@5.46.2)
|
||||
vue: 3.5.34
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
|
||||
'@volar/language-core@2.4.28':
|
||||
dependencies:
|
||||
'@volar/source-map': 2.4.28
|
||||
|
||||
'@volar/source-map@2.4.28': {}
|
||||
|
||||
'@volar/typescript@2.4.28(typescript@7.0.2)':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.4.28
|
||||
path-browserify: 1.0.1
|
||||
vscode-uri: 3.2.0
|
||||
optionalDependencies:
|
||||
typescript: 7.0.2
|
||||
|
||||
'@vue/compiler-core@3.5.34':
|
||||
dependencies:
|
||||
@@ -2756,6 +2996,16 @@ snapshots:
|
||||
|
||||
'@vue/devtools-api@6.6.4': {}
|
||||
|
||||
'@vue/language-core@3.3.13':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.4.28
|
||||
'@vue/compiler-dom': 3.5.34
|
||||
'@vue/shared': 3.5.34
|
||||
alien-signals: 3.2.1
|
||||
muggle-string: 0.4.1
|
||||
path-browserify: 1.0.1
|
||||
picomatch: 4.0.4
|
||||
|
||||
'@vue/reactivity@3.5.34':
|
||||
dependencies:
|
||||
'@vue/shared': 3.5.34
|
||||
@@ -2772,26 +3022,26 @@ snapshots:
|
||||
'@vue/shared': 3.5.34
|
||||
csstype: 3.2.3
|
||||
|
||||
'@vue/server-renderer@3.5.34(vue@3.5.34)':
|
||||
'@vue/server-renderer@3.5.34(vue@3.5.34(typescript@7.0.2))':
|
||||
dependencies:
|
||||
'@vue/compiler-ssr': 3.5.34
|
||||
'@vue/shared': 3.5.34
|
||||
vue: 3.5.34
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
|
||||
'@vue/shared@3.5.34': {}
|
||||
|
||||
'@vueuse/core@14.3.0(vue@3.5.34)':
|
||||
'@vueuse/core@14.3.0(vue@3.5.34(typescript@7.0.2))':
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.21
|
||||
'@vueuse/metadata': 14.3.0
|
||||
'@vueuse/shared': 14.3.0(vue@3.5.34)
|
||||
vue: 3.5.34
|
||||
'@vueuse/shared': 14.3.0(vue@3.5.34(typescript@7.0.2))
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
|
||||
'@vueuse/metadata@14.3.0': {}
|
||||
|
||||
'@vueuse/shared@14.3.0(vue@3.5.34)':
|
||||
'@vueuse/shared@14.3.0(vue@3.5.34(typescript@7.0.2))':
|
||||
dependencies:
|
||||
vue: 3.5.34
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
|
||||
'@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
dependencies:
|
||||
@@ -2935,10 +3185,10 @@ snapshots:
|
||||
slate-history: 0.66.0(slate@0.72.8)
|
||||
snabbdom: 3.6.3
|
||||
|
||||
'@wangeditor/editor-for-vue@5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34)':
|
||||
'@wangeditor/editor-for-vue@5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34(typescript@7.0.2))':
|
||||
dependencies:
|
||||
'@wangeditor/editor': 5.1.23
|
||||
vue: 3.5.34
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
|
||||
'@wangeditor/editor@5.1.23':
|
||||
dependencies:
|
||||
@@ -3013,6 +3263,8 @@ snapshots:
|
||||
base64-js: 1.5.1
|
||||
pako: 2.1.0
|
||||
|
||||
alien-signals@3.2.1: {}
|
||||
|
||||
animate.css@4.1.1: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
@@ -3065,12 +3317,12 @@ snapshots:
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
avue-plugin-ueditor@1.0.4(axios@1.16.0(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34):
|
||||
avue-plugin-ueditor@1.0.4(axios@1.16.0(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34(typescript@7.0.2)):
|
||||
dependencies:
|
||||
'@wangeditor/editor': 5.1.23
|
||||
'@wangeditor/editor-for-vue': 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34)
|
||||
'@wangeditor/editor-for-vue': 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34(typescript@7.0.2))
|
||||
axios: 1.16.0(debug@4.4.3(supports-color@7.2.0))
|
||||
vue: 3.5.34
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
|
||||
axios@0.21.4(debug@4.4.3(supports-color@7.2.0)):
|
||||
dependencies:
|
||||
@@ -3323,15 +3575,15 @@ snapshots:
|
||||
tslib: 2.3.0
|
||||
zrender: 6.1.0
|
||||
|
||||
element-plus@2.14.2(vue@3.5.34):
|
||||
element-plus@2.14.2(vue@3.5.34(typescript@7.0.2)):
|
||||
dependencies:
|
||||
'@ctrl/tinycolor': 4.2.0
|
||||
'@element-plus/icons-vue': 2.3.2(vue@3.5.34)
|
||||
'@element-plus/icons-vue': 2.3.2(vue@3.5.34(typescript@7.0.2))
|
||||
'@floating-ui/dom': 1.7.6
|
||||
'@popperjs/core': '@sxzz/popperjs-es@2.11.8'
|
||||
'@types/lodash': 4.17.24
|
||||
'@types/lodash-es': 4.17.12
|
||||
'@vueuse/core': 14.3.0(vue@3.5.34)
|
||||
'@vueuse/core': 14.3.0(vue@3.5.34(typescript@7.0.2))
|
||||
async-validator: 4.2.5
|
||||
dayjs: 1.11.20
|
||||
lodash: 4.18.1
|
||||
@@ -3339,7 +3591,7 @@ snapshots:
|
||||
lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1)
|
||||
memoize-one: 6.0.0
|
||||
normalize-wheel-es: 1.2.0
|
||||
vue: 3.5.34
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
vue-component-type-helpers: 3.3.6
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
@@ -3794,6 +4046,8 @@ snapshots:
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
muggle-string@0.4.1: {}
|
||||
|
||||
namespace-emitter@2.0.1: {}
|
||||
|
||||
nanoid@3.3.12: {}
|
||||
@@ -3827,6 +4081,8 @@ snapshots:
|
||||
|
||||
parsedbf@2.0.0: {}
|
||||
|
||||
path-browserify@1.0.1: {}
|
||||
|
||||
path-intersection@2.2.1: {}
|
||||
|
||||
path-is-absolute@1.0.1: {}
|
||||
@@ -4096,6 +4352,29 @@ snapshots:
|
||||
|
||||
typedarray@0.0.6: {}
|
||||
|
||||
typescript@7.0.2:
|
||||
optionalDependencies:
|
||||
'@typescript/typescript-aix-ppc64': 7.0.2
|
||||
'@typescript/typescript-darwin-arm64': 7.0.2
|
||||
'@typescript/typescript-darwin-x64': 7.0.2
|
||||
'@typescript/typescript-freebsd-arm64': 7.0.2
|
||||
'@typescript/typescript-freebsd-x64': 7.0.2
|
||||
'@typescript/typescript-linux-arm': 7.0.2
|
||||
'@typescript/typescript-linux-arm64': 7.0.2
|
||||
'@typescript/typescript-linux-loong64': 7.0.2
|
||||
'@typescript/typescript-linux-mips64el': 7.0.2
|
||||
'@typescript/typescript-linux-ppc64': 7.0.2
|
||||
'@typescript/typescript-linux-riscv64': 7.0.2
|
||||
'@typescript/typescript-linux-s390x': 7.0.2
|
||||
'@typescript/typescript-linux-x64': 7.0.2
|
||||
'@typescript/typescript-netbsd-arm64': 7.0.2
|
||||
'@typescript/typescript-netbsd-x64': 7.0.2
|
||||
'@typescript/typescript-openbsd-arm64': 7.0.2
|
||||
'@typescript/typescript-openbsd-x64': 7.0.2
|
||||
'@typescript/typescript-sunos-x64': 7.0.2
|
||||
'@typescript/typescript-win32-arm64': 7.0.2
|
||||
'@typescript/typescript-win32-x64': 7.0.2
|
||||
|
||||
ufo@1.6.4: {}
|
||||
|
||||
underscore@1.13.8: {}
|
||||
@@ -4118,7 +4397,7 @@ snapshots:
|
||||
|
||||
universalify@2.0.1: {}
|
||||
|
||||
unplugin-auto-import@0.11.5(@vueuse/core@14.3.0(vue@3.5.34))(rollup@4.60.3):
|
||||
unplugin-auto-import@0.11.5(@vueuse/core@14.3.0(vue@3.5.34(typescript@7.0.2)))(rollup@4.60.3):
|
||||
dependencies:
|
||||
'@antfu/utils': 0.7.10
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.3)
|
||||
@@ -4127,7 +4406,7 @@ snapshots:
|
||||
unimport: 1.3.0(rollup@4.60.3)
|
||||
unplugin: 1.16.1
|
||||
optionalDependencies:
|
||||
'@vueuse/core': 14.3.0(vue@3.5.34)
|
||||
'@vueuse/core': 14.3.0(vue@3.5.34(typescript@7.0.2))
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
|
||||
@@ -4182,48 +4461,58 @@ snapshots:
|
||||
sass: 1.99.0
|
||||
terser: 5.46.2
|
||||
|
||||
vscode-uri@3.2.0: {}
|
||||
|
||||
vue-component-type-helpers@3.3.6: {}
|
||||
|
||||
vue-i18n@11.4.2(vue@3.5.34):
|
||||
vue-i18n@11.4.2(vue@3.5.34(typescript@7.0.2)):
|
||||
dependencies:
|
||||
'@intlify/core-base': 11.4.2
|
||||
'@intlify/devtools-types': 11.4.2
|
||||
'@intlify/shared': 11.4.2
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.34
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
|
||||
vue-pdf-embed@2.1.6(vue@3.5.34):
|
||||
vue-pdf-embed@2.1.6(vue@3.5.34(typescript@7.0.2)):
|
||||
dependencies:
|
||||
pdfjs-dist: 6.2.108
|
||||
vue: 3.5.34
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
|
||||
vue-router@4.6.4(vue@3.5.34):
|
||||
vue-router@4.6.4(vue@3.5.34(typescript@7.0.2)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.34
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
|
||||
vue3-clipboard@1.0.0(vue@3.5.34):
|
||||
vue-tsc@3.3.13(typescript@7.0.2):
|
||||
dependencies:
|
||||
'@volar/typescript': 2.4.28(typescript@7.0.2)
|
||||
'@vue/language-core': 3.3.13
|
||||
typescript: 7.0.2
|
||||
|
||||
vue3-clipboard@1.0.0(vue@3.5.34(typescript@7.0.2)):
|
||||
dependencies:
|
||||
clipboard: 2.0.11
|
||||
vue: 3.5.34
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
|
||||
vue@3.5.34:
|
||||
vue@3.5.34(typescript@7.0.2):
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.34
|
||||
'@vue/compiler-sfc': 3.5.34
|
||||
'@vue/runtime-dom': 3.5.34
|
||||
'@vue/server-renderer': 3.5.34(vue@3.5.34)
|
||||
'@vue/server-renderer': 3.5.34(vue@3.5.34(typescript@7.0.2))
|
||||
'@vue/shared': 3.5.34
|
||||
optionalDependencies:
|
||||
typescript: 7.0.2
|
||||
|
||||
vuedraggable@4.1.0(vue@3.5.34):
|
||||
vuedraggable@4.1.0(vue@3.5.34(typescript@7.0.2)):
|
||||
dependencies:
|
||||
sortablejs: 1.14.0
|
||||
vue: 3.5.34
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
|
||||
vuex@4.1.0(vue@3.5.34):
|
||||
vuex@4.1.0(vue@3.5.34(typescript@7.0.2)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.34
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
|
||||
webpack-virtual-modules@0.6.2: {}
|
||||
|
||||
|
||||
+3
-3
@@ -2,9 +2,9 @@ packages:
|
||||
- '.'
|
||||
|
||||
allowBuilds:
|
||||
'@parcel/watcher': set this to true or false
|
||||
es5-ext: set this to true or false
|
||||
esbuild: set this to true or false
|
||||
'@parcel/watcher': true
|
||||
es5-ext: true
|
||||
esbuild: true
|
||||
|
||||
patchedDependencies:
|
||||
'@smallwei/avue': patches/@smallwei__avue.patch
|
||||
|
||||
@@ -36,6 +36,8 @@ export const approve = id =>
|
||||
url: `${baseUrl}/approve`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
// 审批通过后同步推送合规(附件转传可达 20s+),覆盖全局 10s 超时
|
||||
timeout: 120000,
|
||||
});
|
||||
|
||||
export const reject = id =>
|
||||
@@ -90,3 +92,11 @@ export const expireStats = params =>
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
|
||||
/** 按合同负责部门预览下一合同编号 */
|
||||
export const nextContractNo = organizationId =>
|
||||
request({
|
||||
url: `${baseUrl}/next-contract-no`,
|
||||
method: 'get',
|
||||
params: { organizationId },
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -43,6 +43,8 @@ export const approve = id =>
|
||||
url: `${baseUrl}/approve`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
// 审批通过后同步推送合规(远程多次调用),覆盖全局 10s 超时
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
export const reject = id =>
|
||||
|
||||
@@ -52,6 +52,17 @@ export const createFileTask = data =>
|
||||
request({ url: `${fileTaskBaseUrl}/createFileTask`, method: 'post', data });
|
||||
export const getPartUploadUrl = (id, partNumber) =>
|
||||
request({ url: `${fileTaskBaseUrl}/getUploadUrl`, method: 'get', params: { id, partNumber } });
|
||||
export const uploadPartByProxy = (id, partNumber, partBlob) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', partBlob, `part-${partNumber}.bin`);
|
||||
return request({
|
||||
url: `${fileTaskBaseUrl}/uploadPart`,
|
||||
method: 'post',
|
||||
params: { id, partNumber },
|
||||
data: formData,
|
||||
timeout: 600000,
|
||||
});
|
||||
};
|
||||
export const updateFileTask = data =>
|
||||
request({ url: `${fileTaskBaseUrl}/updateFileTask`, method: 'post', data });
|
||||
export const pauseFileTask = ids =>
|
||||
|
||||
@@ -34,6 +34,14 @@ export const getPunchRecords = waybillId =>
|
||||
params: { waybillId },
|
||||
});
|
||||
|
||||
/** 编辑打卡记录(含打卡状态;punched=false 表示取消打卡) */
|
||||
export const savePunchRecord = data =>
|
||||
request({
|
||||
url: `${baseUrl}/punch-record/save`,
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
|
||||
/** 运单车辆实时定位 */
|
||||
export const locateVehicle = id =>
|
||||
request({
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
@@ -10,6 +10,13 @@ export const getReferenceAmount = (paymentType, referenceId, excludeId) =>
|
||||
method: 'get',
|
||||
params: { paymentType, referenceId, excludeId: excludeId || undefined },
|
||||
});
|
||||
/** 按合同查询可选项目预付核销列表(审批通过且仍有可核销余额) */
|
||||
export const getProjectAdvanceWriteOffOptions = params =>
|
||||
request({
|
||||
url: `${baseUrl}/project-advance-write-off-options`,
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
|
||||
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
||||
|
||||
@@ -31,8 +31,15 @@ export const approve = data => request({ url: `${baseUrl}/approve`, method: 'pos
|
||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||
// 同步金蝶涉及多次金蝶远程接口调用(A102-4 预检 + A102-2 保存),可达 20s+,覆盖全局 10s 超时
|
||||
export const syncKingdee = id =>
|
||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
|
||||
// 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 =>
|
||||
|
||||
@@ -24,6 +24,8 @@ export const getContractList = (current, size, params) =>
|
||||
|
||||
export const getFeeOptions = () => request({ url: `${baseUrl}/fee-options`, method: 'get' });
|
||||
|
||||
export const getNextNo = () => request({ url: `${baseUrl}/next-no`, method: 'get' });
|
||||
|
||||
export const getCandidateDetails = (current, size, params) =>
|
||||
request({
|
||||
url: `${baseUrl}/candidate-details`,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+14
-2
@@ -57,26 +57,38 @@ export const clearNonTopDept = signal => {
|
||||
});
|
||||
};
|
||||
|
||||
export const syncOaCompany = (current = 1, size = 20, signal) => {
|
||||
export const syncOaCompany = (current = 1, size = 20, signal, incremental = false, modifiedSince) => {
|
||||
return request({
|
||||
url: '/blade-system/dept/sync-oa-company',
|
||||
method: 'post',
|
||||
params: {
|
||||
current,
|
||||
size,
|
||||
incremental: !!incremental,
|
||||
modifiedSince: modifiedSince || undefined,
|
||||
},
|
||||
timeout: 60000,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const syncOaDepartment = (current = 1, size = 20, signal) => {
|
||||
export const syncOaDepartment = (
|
||||
current = 1,
|
||||
size = 20,
|
||||
signal,
|
||||
incremental = false,
|
||||
modifiedSince,
|
||||
companyOaId
|
||||
) => {
|
||||
return request({
|
||||
url: '/blade-system/dept/sync-oa-department',
|
||||
method: 'post',
|
||||
params: {
|
||||
current,
|
||||
size,
|
||||
incremental: !!incremental,
|
||||
modifiedSince: modifiedSince || undefined,
|
||||
companyOaId: companyOaId || undefined,
|
||||
},
|
||||
timeout: 60000,
|
||||
signal,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import request from '@/axios';
|
||||
|
||||
export const getList = (current, size, params) => {
|
||||
return request({
|
||||
url: '/blade-system/mp-user/list',
|
||||
method: 'get',
|
||||
params: {
|
||||
...params,
|
||||
current,
|
||||
size,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getDetail = id => {
|
||||
return request({
|
||||
url: '/blade-system/mp-user/detail',
|
||||
method: 'get',
|
||||
params: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const submit = row => {
|
||||
return request({
|
||||
url: '/blade-system/mp-user/submit',
|
||||
method: 'post',
|
||||
data: row,
|
||||
});
|
||||
};
|
||||
|
||||
export const remove = ids => {
|
||||
return request({
|
||||
url: '/blade-system/mp-user/remove',
|
||||
method: 'post',
|
||||
params: {
|
||||
ids,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const changeStatus = (id, status) => {
|
||||
return request({
|
||||
url: '/blade-system/mp-user/change-status',
|
||||
method: 'post',
|
||||
params: {
|
||||
id,
|
||||
status,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const resetPassword = userIds => {
|
||||
return request({
|
||||
url: '/blade-system/mp-user/reset-password',
|
||||
method: 'post',
|
||||
params: {
|
||||
userIds,
|
||||
},
|
||||
});
|
||||
};
|
||||
+14
-1
@@ -31,13 +31,15 @@ export const add = row => {
|
||||
});
|
||||
};
|
||||
|
||||
export const syncIamAccounts = (current = 1, size = 50, signal) => {
|
||||
export const syncIamAccounts = (current = 1, size = 50, signal, incremental = false, modifiedSince) => {
|
||||
return request({
|
||||
url: '/blade-system/user/sync-iam-accounts',
|
||||
method: 'post',
|
||||
params: {
|
||||
current,
|
||||
size,
|
||||
incremental: !!incremental,
|
||||
modifiedSince: modifiedSince || undefined,
|
||||
},
|
||||
timeout: 60000,
|
||||
signal,
|
||||
@@ -52,6 +54,17 @@ export const update = row => {
|
||||
});
|
||||
};
|
||||
|
||||
export const changeStatus = (id, status) => {
|
||||
return request({
|
||||
url: '/blade-system/user/change-status',
|
||||
method: 'post',
|
||||
params: {
|
||||
id,
|
||||
status,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const updatePlatform = (userId, userType, userExt) => {
|
||||
return request({
|
||||
url: '/blade-system/user/update-platform',
|
||||
|
||||
@@ -9,6 +9,8 @@ export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get'
|
||||
|
||||
export const follow = data => request({ url: `${baseUrl}/follow`, method: 'post', data });
|
||||
|
||||
export const update = data => request({ url: `${baseUrl}/update`, method: 'post', data });
|
||||
|
||||
export const complete = data => request({ url: `${baseUrl}/complete`, method: 'post', data });
|
||||
|
||||
export const batchComplete = ids =>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import request from '@/axios';
|
||||
|
||||
/**
|
||||
* 认证记录列表:按审核状态分页,按提交时间倒序
|
||||
* 审核对象是**一条认证记录**,不是一辆车或一名司机,故必须带 bizType 区分
|
||||
* status 为审核状态(0审核中 1已通过 2已驳回 3已作废);不传时后端按 0 处理
|
||||
*/
|
||||
export const getPendingPage = (bizType, current, size, params) => {
|
||||
return request({
|
||||
url: '/blade-transport/certification-audit/pending-page',
|
||||
method: 'get',
|
||||
params: {
|
||||
...params,
|
||||
bizType,
|
||||
current,
|
||||
size,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 审核详情:同时返回本次待审快照与该对象当前生效快照,供前端做字段级对比
|
||||
* 从未认证通过时 effectiveSnapshotJson 为空串
|
||||
*/
|
||||
export const getAuditDetail = id => {
|
||||
return request({
|
||||
url: '/blade-transport/certification-audit/detail',
|
||||
method: 'get',
|
||||
params: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 审核一条认证记录
|
||||
* 通过时不传驳回原因;驳回时必须传,且原因不超过 200 字
|
||||
*/
|
||||
export const auditCertification = (id, pass, rejectReason) => {
|
||||
return request({
|
||||
url: '/blade-transport/certification-audit/audit',
|
||||
method: 'post',
|
||||
params: {
|
||||
id,
|
||||
pass,
|
||||
...(rejectReason ? { rejectReason } : {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -51,14 +51,6 @@ export const changeStatus = (id, status) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const auditCertification = (id, certificationStatus, rejectReason) => {
|
||||
return request({
|
||||
url: '/blade-transport/transport-vehicle/audit-certification',
|
||||
method: 'post',
|
||||
params: { id, certificationStatus, rejectReason },
|
||||
});
|
||||
};
|
||||
|
||||
export const getExpiryStat = params => {
|
||||
return request({
|
||||
url: '/blade-transport/transport-vehicle/expiry-stat',
|
||||
|
||||
+23
-9
@@ -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;
|
||||
@@ -189,7 +192,14 @@ axios.interceptors.response.use(
|
||||
// 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。
|
||||
if (isJsonContentType(res) && !isPlainObject(res.data)) {
|
||||
try {
|
||||
const text = res.data instanceof Blob ? await res.data.text() : res.data;
|
||||
let text = '';
|
||||
if (typeof res.data?.text === 'function') {
|
||||
text = await res.data.text();
|
||||
} else if (typeof res.data === 'string') {
|
||||
text = res.data;
|
||||
} else if (typeof res.data?.arrayBuffer === 'function') {
|
||||
text = new TextDecoder().decode(await res.data.arrayBuffer());
|
||||
}
|
||||
const parsed = typeof text === 'string' && text ? JSON.parse(text) : null;
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
res.data = parsed;
|
||||
@@ -253,12 +263,12 @@ 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;
|
||||
},
|
||||
@@ -266,7 +276,11 @@ axios.interceptors.response.use(
|
||||
NProgress.done();
|
||||
// 网络级失败(HTTP 状态 >500、网关错误、超时、断网等 validateStatus 放行之外的响应)
|
||||
// 原为静默 reject,接口报错页面无任何感知;统一补错误提示,取消请求与刷新令牌请求除外
|
||||
if (!axios.isCancel(error) && !isRefreshTokenRequest(error?.config)) {
|
||||
if (
|
||||
!axios.isCancel(error) &&
|
||||
!isRefreshTokenRequest(error?.config) &&
|
||||
error?.config?.meta?.suppressErrorMessage !== true
|
||||
) {
|
||||
ElMessage({
|
||||
message: error?.response?.data?.msg || '请求失败,请稍后重试',
|
||||
type: 'error',
|
||||
|
||||
@@ -16,7 +16,11 @@
|
||||
<div class="address-map-picker__info">{{ status }}</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selected.longitude || resolving" @click="confirm">
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!hasCoordinateValue(selected.longitude) || !hasCoordinateValue(selected.latitude) || resolving"
|
||||
@click="confirm"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -271,7 +275,12 @@ export default {
|
||||
this.runGeocode('address', lnglat)
|
||||
.then(result => {
|
||||
const detailAddress = result.regeocode?.formattedAddress || fallbackAddress;
|
||||
this.selected = { ...this.selected, address: detailAddress };
|
||||
const region = this.resolveRegionFromGeocode(result);
|
||||
this.selected = {
|
||||
...this.selected,
|
||||
address: detailAddress,
|
||||
...region,
|
||||
};
|
||||
this.keyword = detailAddress || this.keyword;
|
||||
this.status = detailAddress || '已选点,可确认回填';
|
||||
})
|
||||
@@ -285,6 +294,26 @@ export default {
|
||||
this.resolving = false;
|
||||
});
|
||||
},
|
||||
resolveRegionFromGeocode(result = {}) {
|
||||
const component = result.regeocode?.addressComponent || {};
|
||||
const city = Array.isArray(component.city) ? '' : String(component.city || '').trim();
|
||||
const province = String(component.province || '').trim();
|
||||
const district = String(component.district || '').trim();
|
||||
const country = String(component.country || '').trim();
|
||||
const adcode = String(component.adcode || '').trim();
|
||||
return {
|
||||
country,
|
||||
province,
|
||||
city: city || province,
|
||||
district,
|
||||
adcode,
|
||||
regionCode: adcode,
|
||||
provinceName: province,
|
||||
cityName: city || province,
|
||||
districtName: district,
|
||||
countryName: country,
|
||||
};
|
||||
},
|
||||
renderMarker(point) {
|
||||
this.clearMarker();
|
||||
this.marker = new window.AMap.Marker({ position: point });
|
||||
@@ -297,8 +326,31 @@ export default {
|
||||
this.marker = null;
|
||||
}
|
||||
},
|
||||
syncSelectedFromMarker() {
|
||||
// 确认时以当前标记点坐标为准,避免换地址后仍回填上一次经纬度
|
||||
if (!this.marker || typeof this.marker.getPosition !== 'function') {
|
||||
return;
|
||||
}
|
||||
const point = this.marker.getPosition();
|
||||
if (!point) {
|
||||
return;
|
||||
}
|
||||
const longitude =
|
||||
typeof point.getLng === 'function' ? point.getLng() : point.lng ?? point.longitude;
|
||||
const latitude =
|
||||
typeof point.getLat === 'function' ? point.getLat() : point.lat ?? point.latitude;
|
||||
if (!Number.isFinite(Number(longitude)) || !Number.isFinite(Number(latitude))) {
|
||||
return;
|
||||
}
|
||||
this.selected = {
|
||||
...this.selected,
|
||||
longitude: Number(longitude),
|
||||
latitude: Number(latitude),
|
||||
};
|
||||
},
|
||||
confirm() {
|
||||
if (!this.selected.longitude || !this.selected.latitude) {
|
||||
this.syncSelectedFromMarker();
|
||||
if (!this.hasCoordinateValue(this.selected.longitude) || !this.hasCoordinateValue(this.selected.latitude)) {
|
||||
this.$message.warning('请先搜索或点击地图完成选点');
|
||||
return;
|
||||
}
|
||||
@@ -308,8 +360,18 @@ export default {
|
||||
}
|
||||
this.$emit('confirm', {
|
||||
address: this.selected.address,
|
||||
longitude: this.selected.longitude,
|
||||
latitude: this.selected.latitude,
|
||||
longitude: Number(this.selected.longitude),
|
||||
latitude: Number(this.selected.latitude),
|
||||
country: this.selected.country || this.selected.countryName || '',
|
||||
province: this.selected.province || this.selected.provinceName || '',
|
||||
city: this.selected.city || this.selected.cityName || '',
|
||||
district: this.selected.district || this.selected.districtName || '',
|
||||
adcode: this.selected.adcode || this.selected.regionCode || '',
|
||||
regionCode: this.selected.regionCode || this.selected.adcode || '',
|
||||
countryName: this.selected.countryName || this.selected.country || '',
|
||||
provinceName: this.selected.provinceName || this.selected.province || '',
|
||||
cityName: this.selected.cityName || this.selected.city || '',
|
||||
districtName: this.selected.districtName || this.selected.district || '',
|
||||
});
|
||||
this.visible = false;
|
||||
},
|
||||
|
||||
@@ -576,7 +576,7 @@ export default {
|
||||
}
|
||||
|
||||
.mode-content :deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mode-content :deep(.el-form-item:last-child) {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="avue-crud__right table-header-tools">
|
||||
<el-button v-if="showRefresh" icon="el-icon-refresh" circle @click="$emit('refresh')" />
|
||||
<el-button
|
||||
v-if="showColumn"
|
||||
icon="el-icon-operation"
|
||||
circle
|
||||
@click="columnSettingVisible = true"
|
||||
/>
|
||||
<el-button
|
||||
v-if="showSearch"
|
||||
icon="el-icon-search"
|
||||
circle
|
||||
@click="$emit('update:searchVisible', !searchVisible)"
|
||||
/>
|
||||
<el-button v-if="showGrid" icon="el-icon-grid" circle @click="toggleTableSize" />
|
||||
|
||||
<el-dialog
|
||||
v-if="showColumn"
|
||||
v-model="columnSettingVisible"
|
||||
title="列设置"
|
||||
width="360px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<el-checkbox-group
|
||||
class="table-header-tools__column-setting"
|
||||
:model-value="visibleColumnKeys"
|
||||
@update:model-value="$emit('update:visibleColumnKeys', $event)"
|
||||
>
|
||||
<el-checkbox v-for="item in columns" :key="item.prop" :label="item.prop">
|
||||
{{ item.label }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<template #footer>
|
||||
<el-button @click="columnSettingVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const TABLE_SIZE_OPTIONS = ['default', 'large', 'small'];
|
||||
|
||||
export default {
|
||||
name: 'TableHeaderTools',
|
||||
props: {
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
visibleColumnKeys: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
tableSize: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
searchVisible: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showRefresh: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showColumn: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showSearch: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showGrid: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['refresh', 'update:visibleColumnKeys', 'update:tableSize', 'update:searchVisible'],
|
||||
data() {
|
||||
return {
|
||||
columnSettingVisible: false,
|
||||
};
|
||||
},
|
||||
deactivated() {
|
||||
// keep-alive 缓存页面时关闭列设置弹窗,避免 Teleport 到 body 的弹窗切页后残留
|
||||
this.columnSettingVisible = false;
|
||||
},
|
||||
methods: {
|
||||
toggleTableSize() {
|
||||
const index = TABLE_SIZE_OPTIONS.indexOf(this.tableSize);
|
||||
const next = TABLE_SIZE_OPTIONS[(index + 1) % TABLE_SIZE_OPTIONS.length];
|
||||
this.$emit('update:tableSize', next);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.table-header-tools.avue-crud__right {
|
||||
width: auto;
|
||||
margin: 0 0 0 auto;
|
||||
flex: none;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.table-header-tools__column-setting {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 360px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -9,18 +9,21 @@
|
||||
<el-table :data="rows" border @selection-change="selectedRows = $event">
|
||||
<el-table-column v-if="!readonly" type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="240" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="240"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span
|
||||
class="vehicle-attachment-table__file-name"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>
|
||||
{{ attachmentName(row) }}
|
||||
</span>
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220">
|
||||
|
||||
@@ -305,6 +305,10 @@ export const createOption = () => ({
|
||||
searchOrder: 3,
|
||||
searchPlaceholder: '请选择',
|
||||
dicData: [],
|
||||
props: {
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
},
|
||||
filterable: true,
|
||||
hide: true,
|
||||
display: false,
|
||||
|
||||
@@ -2,6 +2,9 @@ const dimensionOptions = [
|
||||
{ label: '重量', value: '重量' },
|
||||
{ label: '体积', value: '体积' },
|
||||
{ label: '数量', value: '数量' },
|
||||
{ label: '整车', value: '整车' },
|
||||
{ label: '里程', value: '里程' },
|
||||
{ label: '吨·公里', value: '吨·公里' },
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
@@ -35,7 +38,6 @@ export const createOption = () => ({
|
||||
delBtn: false,
|
||||
selection: true,
|
||||
dialogClickModal: false,
|
||||
menuWidth: 240,
|
||||
menuFixed: 'right',
|
||||
column: [
|
||||
{
|
||||
@@ -109,6 +111,7 @@ export const createOption = () => ({
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
minWidth: 110,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
@@ -117,6 +120,7 @@ export const createOption = () => ({
|
||||
{
|
||||
label: '更新人',
|
||||
prop: 'updateUserName',
|
||||
minWidth: 110,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
@@ -128,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,
|
||||
@@ -139,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: '水路运输' },
|
||||
@@ -117,6 +117,7 @@ export const contractCategoryOptions = [
|
||||
export const signTypeOptions = [
|
||||
{ label: '新签', value: '新签' },
|
||||
{ label: '续签', value: '续签' },
|
||||
{ label: '补充协议', value: '补充协议' },
|
||||
];
|
||||
|
||||
export const contractStageOptions = [
|
||||
@@ -142,6 +143,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: '到场' },
|
||||
|
||||
@@ -6,9 +6,12 @@ import {
|
||||
contractStageOptions,
|
||||
createCrudOption,
|
||||
effectiveTypeOptions,
|
||||
nonNegativeRule,
|
||||
hcsSyncStatusOptions,
|
||||
selectRule,
|
||||
signTypeOptions,
|
||||
textRule,
|
||||
yesNoOptions,
|
||||
} from './common';
|
||||
|
||||
const contractSettlementModeOptions = [
|
||||
@@ -61,13 +64,14 @@ export const config = {
|
||||
['contractNo', '合同编号'],
|
||||
['contractName', '合同名称'],
|
||||
['projectName', '所属项目'],
|
||||
['organizationName', '所属组织'],
|
||||
['organizationName', '合同负责部门'],
|
||||
['contractCategory', '合同类别'],
|
||||
['signType', '签约类型'],
|
||||
['partyA', '甲方'],
|
||||
['partyB', '乙方'],
|
||||
['handlerUserName', '经办人'],
|
||||
['signDate', '签订日期'],
|
||||
['applyDate', '申请日期'],
|
||||
['contractFormat', '合同格式'],
|
||||
['legalSealFlag', '是否需要加盖法人章'],
|
||||
['copyCount', '一式(份)'],
|
||||
@@ -76,7 +80,8 @@ export const config = {
|
||||
['templateFlag', '是否范本'],
|
||||
['originalContractNo', '原件合同编号'],
|
||||
['electronicSealFlag', '是否电子章'],
|
||||
['remark', '备注', 2],
|
||||
['mainContent', '合同主要内容', 2],
|
||||
['remark', '其他事项说明', 2],
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -209,7 +214,7 @@ export const config = {
|
||||
label: '流程',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['approved', 'change_approved'],
|
||||
stage: ['formal', 'terminated'],
|
||||
stage: ['temporary', 'formal', 'terminated'],
|
||||
permission: 'contract_manage_view',
|
||||
},
|
||||
{
|
||||
@@ -249,8 +254,8 @@ export const option = createCrudOption([
|
||||
searchOrder: 6,
|
||||
searchPlaceholder: '请输入',
|
||||
order: 185,
|
||||
disabled: true,
|
||||
placeholder: '不填写,则系统自动生成',
|
||||
maxlength: 100,
|
||||
placeholder: '不填写则按部门规则自动生成',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
@@ -294,17 +299,17 @@ export const option = createCrudOption([
|
||||
display: false,
|
||||
},
|
||||
{
|
||||
label: '所属组织',
|
||||
label: '合同负责部门',
|
||||
prop: 'organizationName',
|
||||
search: true,
|
||||
searchOrder: 3,
|
||||
searchslot: true,
|
||||
searchPlaceholder: '请选择 所属组织',
|
||||
searchPlaceholder: '请选择 合同负责部门',
|
||||
formslot: true,
|
||||
order: 120,
|
||||
dicData: [],
|
||||
minWidth: 150,
|
||||
rules: selectRule('所属组织'),
|
||||
rules: selectRule('合同负责部门'),
|
||||
},
|
||||
{
|
||||
label: '合同类别',
|
||||
@@ -435,6 +440,17 @@ export const option = createCrudOption([
|
||||
minWidth: 130,
|
||||
rules: [{ required: true, message: '请选择签订日期', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '申请日期',
|
||||
prop: 'applyDate',
|
||||
type: 'date',
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'YYYY-MM-DD',
|
||||
sortable: true,
|
||||
minWidth: 130,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '结算类型',
|
||||
prop: 'settlementMode',
|
||||
@@ -468,11 +484,10 @@ export const option = createCrudOption([
|
||||
{ label: '否', value: 0 },
|
||||
{ label: '是', value: 1 },
|
||||
],
|
||||
value: 0,
|
||||
clearable: true,
|
||||
hide: true,
|
||||
order: 70,
|
||||
minWidth: 100,
|
||||
rules: selectRule('是否需要加盖法人章'),
|
||||
},
|
||||
{
|
||||
label: '一式(份)',
|
||||
@@ -500,6 +515,10 @@ export const option = createCrudOption([
|
||||
hide: true,
|
||||
order: 45,
|
||||
minWidth: 130,
|
||||
rules: [
|
||||
{ required: true, message: '请输入合同金额', trigger: 'blur' },
|
||||
nonNegativeRule('合同金额'),
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '是否范本',
|
||||
@@ -589,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',
|
||||
@@ -617,7 +680,19 @@ export const option = createCrudOption([
|
||||
minWidth: 110,
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
label: '合同主要内容',
|
||||
prop: 'mainContent',
|
||||
type: 'textarea',
|
||||
span: 24,
|
||||
minRows: 4,
|
||||
hide: true,
|
||||
order: 41,
|
||||
maxlength: 1000,
|
||||
showWordLimit: true,
|
||||
rules: textRule('合同主要内容', 1000),
|
||||
},
|
||||
{
|
||||
label: '其他事项说明',
|
||||
prop: 'remark',
|
||||
type: 'textarea',
|
||||
span: 24,
|
||||
@@ -626,7 +701,7 @@ export const option = createCrudOption([
|
||||
order: 40,
|
||||
maxlength: 2000,
|
||||
showWordLimit: true,
|
||||
rules: textRule('备注', 2000),
|
||||
rules: textRule('其他事项说明', 2000),
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
|
||||
@@ -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 = [
|
||||
@@ -71,7 +73,7 @@ export const config = {
|
||||
action: 'mkAudit',
|
||||
label: '流程',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing', 'change_reviewing'],
|
||||
status: ['reviewing', 'change_reviewing', 'approved', 'change_approved'],
|
||||
permission: 'project_apply_approve',
|
||||
mkOnly: true,
|
||||
},
|
||||
@@ -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',
|
||||
|
||||
@@ -400,7 +400,6 @@ export const option = {
|
||||
span: 6,
|
||||
order: 350,
|
||||
minWidth: 150,
|
||||
rules: [{ required: true, message: '请选择计划开始日期', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '计划结束日期',
|
||||
@@ -411,7 +410,6 @@ export const option = {
|
||||
span: 6,
|
||||
order: 340,
|
||||
minWidth: 150,
|
||||
rules: [{ required: true, message: '请选择计划结束日期', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '数据来源',
|
||||
|
||||
@@ -98,6 +98,24 @@ const getProcessConfigNodes = row => {
|
||||
|
||||
const isAcceptProcessNode = node => node.key === 'accept' || node.name === '接单';
|
||||
|
||||
/**
|
||||
* 过程节点列显示文案。
|
||||
*
|
||||
* 该列在库里存的是「原始值」,且历史上写入了两种格式:司机打卡推进写的是配置的
|
||||
* key(unload / dispatch / load …),配载单新建默认值写的是中文名(接单)。
|
||||
* 故这里按两种格式都认一次,认不出就原样显示,不把值吞掉。
|
||||
*
|
||||
* 运单创建后、司机首次打卡前该字段为空,属正常——显示「-」,避免留白看不出是没值还是没渲染。
|
||||
*/
|
||||
const formatCurrentProcessNode = row => {
|
||||
const value = String(getFirstValue(row, ['currentProcessNode', 'currentNode']) || '').trim();
|
||||
if (!value) return '-';
|
||||
const node = getProcessConfigNodes(row).find(
|
||||
item => item.key === value || item.name === value
|
||||
);
|
||||
return node?.name || node?.key || value;
|
||||
};
|
||||
|
||||
const isTruthyFlag = value => {
|
||||
if (value === true || value === 1) return true;
|
||||
if (value === false || value === 0 || value === null || value === undefined || value === '') {
|
||||
@@ -608,6 +626,7 @@ export const option = {
|
||||
{
|
||||
label: '过程节点',
|
||||
prop: 'currentProcessNode',
|
||||
formatter: row => formatCurrentProcessNode(row),
|
||||
minWidth: 140,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
|
||||
@@ -1,22 +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 },
|
||||
{ prop: 'kingdeeLockStatus', label: '金蝶锁状态', minWidth: 150 },
|
||||
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
|
||||
];
|
||||
/**
|
||||
* 付款申请列表(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 },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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,6 +21,8 @@ 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 },
|
||||
@@ -28,15 +32,15 @@ export const formalSettlementTableColumns = [
|
||||
];
|
||||
|
||||
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 },
|
||||
@@ -48,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 },
|
||||
];
|
||||
|
||||
@@ -66,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 },
|
||||
@@ -86,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 },
|
||||
@@ -94,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 },
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
const identityDic = [
|
||||
{ label: '司机', value: 'driver' },
|
||||
{ label: '管理员', value: 'admin' },
|
||||
];
|
||||
|
||||
const statusDic = [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '停用', value: 0 },
|
||||
];
|
||||
|
||||
const validatePhone = (rule, value, callback) => {
|
||||
const phone = String(value || '').trim();
|
||||
if (!phone) {
|
||||
callback(new Error('请输入手机号'));
|
||||
return;
|
||||
}
|
||||
if (!/^1\d{10}$/.test(phone)) {
|
||||
callback(new Error('请输入正确的手机号'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
const validateRemark = (rule, value, callback) => {
|
||||
if (String(value || '').length > 200) {
|
||||
callback(new Error('备注不能超过200字'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
height: 'auto',
|
||||
calcHeight: 22,
|
||||
dialogWidth: 760,
|
||||
labelPosition: 'right',
|
||||
labelWidth: 'auto',
|
||||
tip: false,
|
||||
searchBtnText: '查询',
|
||||
emptyBtnText: '重置',
|
||||
saveBtnText: '提交',
|
||||
updateBtnText: '提交',
|
||||
searchShow: true,
|
||||
searchIcon: true,
|
||||
searchLabelWidth: 160,
|
||||
searchMenuSpan: 24,
|
||||
searchIndex: 4,
|
||||
searchMenuPosition: 'right',
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 70,
|
||||
viewBtn: false,
|
||||
delBtn: false,
|
||||
editBtn: false,
|
||||
addBtn: false,
|
||||
selection: true,
|
||||
dialogClickModal: false,
|
||||
menuWidth: 320,
|
||||
column: [
|
||||
{
|
||||
label: '手机号',
|
||||
prop: 'phone',
|
||||
minWidth: 130,
|
||||
search: true,
|
||||
searchOrder: 4,
|
||||
searchSpan: 6,
|
||||
searchPlaceholder: '请输入',
|
||||
maxlength: 11,
|
||||
showWordLimit: true,
|
||||
order: 100,
|
||||
rules: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ validator: validatePhone, trigger: 'blur' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '账号',
|
||||
prop: 'account',
|
||||
minWidth: 130,
|
||||
search: true,
|
||||
searchOrder: 3,
|
||||
searchSpan: 6,
|
||||
searchPlaceholder: '请输入',
|
||||
maxlength: 45,
|
||||
order: 90,
|
||||
placeholder: '默认与手机号相同',
|
||||
},
|
||||
{
|
||||
label: '姓名',
|
||||
prop: 'realName',
|
||||
minWidth: 100,
|
||||
search: true,
|
||||
searchOrder: 2,
|
||||
searchSpan: 6,
|
||||
searchPlaceholder: '请输入',
|
||||
maxlength: 10,
|
||||
showWordLimit: true,
|
||||
order: 80,
|
||||
},
|
||||
{
|
||||
label: '昵称',
|
||||
prop: 'name',
|
||||
minWidth: 100,
|
||||
maxlength: 20,
|
||||
showWordLimit: true,
|
||||
order: 70,
|
||||
},
|
||||
{
|
||||
label: '小程序身份',
|
||||
prop: 'identities',
|
||||
type: 'select',
|
||||
multiple: true,
|
||||
dataType: 'string',
|
||||
dicData: identityDic,
|
||||
minWidth: 140,
|
||||
search: true,
|
||||
searchOrder: 1,
|
||||
searchSpan: 6,
|
||||
searchPlaceholder: '请选择',
|
||||
order: 60,
|
||||
rules: [{ required: true, message: '请选择小程序身份', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '性别',
|
||||
prop: 'sex',
|
||||
type: 'select',
|
||||
dicUrl: '/blade-system/dict/dictionary?code=sex',
|
||||
props: {
|
||||
label: 'dictValue',
|
||||
value: 'dictKey',
|
||||
},
|
||||
dataType: 'number',
|
||||
minWidth: 80,
|
||||
hide: true,
|
||||
order: 50,
|
||||
},
|
||||
{
|
||||
label: 'openid',
|
||||
prop: 'openid',
|
||||
minWidth: 180,
|
||||
overHidden: true,
|
||||
hide: true,
|
||||
addDisplay: false,
|
||||
editDisplay: true,
|
||||
viewDisplay: true,
|
||||
order: 40,
|
||||
placeholder: '可清空以解绑微信',
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
dicData: statusDic,
|
||||
dataType: 'number',
|
||||
minWidth: 90,
|
||||
search: true,
|
||||
searchOrder: 0,
|
||||
searchSpan: 6,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
value: 1,
|
||||
order: 30,
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
prop: 'remark',
|
||||
type: 'textarea',
|
||||
span: 24,
|
||||
minRows: 2,
|
||||
maxlength: 200,
|
||||
showWordLimit: true,
|
||||
hide: true,
|
||||
order: 20,
|
||||
rules: [{ validator: validateRemark, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
minWidth: 170,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: true,
|
||||
order: 10,
|
||||
},
|
||||
{
|
||||
label: '更新人',
|
||||
prop: 'updateUserName',
|
||||
minWidth: 100,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: true,
|
||||
order: 5,
|
||||
},
|
||||
{
|
||||
label: '更新时间',
|
||||
prop: 'updateTime',
|
||||
minWidth: 170,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: true,
|
||||
order: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -27,7 +27,8 @@ export const userOption = safe => {
|
||||
index: true,
|
||||
selection: true,
|
||||
addBtn: false,
|
||||
viewBtn: true,
|
||||
viewBtn: false,
|
||||
menuWidth: 160,
|
||||
dialogType: 'dialog',
|
||||
dialogWidth: '90%',
|
||||
labelPosition: 'right',
|
||||
@@ -37,8 +38,11 @@ export const userOption = safe => {
|
||||
{
|
||||
label: '账号名',
|
||||
prop: 'account',
|
||||
slot: true,
|
||||
minWidth: 180,
|
||||
search: true,
|
||||
searchOrder: 2,
|
||||
searchPlaceholder: '支持模糊搜索',
|
||||
display: false,
|
||||
},
|
||||
{
|
||||
@@ -46,6 +50,7 @@ export const userOption = safe => {
|
||||
prop: 'realName',
|
||||
search: true,
|
||||
searchOrder: 4,
|
||||
searchPlaceholder: '支持模糊搜索',
|
||||
display: false,
|
||||
},
|
||||
{
|
||||
@@ -53,6 +58,7 @@ export const userOption = safe => {
|
||||
prop: 'phone',
|
||||
search: true,
|
||||
searchOrder: 3,
|
||||
searchPlaceholder: '支持模糊搜索',
|
||||
display: false,
|
||||
},
|
||||
{ label: '工号', prop: 'code', display: false },
|
||||
@@ -60,11 +66,13 @@ export const userOption = safe => {
|
||||
label: '所属部门',
|
||||
prop: 'deptName',
|
||||
slot: true,
|
||||
overHidden: true,
|
||||
minWidth: 200,
|
||||
overHidden: false,
|
||||
display: false,
|
||||
},
|
||||
{ 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 },
|
||||
{
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// 一级 value 与二级 value 都用中文原文:库里 exception_type 存的是大类中文名、
|
||||
// exception_reason 存子类中文名(小程序上报即如此),此处必须与之一致,
|
||||
// 否则按异常类型做精确匹配的查询会一行都查不到。
|
||||
export const exceptionCategoryOptions = [
|
||||
{
|
||||
label: '交通异常',
|
||||
value: 'traffic',
|
||||
value: '交通异常',
|
||||
children: [
|
||||
{ label: '道路拥堵', value: '道路拥堵' },
|
||||
{ label: '交通事故', value: '交通事故' },
|
||||
@@ -12,7 +15,7 @@ export const exceptionCategoryOptions = [
|
||||
},
|
||||
{
|
||||
label: '车辆异常',
|
||||
value: 'vehicle',
|
||||
value: '车辆异常',
|
||||
children: [
|
||||
{ label: '车辆故障维修', value: '车辆故障维修' },
|
||||
{ label: '车辆事故', value: '车辆事故' },
|
||||
@@ -24,7 +27,7 @@ export const exceptionCategoryOptions = [
|
||||
},
|
||||
{
|
||||
label: '货物异常',
|
||||
value: 'cargo',
|
||||
value: '货物异常',
|
||||
children: [
|
||||
{ label: '货损', value: '货损' },
|
||||
{ label: '货差', value: '货差' },
|
||||
@@ -35,7 +38,7 @@ export const exceptionCategoryOptions = [
|
||||
},
|
||||
{
|
||||
label: '其他异常',
|
||||
value: 'other',
|
||||
value: '其他异常',
|
||||
children: [
|
||||
{ label: '变更路线地址', value: '变更路线地址' },
|
||||
{ label: '其他', value: '其他异常-其他' },
|
||||
@@ -43,6 +46,16 @@ export const exceptionCategoryOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
/** 一级类型的中文名列表,供编辑表单的级联选择使用 */
|
||||
export const exceptionTypeOptions = exceptionCategoryOptions.map(item => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
}));
|
||||
|
||||
/** 按一级类型取二级选项 */
|
||||
export const exceptionReasonOptions = typeValue =>
|
||||
exceptionCategoryOptions.find(item => item.value === typeValue)?.children || [];
|
||||
|
||||
export const disposalStatusOptions = [
|
||||
{ label: '待处理', value: 'pending' },
|
||||
{ label: '处理中', value: 'processing' },
|
||||
@@ -65,20 +78,30 @@ export const tableColumns = [
|
||||
{ label: '项目', prop: 'projectName', minWidth: 160 },
|
||||
{ label: '车辆', prop: 'vehicleNo', minWidth: 150, link: true },
|
||||
{ label: '上报人', prop: 'reporterName', minWidth: 120 },
|
||||
{ label: '上报时间', prop: 'reportTime', minWidth: 170 },
|
||||
{ label: '异常类型', prop: 'exceptionTypeName', minWidth: 160 },
|
||||
{ label: '上报说明', prop: 'reportDescription', minWidth: 180 },
|
||||
{ label: '异常节点', prop: 'nodeName', minWidth: 120 },
|
||||
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
|
||||
{ label: '最新跟进说明', prop: 'latestFollowContent', minWidth: 200 },
|
||||
{ label: '状态', prop: 'disposalStatusName', minWidth: 120 },
|
||||
];
|
||||
|
||||
export const detailFields = [
|
||||
{ label: '配载单号/总单号', prop: 'loadingOrMasterNo', link: true },
|
||||
{ label: '运单号', prop: 'waybillNo', link: true },
|
||||
{ label: '项目', prop: 'projectName', link: true },
|
||||
{ label: '车牌号', prop: 'vehicleNo', link: true },
|
||||
{ label: '上报人', prop: 'reporterName' },
|
||||
// 详情/跟进弹窗的只读信息,按来源分两块:
|
||||
// 「运单信息」是上报时从运单复制的字段,「异常信息」是司机上报的内容。
|
||||
// 与编辑弹窗的分区口径一致,改动时两处要一起改。
|
||||
export const detailWaybillFields = [
|
||||
{ label: '运单号', prop: 'waybillNo' },
|
||||
{ label: '配载单号/总单号', prop: 'loadingOrMasterNo' },
|
||||
{ label: '车牌号', prop: 'vehicleNo' },
|
||||
{ label: '项目', prop: 'projectName' },
|
||||
{ label: '承运商', prop: 'carrierName' },
|
||||
];
|
||||
|
||||
export const detailExceptionFields = [
|
||||
{ label: '上报人', prop: 'reporterName' },
|
||||
{ label: '上报时间', prop: 'reportTime' },
|
||||
{ label: '异常节点', prop: 'nodeName' },
|
||||
{ label: '异常类型', prop: 'exceptionTypeName' },
|
||||
{ label: '异常原因', prop: 'exceptionReason' },
|
||||
{ label: '上报说明', prop: 'reportDescription', span: 2 },
|
||||
@@ -89,3 +112,21 @@ export const followColumns = [
|
||||
{ label: '跟进人', prop: 'followUserName', minWidth: 130 },
|
||||
{ label: '跟进时间', prop: 'followTime', minWidth: 170 },
|
||||
];
|
||||
|
||||
// 编辑表单只开放上报四件套。车牌、项目、承运商、配载单号由上报时从运单回填,
|
||||
// 允许手改会让记录与运单脱钩;处置状态由跟进与完成流转,不在此处。
|
||||
export const editFormFields = [
|
||||
{ label: '异常类型', prop: 'exceptionType', type: 'select', required: true },
|
||||
{ label: '异常原因', prop: 'exceptionReason', type: 'select' },
|
||||
{ label: '上报说明', prop: 'reportDescription', type: 'textarea', required: true, span: 2 },
|
||||
];
|
||||
|
||||
// 编辑弹窗顶部的运单信息,只读展示,帮助运管员确认改的是哪一单。
|
||||
// 上报人与上报时间属上报行为,放在「异常信息」里,不在此处重复。
|
||||
export const editWaybillFields = [
|
||||
{ label: '运单号', prop: 'waybillNo' },
|
||||
{ label: '配载单号/总单号', prop: 'loadingOrMasterNo' },
|
||||
{ label: '车牌号', prop: 'vehicleNo' },
|
||||
{ label: '项目', prop: 'projectName' },
|
||||
{ label: '承运商', prop: 'carrierName' },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
export const option = {
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
tip: false,
|
||||
searchShow: true,
|
||||
searchMenuSpan: 24,
|
||||
searchIcon: true,
|
||||
searchIndex: 4,
|
||||
searchMenuPosition: 'right',
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 70,
|
||||
selection: false,
|
||||
addBtn: false,
|
||||
editBtn: false,
|
||||
delBtn: false,
|
||||
viewBtn: false,
|
||||
columnBtn: false,
|
||||
menuWidth: 110,
|
||||
column: [
|
||||
{
|
||||
label: '司机姓名',
|
||||
prop: 'driverName',
|
||||
search: true,
|
||||
searchOrder: 3,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '司机手机号',
|
||||
prop: 'driverMobile',
|
||||
search: true,
|
||||
searchOrder: 2,
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
label: '审核状态',
|
||||
prop: 'statusText',
|
||||
slot: true,
|
||||
width: 110,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
label: '提交时间',
|
||||
prop: 'submitTime',
|
||||
slot: true,
|
||||
minWidth: 170,
|
||||
align: 'center',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
export const option = {
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
tip: false,
|
||||
searchShow: true,
|
||||
searchMenuSpan: 24,
|
||||
searchIcon: true,
|
||||
searchIndex: 4,
|
||||
searchMenuPosition: 'right',
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 70,
|
||||
selection: false,
|
||||
addBtn: false,
|
||||
editBtn: false,
|
||||
delBtn: false,
|
||||
viewBtn: false,
|
||||
columnBtn: false,
|
||||
menuWidth: 110,
|
||||
column: [
|
||||
{
|
||||
label: '车牌号',
|
||||
prop: 'bizKey',
|
||||
search: true,
|
||||
searchOrder: 3,
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
label: '司机姓名',
|
||||
prop: 'driverName',
|
||||
search: true,
|
||||
searchOrder: 2,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '司机手机号',
|
||||
prop: 'driverMobile',
|
||||
search: true,
|
||||
searchOrder: 1,
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
label: '审核状态',
|
||||
prop: 'statusText',
|
||||
slot: true,
|
||||
width: 110,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
label: '提交时间',
|
||||
prop: 'submitTime',
|
||||
slot: true,
|
||||
minWidth: 170,
|
||||
align: 'center',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -95,6 +95,8 @@ export const option = {
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
// 这里是「最新一次提交的状态」缓存,不等于该车当前是否已认证:
|
||||
// 已认证由生效快照指针决定,曾审核通过即永久保持,重新提交不会让它退回审核中
|
||||
label: '认证状态',
|
||||
prop: 'certificationStatus',
|
||||
slot: true,
|
||||
@@ -104,9 +106,10 @@ export const option = {
|
||||
type: 'select',
|
||||
dicData: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '认证中', value: 0 },
|
||||
{ label: '已认证', value: 1 },
|
||||
{ label: '审核中', value: 0 },
|
||||
{ label: '已通过', value: 1 },
|
||||
{ label: '已驳回', value: 2 },
|
||||
{ label: '已作废', value: 3 },
|
||||
],
|
||||
minWidth: 110,
|
||||
align: 'center',
|
||||
|
||||
@@ -12,6 +12,9 @@ export const option = {
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
dialogWidth: 1200,
|
||||
// 弹窗根节点 class:avue-crud 弹窗默认 append-to-body 到 body,
|
||||
// 页面内 scoped 样式命中不到弹窗内容,故用该 class 在全局样式里做兜底(见 styles/element-ui.scss)
|
||||
dialogCustomClass: 'vehicle-dispatch-dialog',
|
||||
labelPosition: 'right',
|
||||
labelWidth: 'auto',
|
||||
tip: false,
|
||||
@@ -101,6 +104,7 @@ export const option = {
|
||||
minWidth: 120,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '当前节点',
|
||||
@@ -108,6 +112,7 @@ export const option = {
|
||||
minWidth: 120,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '当前处理人',
|
||||
@@ -115,6 +120,7 @@ export const option = {
|
||||
minWidth: 130,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
@@ -122,6 +128,7 @@ export const option = {
|
||||
minWidth: 120,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
@@ -132,6 +139,7 @@ export const option = {
|
||||
minWidth: 170,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
// ========== 新增/编辑表单专用(不进表格)==========
|
||||
{
|
||||
@@ -165,6 +173,7 @@ export const option = {
|
||||
type: 'textarea',
|
||||
hide: true,
|
||||
span: 24,
|
||||
minRows: 2,
|
||||
maxlength: 200,
|
||||
showWordLimit: true,
|
||||
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
||||
|
||||
@@ -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,
|
||||
|
||||
+103
-5
@@ -173,7 +173,10 @@
|
||||
}
|
||||
|
||||
.el-dialog .el-col {
|
||||
// 取消列自身下边距,改由 form-item 统一控制行距;
|
||||
// flow-root 避免子级 margin 折叠穿透导致换行后行距失效、校验提示被下一行遮挡
|
||||
margin-bottom: 0;
|
||||
display: flow-root;
|
||||
}
|
||||
|
||||
.el-dialog .el-form-item,
|
||||
@@ -181,7 +184,7 @@
|
||||
.el-dialog .el-form-item--small.el-form-item,
|
||||
.el-dialog .el-form-item--large.el-form-item,
|
||||
.el-dialog .el-form-item--medium.el-form-item {
|
||||
//margin-bottom: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* 全站弹窗:底部操作栏浮动效果(参照 business/project-apply)
|
||||
@@ -362,9 +365,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 弹窗内表单项默认上下间距 16px(覆盖默认 18px / 22px,与 customer-archive 对齐)
|
||||
// 4) 弹窗内表单项上下间距:预留绝对定位校验提示(约 14~18px)的可视空间
|
||||
.el-dialog .el-form-item {
|
||||
//margin-bottom: 16px;
|
||||
//margin-bottom: 24px;
|
||||
}
|
||||
|
||||
// 校验提示浮在控件下方,提高层级避免被下一行输入框背景遮挡
|
||||
.el-form-item__error {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
// 5) 通用分组白卡组件(<section-card>):标题融入卡头
|
||||
@@ -446,7 +454,7 @@
|
||||
.dialog-form--carded {
|
||||
.el-form-item {
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
@@ -644,6 +652,28 @@
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
// 车辆调度新增/编辑/查看弹窗:使用部门 el-cascader 默认 display: inline-block、宽度按内容收缩,
|
||||
// 会比同行的 el-input(--el-input-width 默认 100%)窄一大截,需显式拉满列宽。
|
||||
// 两个必须注意的点(踩过):
|
||||
// 1) avue-crud 弹窗默认 append-to-body 到 body,页面内 `.vehicle-dispatch-page :deep(...)`
|
||||
// 这类以页面容器为祖先的选择器匹配不到弹窗内容,必须用弹窗自身 class(dialogCustomClass)在全局兜底;
|
||||
// 2) avue 表单项在 el-form-item__content 与控件之间还有两层包裹 div
|
||||
// (form/index3.js 渲染:content > div > [div|elTooltip] > 控件),
|
||||
// 所以要用后代选择器,不能写 `>` 直接子级。
|
||||
.vehicle-dispatch-dialog .el-form-item__content .el-cascader {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// 车辆调度弹窗 label 宽度统一:option 用 labelWidth: 'auto' 时 label 宽度随字数变化
|
||||
// (3 字 64px / 2 字 40px / 4 字带星号 78px),控件可用宽度随之变化,
|
||||
// 于是同一行两列控件宽度不一致(如车牌号 486px vs 使用部门 472px)。
|
||||
// 本弹窗左右两列最长 label 均为 4 个汉字(所属组织 / 使用部门 / 申请时间),故统一按 4em + 24px 取宽。
|
||||
.vehicle-dispatch-dialog .el-form-item__label {
|
||||
flex: 0 0 calc(4em + 24px) !important;
|
||||
width: calc(4em + 24px) !important;
|
||||
}
|
||||
|
||||
// 过程配置新增/编辑/查看弹窗(手写 el-dialog,参照 vehicle/credit-score-quantification):
|
||||
// 灰底弹窗体(全站 .el-dialog__body 已统一)内堆叠 <section-card> 分组白卡,
|
||||
// 卡间 12px 间距由 section-card 自身 margin-bottom 保证,与项目其他弹窗风格统一
|
||||
@@ -995,6 +1025,66 @@ td.el-table-fixed-column--right {
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
// 全站统一:内容型长文本列(文件名 / 单号 / 备注等)单元格内也放了 el-link / span.attachment-file-name,
|
||||
// 会被上面两条「含 el-link 的单元格」规则一并命中,导致:
|
||||
// ① 上面第 950 行的 display: flex 让链接成为 flex item,flex item 默认 min-width: auto 无法收缩,
|
||||
// 超长文本(如未规范命名的附件名)会直接把列撑破;
|
||||
// ② 豁免规则的 overflow: visible 让溢出内容不再被单元格裁切,直接压到相邻列上。
|
||||
// 因此对「内容列」做显式兜底:单元格自身恢复块级 + 省略号截断,链接参与收缩并显示省略号。
|
||||
// 用法:在 <el-table-column> 上配置 class-name="is-ellipsis-link"(动态列可 :class-name 绑定)。
|
||||
// 操作列不要加此类,否则会失去自动换行能力。
|
||||
.el-table .el-table__body .cell.is-ellipsis-link {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
.el-link {
|
||||
display: inline-block;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
|
||||
// Element Plus 的 el-link__inner 是 inline-flex,flex 容器上的 text-overflow 不生效,
|
||||
// 必须降级为块级容器,省略号才会真正渲染出来。
|
||||
.el-link__inner {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-file-name {
|
||||
display: inline-block;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
// 附件上传表格:文件名统一用 span,视觉与可点击行为对齐原 el-link primary
|
||||
.attachment-file-name {
|
||||
color: var(--el-color-primary, #409eff);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
color: var(--el-text-color-placeholder, #c0c4cc);
|
||||
cursor: not-allowed;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 全站统一:Avue 内置行操作按钮(查看 / 编辑 / 删除 / 复制等)视觉对齐为 el-link 链接样式。
|
||||
// 选择器仅命中表格行操作列 .avue-crud__menu 内的 is-text 按钮,不误伤顶部工具栏(.avue-crud__header)
|
||||
// 与弹窗按钮(.avue-crud__dialog__menu)。事件(rowView/rowEdit/rowDel)与权限(permission 指令)
|
||||
@@ -1073,7 +1163,12 @@ td.el-table-fixed-column--right {
|
||||
align-items: normal;
|
||||
}
|
||||
.el-form-item--default {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
// 独立表单页(非弹窗)同样避免 el-col 折叠吃掉行距
|
||||
.el-form .el-col {
|
||||
display: flow-root;
|
||||
}
|
||||
|
||||
// 配套:星号必须保持「高度 auto + 继承行高」,stretch 才会按首行排布;
|
||||
@@ -1248,3 +1343,6 @@ td.el-table-fixed-column--right {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
.is-ellipsis-link {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
/**
|
||||
* 认证审核通用工具
|
||||
*
|
||||
* 认证对象(司机 / 车辆)每次提交产生一条不可变快照记录,「审核」针对的是**一条记录**。
|
||||
* 记录分两侧:本次**待审快照**与该对象**当前生效快照**(最后一次审核通过的那条)。
|
||||
* 待审内容在审核通过前不影响生效值,所以审核页必须逐字段对比两侧,而不是只看一条当前值。
|
||||
*
|
||||
* 本文件提供三件事:
|
||||
* 1. 审核状态文案与标签(与后端 statusText 同一套词表);
|
||||
* 2. 快照字段的展示元数据(中文名、控件类型、分组);
|
||||
* 3. 两侧快照的字段级 diff 计算,含「从未认证通过、无对比基准」的处理。
|
||||
*
|
||||
* 两侧快照字段名完全一致(同一套结构),故可逐键比较。
|
||||
*/
|
||||
|
||||
const EMPTY_TEXT = '—';
|
||||
|
||||
const TYPE_IMAGE = 'image';
|
||||
const TYPE_DATE = 'date';
|
||||
const TYPE_LONG_TERM = 'longTerm';
|
||||
|
||||
/** 审核状态取值 */
|
||||
export const CERT_RECORD_STATUS = {
|
||||
PENDING: 0,
|
||||
APPROVED: 1,
|
||||
REJECTED: 2,
|
||||
VOIDED: 3,
|
||||
};
|
||||
|
||||
/** 状态文案统一为:审核中 / 已通过 / 已驳回 / 已作废 */
|
||||
const CERT_STATUS_TAG = {
|
||||
0: { label: '审核中', type: 'warning' },
|
||||
1: { label: '已通过', type: 'success' },
|
||||
2: { label: '已驳回', type: 'danger' },
|
||||
3: { label: '已作废', type: 'info' },
|
||||
};
|
||||
|
||||
/** 变更类型 → 差异标签 */
|
||||
export const CERT_CHANGE_TAG = {
|
||||
added: { label: '新增', type: 'success' },
|
||||
modified: { label: '已修改', type: 'warning' },
|
||||
cleared: { label: '已清空', type: 'danger' },
|
||||
};
|
||||
|
||||
/**
|
||||
* 「最新提交状态」展示缓存 → 标签
|
||||
*
|
||||
* 缓存为空表示该对象从未提交过认证(即没有生效快照)。
|
||||
* 注意它只是最近一次提交的状态,不等于「是否已认证」——后者由生效快照指针决定。
|
||||
*/
|
||||
export const certStatusTag = status => {
|
||||
if (status === null || status === undefined || status === '') {
|
||||
return { label: '未认证', type: 'info' };
|
||||
}
|
||||
return CERT_STATUS_TAG[status] || { label: EMPTY_TEXT, type: 'info' };
|
||||
};
|
||||
|
||||
const pad = value => String(value).padStart(2, '0');
|
||||
|
||||
/** 后端若改用时间戳毫秒序列化日期,也能落成日期文案 */
|
||||
const EPOCH_PATTERN = /^\d{13}$/;
|
||||
|
||||
const isEpochText = text => EPOCH_PATTERN.test(text);
|
||||
|
||||
const toDate = text => {
|
||||
const date = new Date(Number(text));
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
};
|
||||
|
||||
/** 提交时间等日期时间展示,兼容 `yyyy-MM-dd HH:mm:ss`、ISO 串与时间戳毫秒 */
|
||||
export const formatCertDateTime = value => {
|
||||
if (value === null || value === undefined || value === '') return EMPTY_TEXT;
|
||||
const text = String(value).trim();
|
||||
const matched = text.match(
|
||||
/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})[T ](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?/
|
||||
);
|
||||
if (matched) {
|
||||
return (
|
||||
`${matched[1]}-${pad(matched[2])}-${pad(matched[3])} ` +
|
||||
`${pad(matched[4])}:${pad(matched[5])}:${pad(matched[6] || 0)}`
|
||||
);
|
||||
}
|
||||
const date = isEpochText(text) ? toDate(text) : null;
|
||||
if (!date) return text;
|
||||
return (
|
||||
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
|
||||
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 快照字段元数据
|
||||
*
|
||||
* 字段名与后端提交 DTO 的 JSON 键一致(司机 PersonCertSubmitRequest / 车辆 VehicleCertSubmitRequest),
|
||||
* 两侧快照共用同一套结构,故同时用于标签与比较规则。
|
||||
*
|
||||
* `longTermKey` 只挂在**被长期有效作废掉的那个字段**上,也就是各组的「有效期止」与强制报废日期:
|
||||
* 勾了长期有效,它们显示「长期有效」、比较时按同一个值算(避免每次审核都报「已变更」)。
|
||||
* 「有效期起」不挂:起期是一个独立的日期,长期有效并不会让它失效,
|
||||
* 挂上去等于把它自己的值藏起来、还让审核员看不到起期的改动。
|
||||
*/
|
||||
const CERT_SNAPSHOT_GROUPS = {
|
||||
driver: [
|
||||
{
|
||||
title: '身份信息',
|
||||
fields: [
|
||||
{ key: 'realName', label: '真实姓名' },
|
||||
{ key: 'idCardNo', label: '身份证号' },
|
||||
{ key: 'idCardFrontUrl', label: '身份证正面照', type: TYPE_IMAGE },
|
||||
{ key: 'idCardBackUrl', label: '身份证反面照', type: TYPE_IMAGE },
|
||||
{ key: 'idCardValidStart', label: '身份证有效期起', type: TYPE_DATE },
|
||||
{
|
||||
key: 'idCardValidEnd',
|
||||
label: '身份证有效期止',
|
||||
type: TYPE_DATE,
|
||||
longTermKey: 'idCardLongTerm',
|
||||
},
|
||||
{ key: 'idCardLongTerm', label: '身份证长期有效', type: TYPE_LONG_TERM },
|
||||
{ key: 'headPhoto', label: '大头照', type: TYPE_IMAGE },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '驾驶证信息',
|
||||
fields: [
|
||||
{ key: 'driverFileNo', label: '驾驶证档案编号' },
|
||||
{ key: 'driverClass', label: '准驾车型' },
|
||||
{ key: 'driverLicenseFrontUrl', label: '驾驶证主页照片', type: TYPE_IMAGE },
|
||||
{ key: 'driverLicenseBackUrl', label: '驾驶证副页照片', type: TYPE_IMAGE },
|
||||
{ key: 'driverValidStart', label: '驾驶证有效期起', type: TYPE_DATE },
|
||||
{
|
||||
key: 'driverValidEnd',
|
||||
label: '驾驶证有效期止',
|
||||
type: TYPE_DATE,
|
||||
longTermKey: 'driverLongTerm',
|
||||
},
|
||||
{ key: 'driverLongTerm', label: '驾驶证长期有效', type: TYPE_LONG_TERM },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '从业资格证信息',
|
||||
fields: [
|
||||
{ key: 'qualificationType', label: '从业资格认证类型' },
|
||||
{ key: 'qualificationNo', label: '从业资格证号' },
|
||||
{
|
||||
key: 'qualificationFront',
|
||||
label: '从业资格证封面页',
|
||||
type: TYPE_IMAGE,
|
||||
legacyKeys: ['qualificationUrl'],
|
||||
},
|
||||
{ key: 'qualificationBack', label: '从业资格证内容页', type: TYPE_IMAGE },
|
||||
{ key: 'qualificationValidStart', label: '从业资格证有效期起', type: TYPE_DATE },
|
||||
{
|
||||
key: 'qualificationValidEnd',
|
||||
label: '从业资格证有效期止',
|
||||
type: TYPE_DATE,
|
||||
longTermKey: 'qualificationLongTerm',
|
||||
},
|
||||
{ key: 'qualificationLongTerm', label: '从业资格证长期有效', type: TYPE_LONG_TERM },
|
||||
],
|
||||
},
|
||||
],
|
||||
/*
|
||||
* 车辆字段名以 VehicleCertSubmitRequest 为准,与 PersonCertSubmitRequest 不同。
|
||||
* 键名对不上不报错——每个字段都算「没有值」,两组全显示「—」且 diff 不出变更。
|
||||
*/
|
||||
vehicle: [
|
||||
{
|
||||
title: '车辆基本信息',
|
||||
fields: [
|
||||
{ key: 'plateNo', label: '车牌号' },
|
||||
{ key: 'vehicleType', label: '车辆类型' },
|
||||
{ key: 'plateColor', label: '车牌颜色' },
|
||||
{ key: 'ownerName', label: '所有人' },
|
||||
{ key: 'vin', label: '车辆识别代号(车架号)' },
|
||||
{ key: 'outerLength', label: '外廓长度(mm)' },
|
||||
{ key: 'outerWidth', label: '外廓宽度(mm)' },
|
||||
{ key: 'outerHeight', label: '外廓高度(mm)' },
|
||||
{ key: 'approvedLoadKg', label: '核定载质量(KG)' },
|
||||
{ key: 'tractionMassKg', label: '准牵引总质量(KG)' },
|
||||
{ key: 'energyType', label: '能源类型' },
|
||||
{ key: 'businessRelation', label: '业务关系' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '行驶证信息',
|
||||
fields: [
|
||||
{ key: 'compulsoryScrapDate', label: '强制报废日期', type: TYPE_DATE, longTermKey: 'compulsoryScrapLongTerm' },
|
||||
{ key: 'compulsoryScrapLongTerm', label: '强制报废长期有效', type: TYPE_LONG_TERM },
|
||||
{ key: 'drivingLicenseNo', label: '行驶证档案编号' },
|
||||
{ key: 'drivingLicenseImage', label: '行驶证主页正面', type: TYPE_IMAGE },
|
||||
{ key: 'drivingLicenseMainBack', label: '行驶证主页反面', type: TYPE_IMAGE },
|
||||
{ key: 'drivingLicenseViceFront', label: '行驶证副页正面', type: TYPE_IMAGE },
|
||||
{ key: 'drivingLicenseViceBack', label: '行驶证副页反面', type: TYPE_IMAGE },
|
||||
{ key: 'drivingLicenseStartDate', label: '行驶证有效期起', type: TYPE_DATE },
|
||||
{
|
||||
key: 'drivingLicenseEndDate',
|
||||
label: '行驶证有效期止',
|
||||
type: TYPE_DATE,
|
||||
longTermKey: 'drivingLicenseLongTerm',
|
||||
},
|
||||
{ key: 'drivingLicenseLongTerm', label: '行驶证长期有效', type: TYPE_LONG_TERM },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '道路运输证信息',
|
||||
fields: [
|
||||
{ key: 'roadTransportCertNo', label: '道路运输证号' },
|
||||
{ key: 'roadTransportCertImage', label: '道路运输证照片', type: TYPE_IMAGE },
|
||||
{ key: 'roadTransportCertStartDate', label: '道路运输证有效期起', type: TYPE_DATE },
|
||||
{
|
||||
key: 'roadTransportCertEndDate',
|
||||
label: '道路运输证有效期止',
|
||||
type: TYPE_DATE,
|
||||
longTermKey: 'roadTransportCertLongTerm',
|
||||
},
|
||||
{ key: 'roadTransportCertLongTerm', label: '道路运输证长期有效', type: TYPE_LONG_TERM },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '机动车登记本信息',
|
||||
fields: [
|
||||
{ key: 'registrationNo', label: '机动车登记编号' },
|
||||
{ key: 'registrationImage', label: '机动车登记本照片', type: TYPE_IMAGE },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/** 提交元信息与主键不参与对比(两侧必相同,展示出来只是噪音) */
|
||||
const IGNORED_KEYS = [
|
||||
'id',
|
||||
'bizType',
|
||||
'bizId',
|
||||
'bizKey',
|
||||
'certificationStatus',
|
||||
'snapshotJson',
|
||||
'pendingSnapshotJson',
|
||||
'effectiveSnapshotJson',
|
||||
];
|
||||
|
||||
/**
|
||||
* 解析快照 JSON
|
||||
*
|
||||
* 后端把 null 序列化为空串,故空串 / 'null' 都按「没有快照」处理,不算解析失败。
|
||||
*/
|
||||
const readSnapshot = json => {
|
||||
if (json && typeof json === 'object') {
|
||||
return { value: Object.keys(json).length ? json : null, invalid: false };
|
||||
}
|
||||
const text = String(json === null || json === undefined ? '' : json).trim();
|
||||
if (!text || text === 'null') {
|
||||
return { value: null, invalid: false };
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
return { value: parsed, invalid: false };
|
||||
}
|
||||
return { value: null, invalid: true };
|
||||
} catch (error) {
|
||||
return { value: null, invalid: true };
|
||||
}
|
||||
};
|
||||
|
||||
/** 解析快照 JSON,无快照或解析失败时返回 null */
|
||||
export const parseSnapshot = json => readSnapshot(json).value;
|
||||
|
||||
const isEmptyValue = value => value === null || value === undefined || value === '';
|
||||
|
||||
const isLongTermOn = value => value === 1 || value === '1' || value === true;
|
||||
|
||||
/** 日期归一化到 yyyy-MM-dd,便于 `2026-01-01` 与 `2026-01-01T00:00:00` 判等 */
|
||||
const normalizeDate = value => {
|
||||
if (isEmptyValue(value)) return '';
|
||||
const text = String(value).trim();
|
||||
const matched = text.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/);
|
||||
if (matched) return `${matched[1]}-${pad(matched[2])}-${pad(matched[3])}`;
|
||||
const date = isEpochText(text) ? toDate(text) : null;
|
||||
if (date) {
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
/**
|
||||
* 取字段值,兼容字段改名前的旧键
|
||||
*
|
||||
* 后端把某字段改名后,改名之前提交的记录里存的仍是旧键(例:从业资格证封面页
|
||||
* 由 `qualificationUrl` 改为 `qualificationFront`)。只读新键会得到空,
|
||||
* 审核页就把已填的封面页显示成「—」,误导审核员以为对方没传。
|
||||
* 新旧键都存在时以新键为准。
|
||||
*/
|
||||
const readFieldValue = (snapshot, field) => {
|
||||
if (!snapshot) return undefined;
|
||||
const value = snapshot[field.key];
|
||||
if (!isEmptyValue(value)) return value;
|
||||
const legacyKeys = field.legacyKeys || [];
|
||||
for (let i = 0; i < legacyKeys.length; i += 1) {
|
||||
if (!isEmptyValue(snapshot[legacyKeys[i]])) return snapshot[legacyKeys[i]];
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
/** 比较用的归一化取值:展示相同即视为未变更 */
|
||||
const canonicalOf = (snapshot, field) => {
|
||||
const value = readFieldValue(snapshot, field);
|
||||
if (field.type === TYPE_LONG_TERM) {
|
||||
if (isEmptyValue(value)) return '';
|
||||
return isLongTermOn(value) ? '1' : '0';
|
||||
}
|
||||
if (field.type === TYPE_DATE) {
|
||||
if (field.longTermKey && isLongTermOn(snapshot && snapshot[field.longTermKey])) {
|
||||
return '长期有效';
|
||||
}
|
||||
return normalizeDate(value);
|
||||
}
|
||||
return isEmptyValue(value) ? '' : String(value).trim();
|
||||
};
|
||||
|
||||
/** 展示文案:日期型受「长期有效」影响,图片型返回 URL 由组件渲染缩略图 */
|
||||
const textOf = (snapshot, field) => {
|
||||
const value = readFieldValue(snapshot, field);
|
||||
if (field.type === TYPE_IMAGE) {
|
||||
return isEmptyValue(value) ? '' : String(value);
|
||||
}
|
||||
if (field.type === TYPE_LONG_TERM) {
|
||||
if (isEmptyValue(value)) return EMPTY_TEXT;
|
||||
return isLongTermOn(value) ? '是' : '否';
|
||||
}
|
||||
if (field.type === TYPE_DATE) {
|
||||
if (field.longTermKey && isLongTermOn(snapshot && snapshot[field.longTermKey])) {
|
||||
return '长期有效';
|
||||
}
|
||||
return normalizeDate(value) || EMPTY_TEXT;
|
||||
}
|
||||
return isEmptyValue(value) ? EMPTY_TEXT : String(value);
|
||||
};
|
||||
|
||||
const buildRow = (field, pendingSnapshot, effectiveSnapshot, hasBaseline) => {
|
||||
const type = field.type || 'text';
|
||||
const target = { ...field, type };
|
||||
const pendingCanonical = canonicalOf(pendingSnapshot, target);
|
||||
const effectiveCanonical = canonicalOf(effectiveSnapshot, target);
|
||||
let changeType = '';
|
||||
if (hasBaseline && pendingCanonical !== effectiveCanonical) {
|
||||
if (!effectiveCanonical) {
|
||||
changeType = 'added';
|
||||
} else if (!pendingCanonical) {
|
||||
changeType = 'cleared';
|
||||
} else {
|
||||
changeType = 'modified';
|
||||
}
|
||||
}
|
||||
return {
|
||||
key: field.key,
|
||||
label: field.label,
|
||||
type,
|
||||
effective: {
|
||||
raw: readFieldValue(effectiveSnapshot, target),
|
||||
text: textOf(effectiveSnapshot, target),
|
||||
},
|
||||
pending: {
|
||||
raw: readFieldValue(pendingSnapshot, target),
|
||||
text: textOf(pendingSnapshot, target),
|
||||
},
|
||||
changed: Boolean(changeType),
|
||||
changeType,
|
||||
};
|
||||
};
|
||||
|
||||
const unionKeys = (pendingSnapshot, effectiveSnapshot) => {
|
||||
const keys = [];
|
||||
[pendingSnapshot, effectiveSnapshot].forEach(snapshot => {
|
||||
Object.keys(snapshot || {}).forEach(key => {
|
||||
if (!keys.includes(key)) keys.push(key);
|
||||
});
|
||||
});
|
||||
return keys;
|
||||
};
|
||||
|
||||
/**
|
||||
* 构造「待审快照 vs 当前生效值」的字段级 diff
|
||||
*
|
||||
* @param {string} bizType 认证对象类型:driver / vehicle
|
||||
* @param {string} pendingJson 本次待审快照 JSON
|
||||
* @param {string} effectiveJson 该对象当前生效快照 JSON,从未认证通过时为空
|
||||
* @returns {Object} { hasBaseline, groups, totalCount, changedCount, ... }
|
||||
* hasBaseline 为 false 表示无对比基准(从未认证通过),此时不产出任何变更标记
|
||||
*/
|
||||
export const buildSnapshotDiff = (bizType, pendingJson, effectiveJson) => {
|
||||
const pending = readSnapshot(pendingJson);
|
||||
const effective = readSnapshot(effectiveJson);
|
||||
const hasBaseline = effective.value !== null;
|
||||
const groupsConfig = CERT_SNAPSHOT_GROUPS[bizType] || [];
|
||||
const knownKeys = [];
|
||||
|
||||
const groups = groupsConfig.map(group => {
|
||||
const rows = group.fields.map(field => {
|
||||
// 旧键已由所属字段并入展示,不能再作为独立字段出现,否则同一张封面页会出现两行
|
||||
knownKeys.push(field.key, ...(field.legacyKeys || []));
|
||||
return buildRow(field, pending.value, effective.value, hasBaseline);
|
||||
});
|
||||
return {
|
||||
title: group.title,
|
||||
rows,
|
||||
changedCount: rows.filter(row => row.changed).length,
|
||||
};
|
||||
});
|
||||
|
||||
// 元数据未登记的字段照样展示,避免后端新增字段时审核员看不到变化
|
||||
const extraKeys = unionKeys(pending.value, effective.value).filter(
|
||||
key => !knownKeys.includes(key) && !IGNORED_KEYS.includes(key)
|
||||
);
|
||||
if (extraKeys.length) {
|
||||
const rows = extraKeys.map(key =>
|
||||
buildRow({ key, label: key }, pending.value, effective.value, hasBaseline)
|
||||
);
|
||||
groups.push({
|
||||
title: '其他字段',
|
||||
rows,
|
||||
changedCount: rows.filter(row => row.changed).length,
|
||||
});
|
||||
}
|
||||
|
||||
const rows = groups.reduce((all, group) => all.concat(group.rows), []);
|
||||
return {
|
||||
hasBaseline,
|
||||
pendingInvalid: pending.invalid,
|
||||
effectiveInvalid: effective.invalid,
|
||||
groups,
|
||||
totalCount: rows.length,
|
||||
changedCount: rows.filter(row => row.changed).length,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 合同下拉/选择展示:合同编号/合同名称
|
||||
*
|
||||
* @param {Object} contract 合同对象
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatContractOptionLabel(contract = {}) {
|
||||
const contractNo = String(contract?.contractNo ?? '').trim();
|
||||
const contractName = String(contract?.contractName ?? '').trim();
|
||||
if (contractNo && contractName) {
|
||||
return `${contractNo}/${contractName}`;
|
||||
}
|
||||
return contractNo || contractName || '';
|
||||
}
|
||||
@@ -13,6 +13,92 @@ const coordinateRanges = {
|
||||
},
|
||||
};
|
||||
|
||||
/** WGS-84 → GCJ-02(高德)偏移算法常量 */
|
||||
const WGS84_TO_GCJ02_PI = Math.PI;
|
||||
const WGS84_TO_GCJ02_A = 6378245.0;
|
||||
const WGS84_TO_GCJ02_EE = 0.00669342162296594323;
|
||||
|
||||
const isOutOfChina = (longitude, latitude) =>
|
||||
longitude < 72.004 || longitude > 137.8347 || latitude < 0.8293 || latitude > 55.8271;
|
||||
|
||||
const transformLatitude = (longitude, latitude) => {
|
||||
let result =
|
||||
-100.0 +
|
||||
2.0 * longitude +
|
||||
3.0 * latitude +
|
||||
0.2 * latitude * latitude +
|
||||
0.1 * longitude * latitude +
|
||||
0.2 * Math.sqrt(Math.abs(longitude));
|
||||
result +=
|
||||
((20.0 * Math.sin(6.0 * longitude * WGS84_TO_GCJ02_PI) +
|
||||
20.0 * Math.sin(2.0 * longitude * WGS84_TO_GCJ02_PI)) *
|
||||
2.0) /
|
||||
3.0;
|
||||
result +=
|
||||
((20.0 * Math.sin(latitude * WGS84_TO_GCJ02_PI) +
|
||||
40.0 * Math.sin((latitude / 3.0) * WGS84_TO_GCJ02_PI)) *
|
||||
2.0) /
|
||||
3.0;
|
||||
result +=
|
||||
((160.0 * Math.sin((latitude / 12.0) * WGS84_TO_GCJ02_PI) +
|
||||
320 * Math.sin((latitude * WGS84_TO_GCJ02_PI) / 30.0)) *
|
||||
2.0) /
|
||||
3.0;
|
||||
return result;
|
||||
};
|
||||
|
||||
const transformLongitude = (longitude, latitude) => {
|
||||
let result =
|
||||
300.0 +
|
||||
longitude +
|
||||
2.0 * latitude +
|
||||
0.1 * longitude * longitude +
|
||||
0.1 * longitude * latitude +
|
||||
0.1 * Math.sqrt(Math.abs(longitude));
|
||||
result +=
|
||||
((20.0 * Math.sin(6.0 * longitude * WGS84_TO_GCJ02_PI) +
|
||||
20.0 * Math.sin(2.0 * longitude * WGS84_TO_GCJ02_PI)) *
|
||||
2.0) /
|
||||
3.0;
|
||||
result +=
|
||||
((20.0 * Math.sin(longitude * WGS84_TO_GCJ02_PI) +
|
||||
40.0 * Math.sin((longitude / 3.0) * WGS84_TO_GCJ02_PI)) *
|
||||
2.0) /
|
||||
3.0;
|
||||
result +=
|
||||
((150.0 * Math.sin((longitude / 12.0) * WGS84_TO_GCJ02_PI) +
|
||||
300.0 * Math.sin((longitude / 30.0) * WGS84_TO_GCJ02_PI)) *
|
||||
2.0) /
|
||||
3.0;
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* WGS-84(GPS / LBS)转 GCJ-02(高德/国测局),供高德地图绘制使用。
|
||||
* 中国境外坐标不做偏移,原样返回。
|
||||
* @param {number} longitude
|
||||
* @param {number} latitude
|
||||
* @returns {[number, number]} [经度, 纬度]
|
||||
*/
|
||||
export const wgs84ToGcj02 = (longitude, latitude) => {
|
||||
const lng = Number(longitude);
|
||||
const lat = Number(latitude);
|
||||
if (!Number.isFinite(lng) || !Number.isFinite(lat) || isOutOfChina(lng, lat)) {
|
||||
return [lng, lat];
|
||||
}
|
||||
let dLat = transformLatitude(lng - 105.0, lat - 35.0);
|
||||
let dLng = transformLongitude(lng - 105.0, lat - 35.0);
|
||||
const radLat = (lat / 180.0) * WGS84_TO_GCJ02_PI;
|
||||
let magic = Math.sin(radLat);
|
||||
magic = 1 - WGS84_TO_GCJ02_EE * magic * magic;
|
||||
const sqrtMagic = Math.sqrt(magic);
|
||||
dLat =
|
||||
(dLat * 180.0) /
|
||||
(((WGS84_TO_GCJ02_A * (1 - WGS84_TO_GCJ02_EE)) / (magic * sqrtMagic)) * WGS84_TO_GCJ02_PI);
|
||||
dLng = (dLng * 180.0) / ((WGS84_TO_GCJ02_A / sqrtMagic) * Math.cos(radLat) * WGS84_TO_GCJ02_PI);
|
||||
return [lng + dLng, lat + dLat];
|
||||
};
|
||||
|
||||
export const normalizeCoordinateInput = value => {
|
||||
const rawValue = String(value ?? '').replace(/[^\d.-]/g, '');
|
||||
const isNegative = rawValue.startsWith('-');
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
const CURRENCY_NAME_MAP = {
|
||||
CNY: '人民币',
|
||||
RMB: '人民币',
|
||||
USD: '美元',
|
||||
EUR: '欧元',
|
||||
JPY: '日元',
|
||||
GBP: '英镑',
|
||||
};
|
||||
|
||||
const CURRENCY_UNIT_MAP = {
|
||||
CNY: '元',
|
||||
RMB: '元',
|
||||
USD: '美元',
|
||||
EUR: '欧元',
|
||||
JPY: '日元',
|
||||
GBP: '英镑',
|
||||
};
|
||||
|
||||
const CURRENCY_NAME_TO_UNIT = {
|
||||
人民币: '元',
|
||||
美元: '美元',
|
||||
欧元: '欧元',
|
||||
日元: '日元',
|
||||
英镑: '英镑',
|
||||
};
|
||||
|
||||
/**
|
||||
* 归一化币种代码(CNY → RMB)
|
||||
*/
|
||||
export function normalizeCurrencyCode(value, fallback = 'RMB') {
|
||||
const raw =
|
||||
typeof value === 'object'
|
||||
? value?.dictKey || value?.value || value?.code || value?.dictValue || ''
|
||||
: value;
|
||||
const normalized = String(raw || fallback)
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
const code = normalized.includes(' - ') ? normalized.split(' - ')[0].trim() : normalized;
|
||||
if (!code) return fallback;
|
||||
return code === 'CNY' ? 'RMB' : code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 币种中文名称
|
||||
*/
|
||||
export function currencyName(value) {
|
||||
const text = String(value || '');
|
||||
if (text.includes(' - ')) return text.slice(text.indexOf(' - ') + 3);
|
||||
return CURRENCY_NAME_MAP[text.toUpperCase()] || text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金额单位文案:RMB/CNY → 元,USD → 美元
|
||||
*/
|
||||
export function currencyUnit(value) {
|
||||
const text = String(value || '');
|
||||
let code = text;
|
||||
if (text.includes(' - ')) {
|
||||
code = text.split(' - ')[0].trim();
|
||||
}
|
||||
const upperCode = code.toUpperCase();
|
||||
if (CURRENCY_UNIT_MAP[upperCode]) {
|
||||
return CURRENCY_UNIT_MAP[upperCode];
|
||||
}
|
||||
if (CURRENCY_NAME_TO_UNIT[code]) {
|
||||
return CURRENCY_NAME_TO_UNIT[code];
|
||||
}
|
||||
return text || '元';
|
||||
}
|
||||
|
||||
/**
|
||||
* 将计价单位前缀替换为当前币种单位,如 元/吨 → 美元/吨
|
||||
*/
|
||||
export function remapPriceUnitCurrency(priceUnit, currency) {
|
||||
const text = String(priceUnit || '').trim();
|
||||
if (!text || !text.includes('/')) return text;
|
||||
const suffix = text.slice(text.indexOf('/'));
|
||||
return `${currencyUnit(currency)}${suffix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按数量单位生成默认计价单位
|
||||
*/
|
||||
export function resolvePriceUnitByQuantityUnit(quantityUnit, currency = 'RMB') {
|
||||
const unit = currencyUnit(currency);
|
||||
return (
|
||||
{
|
||||
吨: `${unit}/吨`,
|
||||
方: `${unit}/立方`,
|
||||
件: `${unit}/件`,
|
||||
}[String(quantityUnit || '').trim()] || ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字典中的计价单位选项映射到当前币种
|
||||
*/
|
||||
export function mapFeeUnitOptions(options = [], currency = 'RMB') {
|
||||
const unit = currencyUnit(currency);
|
||||
return (options || []).map(item => {
|
||||
const label = String(item.label || item.value || '');
|
||||
const value = String(item.value || item.label || '');
|
||||
return {
|
||||
...item,
|
||||
label: label.replace(/^元/, unit).replace(/^人民币/, unit),
|
||||
value: value.replace(/^元/, unit).replace(/^人民币/, unit),
|
||||
};
|
||||
});
|
||||
}
|
||||
+100
-23
@@ -6,7 +6,31 @@ import 'nprogress/nprogress.css';
|
||||
|
||||
const excelPattern = /\.(xls|xlsx)$/;
|
||||
|
||||
const isExcelResponse = res => {
|
||||
/** 是否为浏览器 Blob(含跨 realm 的 Blob-like) */
|
||||
export const isBlobLike = value =>
|
||||
value != null &&
|
||||
typeof value === 'object' &&
|
||||
typeof value.size === 'number' &&
|
||||
typeof value.type === 'string' &&
|
||||
(typeof value.text === 'function' || typeof value.arrayBuffer === 'function');
|
||||
|
||||
/** axios 拦截器已把 JSON Blob 解析成的业务对象 */
|
||||
export const isBusinessJson = value =>
|
||||
value != null &&
|
||||
typeof value === 'object' &&
|
||||
!isBlobLike(value) &&
|
||||
!(value instanceof ArrayBuffer) &&
|
||||
('code' in value || 'success' in value || 'msg' in value);
|
||||
|
||||
export const isExcelResponse = res => {
|
||||
// 已是业务 JSON(导入成功/失败提示)时,绝不能当失败明细 Excel 处理
|
||||
if (isBusinessJson(res?.data)) {
|
||||
return false;
|
||||
}
|
||||
// 只有文件流才可能是失败明细;普通对象没有 .text/.arrayBuffer
|
||||
if (!isBlobLike(res?.data)) {
|
||||
return false;
|
||||
}
|
||||
const headers = res.headers || {};
|
||||
const contentType = String(headers['content-type'] || res.data?.type || '').toLowerCase();
|
||||
const contentDisposition = String(headers['content-disposition'] || '').toLowerCase();
|
||||
@@ -23,8 +47,34 @@ const isExcelResponse = res => {
|
||||
);
|
||||
};
|
||||
|
||||
const parseBlobJson = async blob => {
|
||||
const text = await blob.text();
|
||||
/**
|
||||
* 解析导入接口响应体为 JSON 对象。
|
||||
* <p>
|
||||
* axios 拦截器在 Content-Type 为 JSON 时,会把 `responseType: 'blob'` 的 Blob
|
||||
* 提前解析成普通对象;此处需同时兼容 Blob / 字符串 / 已解析对象,避免再调
|
||||
* `blob.text()` 时出现「blob.text is not a function」。
|
||||
*/
|
||||
export const parseBlobJson = async data => {
|
||||
if (data == null) {
|
||||
return {};
|
||||
}
|
||||
if (isBusinessJson(data)) {
|
||||
return data;
|
||||
}
|
||||
if (typeof data === 'object' && !isBlobLike(data) && !(data instanceof ArrayBuffer)) {
|
||||
return data;
|
||||
}
|
||||
if (typeof data === 'string') {
|
||||
return data ? JSON.parse(data) : {};
|
||||
}
|
||||
let text = '';
|
||||
if (typeof data.text === 'function') {
|
||||
text = await data.text();
|
||||
} else if (typeof data.arrayBuffer === 'function') {
|
||||
text = new TextDecoder().decode(await data.arrayBuffer());
|
||||
} else if (data instanceof ArrayBuffer) {
|
||||
text = new TextDecoder().decode(data);
|
||||
}
|
||||
return text ? JSON.parse(text) : {};
|
||||
};
|
||||
|
||||
@@ -39,18 +89,21 @@ const parseBlobJson = async blob => {
|
||||
* 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。
|
||||
*/
|
||||
const resolveErrorMessage = async error => {
|
||||
const blob = error?.response?.data ?? error?.data;
|
||||
if (blob instanceof Blob) {
|
||||
const payload = error?.response?.data ?? error?.data;
|
||||
if (isBusinessJson(payload) && payload.msg) {
|
||||
return payload.msg;
|
||||
}
|
||||
if (isBlobLike(payload) || typeof payload === 'string') {
|
||||
try {
|
||||
const result = await parseBlobJson(blob);
|
||||
const result = await parseBlobJson(payload);
|
||||
if (result?.msg) return result.msg;
|
||||
} catch (ignored) {
|
||||
// 响应体不是 JSON(如网关返回的 HTML 错误页),退回通用文案
|
||||
}
|
||||
}
|
||||
const fallback = error?.response?.data?.msg || error?.message;
|
||||
const fallback = payload?.msg || error?.message;
|
||||
// axios 的英文通用文案对用户没有意义,统一换成中文提示
|
||||
if (!fallback || /Request failed with status code/i.test(fallback)) {
|
||||
if (!fallback || /Request failed with status code|\.text is not a function/i.test(fallback)) {
|
||||
return '导入失败,请检查导入文件后重试';
|
||||
}
|
||||
return fallback;
|
||||
@@ -68,8 +121,11 @@ const resolveErrorMessage = async error => {
|
||||
*/
|
||||
const decorateFailExcel = async (blob, businessName, failDetailDecorator) => {
|
||||
if (typeof failDetailDecorator !== 'function') return blob;
|
||||
if (!isBlobLike(blob) && !(blob instanceof ArrayBuffer)) {
|
||||
return blob;
|
||||
}
|
||||
const ExcelJS = await import('exceljs');
|
||||
const buf = await blob.arrayBuffer();
|
||||
const buf = typeof blob.arrayBuffer === 'function' ? await blob.arrayBuffer() : blob;
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(buf);
|
||||
await failDetailDecorator(workbook, { businessName });
|
||||
@@ -77,6 +133,31 @@ const decorateFailExcel = async (blob, businessName, failDetailDecorator) => {
|
||||
return new Blob([out], { type: 'application/vnd.ms-excel' });
|
||||
};
|
||||
|
||||
/** 通知上传组件成功:只传业务响应体,避免其误调 axios 响应上的 .text() */
|
||||
const notifyUploadSuccess = (option, res) => {
|
||||
if (typeof option?.onSuccess !== 'function') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const body = isBusinessJson(res?.data) ? res.data : { code: 200, success: true };
|
||||
option.onSuccess(body);
|
||||
} catch (callbackError) {
|
||||
console.warn('upload onSuccess callback failed', callbackError);
|
||||
}
|
||||
};
|
||||
|
||||
/** 通知上传组件失败 */
|
||||
const notifyUploadError = (option, error) => {
|
||||
if (typeof option?.onError !== 'function') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
option.onError(error instanceof Error ? error : new Error(String(error?.message || error || '导入失败')));
|
||||
} catch (callbackError) {
|
||||
console.warn('upload onError callback failed', callbackError);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开导入弹窗
|
||||
* @param {Object} vm 组件实例
|
||||
@@ -108,9 +189,7 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
||||
const fileName = file && file.name ? file.name.toLowerCase() : '';
|
||||
if (!excelPattern.test(fileName)) {
|
||||
vm.$message.error('请上传 .xls,.xlsx 标准格式文件');
|
||||
if (typeof option.onError === 'function') {
|
||||
option.onError(new Error('请上传 .xls,.xlsx 标准格式文件'));
|
||||
}
|
||||
notifyUploadError(option, new Error('请上传 .xls,.xlsx 标准格式文件'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -130,12 +209,11 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
||||
);
|
||||
vm.$message.warning('部分数据导入失败,已下载失败明细');
|
||||
} else {
|
||||
const result = await parseBlobJson(res.data);
|
||||
if (result.code !== 200) {
|
||||
// 成功体常被 axios 拦截器提前解析成对象;优先走业务 JSON,避免再调 Blob.text
|
||||
const result = isBusinessJson(res?.data) ? res.data : await parseBlobJson(res?.data);
|
||||
if (Number(result.code) !== 200) {
|
||||
vm.$message.error(result.msg || '导入失败');
|
||||
if (typeof option.onError === 'function') {
|
||||
option.onError(new Error(result.msg || '导入失败'));
|
||||
}
|
||||
notifyUploadError(option, new Error(result.msg || '导入失败'));
|
||||
return;
|
||||
}
|
||||
vm.$message.success('导入完成');
|
||||
@@ -149,18 +227,17 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
||||
} else if (typeof vm.initTree === 'function') {
|
||||
vm.initTree();
|
||||
}
|
||||
if (typeof option.onSuccess === 'function') {
|
||||
option.onSuccess(res);
|
||||
}
|
||||
// 上传组件 onSuccess 约定接收「响应体」,不是完整 axios 响应。
|
||||
// 若传入 axios 响应对象,部分环境下会按 Fetch Response 调用 .text(),
|
||||
// 从而在导入已成功后仍弹出「e.text is not a function」。
|
||||
notifyUploadSuccess(option, res);
|
||||
} catch (error) {
|
||||
// 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次,
|
||||
// 只保留一次清晰的中文提示。
|
||||
if (!error?._notified) {
|
||||
vm.$message.error(await resolveErrorMessage(error));
|
||||
}
|
||||
if (typeof option.onError === 'function') {
|
||||
option.onError(error);
|
||||
}
|
||||
notifyUploadError(option, error);
|
||||
} finally {
|
||||
loading.close();
|
||||
NProgress.done();
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { getList as getMeasurementUnitList } from '@/api/base/measurement-unit';
|
||||
|
||||
/**
|
||||
* 加载启用状态的计量单位选项,展示/取值均为「计量单位」字段(unitName)。
|
||||
* @returns {Promise<Array<{ label: string, value: string, dimension: string, id: string|number }>>}
|
||||
*/
|
||||
export const loadMeasurementUnitOptions = () => {
|
||||
return getMeasurementUnitList(1, 9999, { status: 1 }).then(res => {
|
||||
const data = res?.data?.data || res?.data || {};
|
||||
const records = Array.isArray(data) ? data : data.records || [];
|
||||
const names = new Set();
|
||||
return records
|
||||
.filter(item => item.status === undefined || item.status === null || Number(item.status) === 1)
|
||||
.map(item => {
|
||||
const name = String(item.unitName || '').trim();
|
||||
if (!name || names.has(name)) return null;
|
||||
names.add(name);
|
||||
return {
|
||||
id: item.id,
|
||||
label: name,
|
||||
value: name,
|
||||
dimension: String(item.dimension || '').trim(),
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
});
|
||||
};
|
||||
+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] || '';
|
||||
}
|
||||
@@ -144,12 +144,13 @@
|
||||
|
||||
<script>
|
||||
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/airport-master';
|
||||
import { getLazyTree } from '@/api/base/region';
|
||||
import { getDetail as getRegionDetail, getLazyTree } from '@/api/base/region';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
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';
|
||||
@@ -322,6 +323,20 @@ export default {
|
||||
prop: 'shortName',
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
label: '详细地址',
|
||||
prop: 'detailAddress',
|
||||
formslot: true,
|
||||
span: 24,
|
||||
minWidth: 220,
|
||||
overHidden: false,
|
||||
maxlength: 255,
|
||||
showWordLimit: true,
|
||||
rules: [
|
||||
{ required: true, message: '请选择详细地址', trigger: 'change' },
|
||||
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '所属省份',
|
||||
prop: 'provinceCode',
|
||||
@@ -388,20 +403,6 @@ export default {
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '详细地址',
|
||||
prop: 'detailAddress',
|
||||
formslot: true,
|
||||
span: 24,
|
||||
minWidth: 220,
|
||||
overHidden: false,
|
||||
maxlength: 255,
|
||||
showWordLimit: true,
|
||||
rules: [
|
||||
{ required: true, message: '请选择详细地址', trigger: 'change' },
|
||||
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '经度',
|
||||
prop: 'longitude',
|
||||
@@ -578,33 +579,51 @@ export default {
|
||||
return targetColumns.find(item => item.prop === targetProp) || null;
|
||||
},
|
||||
loadProvinceOptions() {
|
||||
getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
|
||||
return getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
|
||||
this.provinceOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||
this.findColumn('provinceCode').dicData = this.provinceOptions;
|
||||
const provinceColumn = this.findColumn('provinceCode');
|
||||
if (provinceColumn) {
|
||||
provinceColumn.dicData = this.provinceOptions;
|
||||
}
|
||||
return this.provinceOptions;
|
||||
});
|
||||
},
|
||||
loadCityOptions(provinceCode) {
|
||||
if (!provinceCode) {
|
||||
this.cityOptions = [];
|
||||
this.findColumn('cityCode').dicData = [];
|
||||
return Promise.resolve();
|
||||
const cityColumn = this.findColumn('cityCode');
|
||||
if (cityColumn) {
|
||||
cityColumn.dicData = [];
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return getLazyTree(provinceCode).then(res => {
|
||||
this.cityOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||
this.findColumn('cityCode').dicData = this.cityOptions;
|
||||
const cityColumn = this.findColumn('cityCode');
|
||||
if (cityColumn) {
|
||||
cityColumn.dicData = this.cityOptions;
|
||||
}
|
||||
return this.cityOptions;
|
||||
});
|
||||
},
|
||||
loadDistrictOptions(cityCode) {
|
||||
if (!cityCode) {
|
||||
this.districtOptions = [];
|
||||
this.findColumn('districtCode').dicData = [];
|
||||
return Promise.resolve();
|
||||
const districtColumn = this.findColumn('districtCode');
|
||||
if (districtColumn) {
|
||||
districtColumn.dicData = [];
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
this.districtLoading = true;
|
||||
return getLazyTree(cityCode)
|
||||
.then(res => {
|
||||
this.districtOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||
this.findColumn('districtCode').dicData = this.districtOptions;
|
||||
const districtColumn = this.findColumn('districtCode');
|
||||
if (districtColumn) {
|
||||
districtColumn.dicData = this.districtOptions;
|
||||
}
|
||||
return this.districtOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.districtLoading = false;
|
||||
@@ -631,6 +650,22 @@ export default {
|
||||
: String(row.districtCode),
|
||||
};
|
||||
},
|
||||
ensureCityOption(row) {
|
||||
if (!row.cityCode || !row.cityName) return;
|
||||
const exists = this.cityOptions.some(item => item.id === row.cityCode);
|
||||
if (exists) return;
|
||||
this.cityOptions = [
|
||||
...this.cityOptions,
|
||||
{
|
||||
id: row.cityCode,
|
||||
title: row.cityName,
|
||||
},
|
||||
];
|
||||
const cityColumn = this.findColumn('cityCode');
|
||||
if (cityColumn) {
|
||||
cityColumn.dicData = this.cityOptions;
|
||||
}
|
||||
},
|
||||
ensureDistrictOption(row) {
|
||||
if (!row.districtCode || !row.districtName) return;
|
||||
const exists = this.districtOptions.some(item => item.id === row.districtCode);
|
||||
@@ -642,7 +677,10 @@ export default {
|
||||
title: row.districtName,
|
||||
},
|
||||
];
|
||||
this.findColumn('districtCode').dicData = this.districtOptions;
|
||||
const districtColumn = this.findColumn('districtCode');
|
||||
if (districtColumn) {
|
||||
districtColumn.dicData = this.districtOptions;
|
||||
}
|
||||
},
|
||||
handleIataChange(value) {
|
||||
this.form.iataCode = String(value || '')
|
||||
@@ -698,25 +736,265 @@ export default {
|
||||
openAddressMapPicker() {
|
||||
this.addressMapPickerVisible = true;
|
||||
},
|
||||
formatMapCoordinate(value) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return '';
|
||||
}
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number)) {
|
||||
return '';
|
||||
}
|
||||
return normalizeCoordinateInput(number.toFixed(6));
|
||||
},
|
||||
patchDialogForm(patch = {}) {
|
||||
Object.assign(this.form, patch);
|
||||
const tableForm = this.$refs.crud?.tableForm;
|
||||
if (tableForm && tableForm !== this.form) {
|
||||
Object.assign(tableForm, patch);
|
||||
}
|
||||
},
|
||||
handleAddressMapConfirm(payload) {
|
||||
const selection = typeof payload === 'string' ? { address: payload } : payload || {};
|
||||
const longitude = this.formatMapCoordinate(selection.longitude);
|
||||
const latitude = this.formatMapCoordinate(selection.latitude);
|
||||
const patch = {};
|
||||
if (selection.address) {
|
||||
this.form.detailAddress = selection.address;
|
||||
patch.detailAddress = selection.address;
|
||||
}
|
||||
if (longitude !== '' && latitude !== '') {
|
||||
patch.longitude = longitude;
|
||||
patch.latitude = latitude;
|
||||
}
|
||||
if (Object.keys(patch).length) {
|
||||
this.patchDialogForm(patch);
|
||||
}
|
||||
this.applyMapRegionSelection(selection);
|
||||
},
|
||||
normalizeAmapCityName(city, province) {
|
||||
if (Array.isArray(city)) {
|
||||
return String(province || '').trim();
|
||||
}
|
||||
const cityName = String(city || '').trim();
|
||||
return cityName || String(province || '').trim();
|
||||
},
|
||||
buildRegionLookupCodes(adcode) {
|
||||
const code = String(adcode || '').trim();
|
||||
if (!code) {
|
||||
return [];
|
||||
}
|
||||
const codes = [code];
|
||||
if (/^\d{6}$/.test(code)) {
|
||||
if (code.endsWith('00')) {
|
||||
const cityCode = code.slice(0, 4);
|
||||
if (cityCode && !codes.includes(cityCode)) {
|
||||
codes.push(cityCode);
|
||||
}
|
||||
}
|
||||
if (code.endsWith('0000')) {
|
||||
const provinceCode = code.slice(0, 2);
|
||||
if (provinceCode && !codes.includes(provinceCode)) {
|
||||
codes.push(provinceCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
return codes;
|
||||
},
|
||||
fetchRegionDetailByCodes(codes = []) {
|
||||
const tryNext = index => {
|
||||
if (index >= codes.length) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return getRegionDetail(codes[index])
|
||||
.then(res => {
|
||||
const detail = res.data && res.data.data;
|
||||
if (detail && detail.code) {
|
||||
return detail;
|
||||
}
|
||||
return tryNext(index + 1);
|
||||
})
|
||||
.catch(() => tryNext(index + 1));
|
||||
};
|
||||
return tryNext(0);
|
||||
},
|
||||
resolveProvinceCode(provinceName) {
|
||||
const name = String(provinceName || '').trim();
|
||||
if (!name) {
|
||||
return '';
|
||||
}
|
||||
const province = this.provinceOptions.find(
|
||||
item => item.title === name || item.id === name || String(item.title).includes(name)
|
||||
);
|
||||
return province ? province.id : '';
|
||||
},
|
||||
resolveCityCode(cityName, regionCode) {
|
||||
const name = String(cityName || '').trim();
|
||||
const code = String(regionCode || '').trim();
|
||||
const city = this.cityOptions.find(
|
||||
item => item.title === name || item.id === code || item.id === name
|
||||
);
|
||||
return city ? city.id : '';
|
||||
},
|
||||
resolveDistrictCode(districtName, regionCode) {
|
||||
const name = String(districtName || '').trim();
|
||||
const code = String(regionCode || '').trim();
|
||||
const district = this.districtOptions.find(
|
||||
item => item.title === name || item.id === code || item.id === name
|
||||
);
|
||||
return district ? district.id : code || '';
|
||||
},
|
||||
applyRegionDetail(detail = {}) {
|
||||
const provinceName = detail.provinceName || '';
|
||||
const cityName = detail.cityName || '';
|
||||
const districtName = detail.districtName || detail.name || '';
|
||||
return this.loadProvinceOptions()
|
||||
.then(() => {
|
||||
const provinceCode =
|
||||
String(detail.provinceCode || '').trim() || this.resolveProvinceCode(provinceName);
|
||||
const province = this.provinceOptions.find(item => item.id === provinceCode);
|
||||
this.patchDialogForm({
|
||||
provinceCode: provinceCode || undefined,
|
||||
provinceName: province ? province.title : provinceName,
|
||||
});
|
||||
if (!provinceCode) {
|
||||
return null;
|
||||
}
|
||||
return this.loadCityOptions(provinceCode);
|
||||
})
|
||||
.then(cities => {
|
||||
if (!cities) {
|
||||
return null;
|
||||
}
|
||||
const cityCode =
|
||||
String(detail.cityCode || '').trim() ||
|
||||
this.resolveCityCode(cityName, detail.districtCode || detail.code);
|
||||
const city = this.cityOptions.find(item => item.id === cityCode);
|
||||
this.patchDialogForm({
|
||||
cityCode: cityCode || undefined,
|
||||
cityName: city ? city.title : cityName,
|
||||
});
|
||||
this.ensureCityOption({
|
||||
cityCode,
|
||||
cityName: city ? city.title : cityName,
|
||||
});
|
||||
if (!cityCode) {
|
||||
return null;
|
||||
}
|
||||
return this.loadDistrictOptions(cityCode);
|
||||
})
|
||||
.then(districts => {
|
||||
if (!districts) {
|
||||
return;
|
||||
}
|
||||
const districtCode = this.resolveDistrictCode(
|
||||
districtName,
|
||||
detail.districtCode || detail.code
|
||||
);
|
||||
this.patchDialogForm({
|
||||
districtCode: districtCode || undefined,
|
||||
});
|
||||
this.handleDistrictChange(districtCode);
|
||||
this.ensureDistrictOption({
|
||||
districtCode,
|
||||
districtName: this.form.districtName || districtName,
|
||||
});
|
||||
});
|
||||
},
|
||||
applyMapRegionByNames(selection = {}) {
|
||||
const provinceName = String(selection.province || selection.provinceName || '').trim();
|
||||
const cityName = this.normalizeAmapCityName(
|
||||
selection.city || selection.cityName,
|
||||
provinceName
|
||||
);
|
||||
const districtName = String(selection.district || selection.districtName || '').trim();
|
||||
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||
// 本页无国家字段:国内 adcode 走中国省份树;无匹配则不强制回填
|
||||
if (
|
||||
selection.longitude !== undefined &&
|
||||
selection.longitude !== null &&
|
||||
selection.longitude !== ''
|
||||
!provinceName &&
|
||||
!cityName &&
|
||||
!districtName &&
|
||||
!(/^\d{6}$/.test(adcode) || String(selection.country || selection.countryName || '').includes('中国'))
|
||||
) {
|
||||
this.form.longitude = Number(selection.longitude).toFixed(6);
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (
|
||||
selection.latitude !== undefined &&
|
||||
selection.latitude !== null &&
|
||||
selection.latitude !== ''
|
||||
) {
|
||||
this.form.latitude = Number(selection.latitude).toFixed(6);
|
||||
return this.loadProvinceOptions()
|
||||
.then(() => {
|
||||
const provinceCode = this.resolveProvinceCode(provinceName);
|
||||
const province = this.provinceOptions.find(item => item.id === provinceCode);
|
||||
this.patchDialogForm({
|
||||
provinceCode: provinceCode || undefined,
|
||||
provinceName: province ? province.title : provinceName,
|
||||
});
|
||||
if (!provinceCode) {
|
||||
return null;
|
||||
}
|
||||
return this.loadCityOptions(provinceCode);
|
||||
})
|
||||
.then(cities => {
|
||||
if (!cities) {
|
||||
return null;
|
||||
}
|
||||
const cityCode = this.resolveCityCode(cityName, adcode);
|
||||
const city = this.cityOptions.find(item => item.id === cityCode);
|
||||
this.patchDialogForm({
|
||||
cityCode: cityCode || undefined,
|
||||
cityName: city ? city.title : cityName,
|
||||
});
|
||||
this.ensureCityOption({
|
||||
cityCode,
|
||||
cityName: city ? city.title : cityName,
|
||||
});
|
||||
if (!cityCode) {
|
||||
return null;
|
||||
}
|
||||
return this.loadDistrictOptions(cityCode);
|
||||
})
|
||||
.then(districts => {
|
||||
if (!districts) {
|
||||
return;
|
||||
}
|
||||
const districtCode = this.resolveDistrictCode(districtName, adcode);
|
||||
this.patchDialogForm({
|
||||
districtCode: districtCode || undefined,
|
||||
});
|
||||
this.handleDistrictChange(districtCode);
|
||||
this.ensureDistrictOption({
|
||||
districtCode,
|
||||
districtName: this.form.districtName || districtName,
|
||||
});
|
||||
});
|
||||
},
|
||||
applyMapRegionSelection(selection = {}) {
|
||||
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||
const hasNameHint = Boolean(
|
||||
selection.country ||
|
||||
selection.countryName ||
|
||||
selection.province ||
|
||||
selection.provinceName ||
|
||||
selection.city ||
|
||||
selection.cityName ||
|
||||
selection.district ||
|
||||
selection.districtName
|
||||
);
|
||||
if (!adcode && !hasNameHint) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.regionInitializing = true;
|
||||
const lookupCodes = this.buildRegionLookupCodes(adcode);
|
||||
return this.fetchRegionDetailByCodes(lookupCodes)
|
||||
.then(detail => {
|
||||
if (detail) {
|
||||
return this.applyRegionDetail(detail);
|
||||
}
|
||||
return this.applyMapRegionByNames(selection);
|
||||
})
|
||||
.catch(() => this.applyMapRegionByNames(selection))
|
||||
.finally(() => {
|
||||
this.$nextTick(() => {
|
||||
window.setTimeout(() => {
|
||||
this.regionInitializing = false;
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
},
|
||||
normalizeRow(row) {
|
||||
row.code = row.iataCode ? `JC-${row.iataCode}` : row.code;
|
||||
@@ -905,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;
|
||||
|
||||
@@ -344,15 +344,21 @@ export default {
|
||||
getDeptTree(this.userInfo.tenantId).then(res => {
|
||||
this.deptOptions = this.flattenDept(res.data.data || []);
|
||||
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
|
||||
deptColumn.dicData = this.deptOptions;
|
||||
if (deptColumn) {
|
||||
deptColumn.dicData = this.deptOptions;
|
||||
}
|
||||
});
|
||||
},
|
||||
flattenDept(tree, level = 0) {
|
||||
const result = [];
|
||||
tree.forEach(item => {
|
||||
// 雪花 ID 必须保持字符串,避免 JS Number 精度丢失导致筛错组织。
|
||||
const id =
|
||||
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
|
||||
if (!id) return;
|
||||
result.push({
|
||||
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
||||
value: item.id,
|
||||
value: id,
|
||||
rawLabel: item.title || item.deptName || item.name,
|
||||
});
|
||||
if (item.children && item.children.length) {
|
||||
@@ -361,6 +367,19 @@ export default {
|
||||
});
|
||||
return result;
|
||||
},
|
||||
normalizeListQuery(params = {}) {
|
||||
// 搜索条件优先于缓存的 query,避免旧组织 ID 覆盖用户新选择的组织。
|
||||
const query = { ...this.query, ...params };
|
||||
const rawDeptId = query.deptId ?? query.searchDeptId;
|
||||
delete query.searchDeptId;
|
||||
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
|
||||
query.deptId = String(rawDeptId).split(',')[0].trim();
|
||||
} else {
|
||||
delete query.deptId;
|
||||
}
|
||||
query.allDept = this.isAdmin ? 1 : 0;
|
||||
return query;
|
||||
},
|
||||
getRecords(res) {
|
||||
const data = res && res.data && res.data.data;
|
||||
if (Array.isArray(data)) return data;
|
||||
@@ -967,13 +986,15 @@ export default {
|
||||
},
|
||||
searchReset() {
|
||||
this.query = {};
|
||||
this.allDept = this.isAdmin ? 1 : 0;
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
searchChange(params, done) {
|
||||
this.query = params;
|
||||
this.query = this.normalizeListQuery(params);
|
||||
this.allDept = this.query.allDept;
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page, params);
|
||||
this.onLoad(this.page, this.query);
|
||||
done();
|
||||
},
|
||||
selectionChange(list) {
|
||||
@@ -994,7 +1015,10 @@ export default {
|
||||
},
|
||||
onLoad(page, params = {}) {
|
||||
this.loading = true;
|
||||
getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: this.allDept })
|
||||
const query = this.normalizeListQuery(params);
|
||||
this.query = query;
|
||||
this.allDept = query.allDept;
|
||||
getList(page.currentPage, page.pageSize, query)
|
||||
.then(res => {
|
||||
const data = res.data.data;
|
||||
this.page.total = data.total;
|
||||
@@ -1027,8 +1051,7 @@ export default {
|
||||
},
|
||||
buildExportParams() {
|
||||
return {
|
||||
...this.query,
|
||||
allDept: this.allDept,
|
||||
...this.normalizeListQuery(this.query),
|
||||
ids: this.ids,
|
||||
[this.website.tokenHeader]: getToken(),
|
||||
};
|
||||
|
||||
@@ -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 || [];
|
||||
|
||||
@@ -264,12 +264,13 @@ import {
|
||||
changeStatus,
|
||||
getPortSelect,
|
||||
} from '@/api/base/port-terminal';
|
||||
import { getLazyTree } from '@/api/base/region';
|
||||
import { getDetail as getRegionDetail, getLazyTree } from '@/api/base/region';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
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';
|
||||
@@ -584,7 +585,7 @@ export default {
|
||||
span: 12,
|
||||
minWidth: 220,
|
||||
overHidden: false,
|
||||
order: 85,
|
||||
order: 90,
|
||||
placeholder: '点击选择地图地址',
|
||||
maxlength: 255,
|
||||
showWordLimit: true,
|
||||
@@ -841,10 +842,60 @@ export default {
|
||||
},
|
||||
handleCountryNameChange(value) {
|
||||
const country = this.countryOptions.find(item => item.title === value);
|
||||
this.setProvinceOptions([]);
|
||||
this.setCityOptions([]);
|
||||
this.setDistrictOptions([]);
|
||||
this.loadProvinceOptions(country ? country.id : '');
|
||||
this.clearSearchCityAndDistrict();
|
||||
// 搜索栏为国家 → 城市 → 区县(无省份),选中国家后需直接加载该国下全部城市
|
||||
this.loadCityOptionsByCountry(country ? country.id : '');
|
||||
},
|
||||
clearSearchCityAndDistrict() {
|
||||
const searchForm = this.$refs.crud && this.$refs.crud.searchForm;
|
||||
if (searchForm) {
|
||||
searchForm.city = undefined;
|
||||
searchForm.districtName = undefined;
|
||||
}
|
||||
if (this.query) {
|
||||
this.query.city = undefined;
|
||||
this.query.districtName = undefined;
|
||||
}
|
||||
},
|
||||
loadCityOptionsByCountry(countryCode) {
|
||||
if (!countryCode) {
|
||||
this.setCityOptions([]);
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
this.cityLoading = true;
|
||||
return this.loadProvinceOptions(countryCode)
|
||||
.then(provinces =>
|
||||
Promise.all((provinces || []).map(province => this.fetchCityOptions(province.id)))
|
||||
)
|
||||
.then(cityGroups => {
|
||||
const cityMap = new Map();
|
||||
cityGroups.flat().forEach(city => {
|
||||
if (city && city.id && !cityMap.has(city.id)) {
|
||||
cityMap.set(city.id, city);
|
||||
}
|
||||
});
|
||||
const cities = Array.from(cityMap.values());
|
||||
this.setCityOptions(cities);
|
||||
return cities;
|
||||
})
|
||||
.finally(() => {
|
||||
this.cityLoading = false;
|
||||
});
|
||||
},
|
||||
fetchCityOptions(provinceCode) {
|
||||
if (!provinceCode) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
if (this.cityOptionCache[provinceCode]) {
|
||||
return Promise.resolve(this.cityOptionCache[provinceCode]);
|
||||
}
|
||||
return getLazyTree(provinceCode).then(res => {
|
||||
const options = (res.data.data || []).map(this.normalizeRegionOption);
|
||||
this.cityOptionCache[provinceCode] = options;
|
||||
return options;
|
||||
});
|
||||
},
|
||||
syncCountryName(countryCode) {
|
||||
const country = this.countryOptions.find(item => item.id === countryCode);
|
||||
@@ -925,10 +976,8 @@ export default {
|
||||
return Promise.resolve(this.cityOptionCache[provinceCode]);
|
||||
}
|
||||
this.cityLoading = true;
|
||||
return getLazyTree(provinceCode)
|
||||
.then(res => {
|
||||
const options = (res.data.data || []).map(this.normalizeRegionOption);
|
||||
this.cityOptionCache[provinceCode] = options;
|
||||
return this.fetchCityOptions(provinceCode)
|
||||
.then(options => {
|
||||
this.setCityOptions(options);
|
||||
return options;
|
||||
})
|
||||
@@ -1047,6 +1096,13 @@ export default {
|
||||
handleCityNameChange(value) {
|
||||
const city = this.cityOptions.find(item => item.title === value);
|
||||
this.setDistrictOptions([]);
|
||||
const searchForm = this.$refs.crud && this.$refs.crud.searchForm;
|
||||
if (searchForm) {
|
||||
searchForm.districtName = undefined;
|
||||
}
|
||||
if (this.query) {
|
||||
this.query.districtName = undefined;
|
||||
}
|
||||
this.loadDistrictOptions(city ? city.id : '');
|
||||
},
|
||||
handleDistrictChange(value) {
|
||||
@@ -1060,17 +1116,224 @@ export default {
|
||||
openAddressMapPicker() {
|
||||
this.addressMapPickerVisible = true;
|
||||
},
|
||||
formatMapCoordinate(value) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return '';
|
||||
}
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number)) {
|
||||
return '';
|
||||
}
|
||||
return normalizeCoordinateInput(number.toFixed(6));
|
||||
},
|
||||
patchDialogForm(patch = {}) {
|
||||
// 同步页面 form 与 Avue 弹窗内部 tableForm,避免只改了一边导致经纬度不刷新
|
||||
Object.assign(this.form, patch);
|
||||
const tableForm = this.$refs.crud?.tableForm;
|
||||
if (tableForm && tableForm !== this.form) {
|
||||
Object.assign(tableForm, patch);
|
||||
}
|
||||
},
|
||||
handleAddressMapConfirm(payload) {
|
||||
const selection = typeof payload === 'string' ? { address: payload } : payload || {};
|
||||
const longitude = this.formatMapCoordinate(selection.longitude);
|
||||
const latitude = this.formatMapCoordinate(selection.latitude);
|
||||
const patch = {};
|
||||
if (selection.address) {
|
||||
this.form.detailAddress = selection.address;
|
||||
patch.detailAddress = selection.address;
|
||||
}
|
||||
if (selection.longitude !== undefined && selection.longitude !== null && selection.longitude !== '') {
|
||||
this.form.longitude = Number(selection.longitude).toFixed(6);
|
||||
// 换地址时必须同步覆盖经纬度,不能保留上一次选点
|
||||
if (longitude !== '' && latitude !== '') {
|
||||
patch.longitude = longitude;
|
||||
patch.latitude = latitude;
|
||||
}
|
||||
if (selection.latitude !== undefined && selection.latitude !== null && selection.latitude !== '') {
|
||||
this.form.latitude = Number(selection.latitude).toFixed(6);
|
||||
if (Object.keys(patch).length) {
|
||||
this.patchDialogForm(patch);
|
||||
}
|
||||
this.applyMapRegionSelection(selection);
|
||||
},
|
||||
normalizeAmapCityName(city, province) {
|
||||
if (Array.isArray(city)) {
|
||||
return String(province || '').trim();
|
||||
}
|
||||
const cityName = String(city || '').trim();
|
||||
return cityName || String(province || '').trim();
|
||||
},
|
||||
buildRegionLookupCodes(adcode) {
|
||||
const code = String(adcode || '').trim();
|
||||
if (!code) {
|
||||
return [];
|
||||
}
|
||||
const codes = [code];
|
||||
if (/^\d{6}$/.test(code)) {
|
||||
if (code.endsWith('00')) {
|
||||
const cityCode = code.slice(0, 4);
|
||||
if (cityCode && !codes.includes(cityCode)) {
|
||||
codes.push(cityCode);
|
||||
}
|
||||
}
|
||||
if (code.endsWith('0000')) {
|
||||
const provinceCode = code.slice(0, 2);
|
||||
if (provinceCode && !codes.includes(provinceCode)) {
|
||||
codes.push(provinceCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
return codes;
|
||||
},
|
||||
fetchRegionDetailByCodes(codes = []) {
|
||||
const tryNext = index => {
|
||||
if (index >= codes.length) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return getRegionDetail(codes[index])
|
||||
.then(res => {
|
||||
const detail = res.data && res.data.data;
|
||||
if (detail && detail.code) {
|
||||
return detail;
|
||||
}
|
||||
return tryNext(index + 1);
|
||||
})
|
||||
.catch(() => tryNext(index + 1));
|
||||
};
|
||||
return tryNext(0);
|
||||
},
|
||||
resolveCountryFromRegionDetail(detail = {}) {
|
||||
const ancestors = String(detail.ancestors || '')
|
||||
.split(',')
|
||||
.map(item => String(item || '').trim())
|
||||
.filter(Boolean);
|
||||
for (let index = 0; index < ancestors.length; index += 1) {
|
||||
const ancestorCode = ancestors[index];
|
||||
if (this.countryOptions.some(item => item.id === ancestorCode)) {
|
||||
return ancestorCode;
|
||||
}
|
||||
}
|
||||
if (
|
||||
detail.parentCode &&
|
||||
Number(detail.regionLevel) === 1 &&
|
||||
this.countryOptions.some(item => item.id === String(detail.parentCode))
|
||||
) {
|
||||
return String(detail.parentCode);
|
||||
}
|
||||
if (/^\d{6}$/.test(String(detail.code || ''))) {
|
||||
return DEFAULT_COUNTRY_CODE;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
applyRegionDetail(detail = {}) {
|
||||
const countryHint = this.resolveCountryFromRegionDetail(detail);
|
||||
return this.resolveCountryCode(countryHint || DEFAULT_COUNTRY_CODE)
|
||||
.then(countryCode => {
|
||||
this.form.countryCode = countryCode;
|
||||
this.syncCountryName(countryCode);
|
||||
return this.loadProvinceOptions(countryCode);
|
||||
})
|
||||
.then(() => {
|
||||
this.form.provinceCode =
|
||||
String(detail.provinceCode || '') || this.resolveProvinceCode(detail.provinceName);
|
||||
const province = this.provinceOptions.find(item => item.id === this.form.provinceCode);
|
||||
this.form.provinceName = province ? province.title : detail.provinceName || '';
|
||||
return this.loadCityOptions(this.form.provinceCode);
|
||||
})
|
||||
.then(() => {
|
||||
this.form.cityCode =
|
||||
String(detail.cityCode || '') ||
|
||||
this.resolveCityCode(detail.cityName, detail.districtCode || detail.code);
|
||||
const city = this.cityOptions.find(item => item.id === this.form.cityCode);
|
||||
this.form.city = city ? city.title : detail.cityName || '';
|
||||
return this.loadDistrictOptions(this.form.cityCode);
|
||||
})
|
||||
.then(() => {
|
||||
this.form.districtCode = this.resolveDistrictCode(
|
||||
detail.districtName || detail.name,
|
||||
detail.districtCode || detail.code
|
||||
);
|
||||
this.handleDistrictChange(this.form.districtCode);
|
||||
});
|
||||
},
|
||||
applyMapRegionByNames(selection = {}) {
|
||||
const provinceName = String(selection.province || selection.provinceName || '').trim();
|
||||
const cityName = this.normalizeAmapCityName(
|
||||
selection.city || selection.cityName,
|
||||
provinceName
|
||||
);
|
||||
const districtName = String(selection.district || selection.districtName || '').trim();
|
||||
const countryName = String(selection.country || selection.countryName || '').trim();
|
||||
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||
const countryPromise = countryName
|
||||
? this.resolveCountryCode(countryName)
|
||||
: Promise.resolve(/^\d{6}$/.test(adcode) ? DEFAULT_COUNTRY_CODE : '');
|
||||
return countryPromise
|
||||
.then(countryCode => {
|
||||
if (!countryCode) {
|
||||
return null;
|
||||
}
|
||||
this.form.countryCode = countryCode;
|
||||
this.syncCountryName(countryCode);
|
||||
return this.loadProvinceOptions(countryCode);
|
||||
})
|
||||
.then(provinces => {
|
||||
if (!provinces) {
|
||||
return null;
|
||||
}
|
||||
this.form.provinceCode = this.resolveProvinceCode(provinceName);
|
||||
const province = this.provinceOptions.find(item => item.id === this.form.provinceCode);
|
||||
this.form.provinceName = province ? province.title : provinceName;
|
||||
if (!this.form.provinceCode) {
|
||||
this.clearCityValue();
|
||||
return null;
|
||||
}
|
||||
return this.loadCityOptions(this.form.provinceCode);
|
||||
})
|
||||
.then(cities => {
|
||||
if (!cities) {
|
||||
return null;
|
||||
}
|
||||
this.form.cityCode = this.resolveCityCode(cityName, adcode);
|
||||
const city = this.cityOptions.find(item => item.id === this.form.cityCode);
|
||||
this.form.city = city ? city.title : cityName;
|
||||
if (!this.form.cityCode) {
|
||||
this.clearDistrictValue();
|
||||
return null;
|
||||
}
|
||||
return this.loadDistrictOptions(this.form.cityCode);
|
||||
})
|
||||
.then(districts => {
|
||||
if (!districts) {
|
||||
return;
|
||||
}
|
||||
this.form.districtCode = this.resolveDistrictCode(districtName, adcode);
|
||||
this.handleDistrictChange(this.form.districtCode);
|
||||
});
|
||||
},
|
||||
applyMapRegionSelection(selection = {}) {
|
||||
if (this.isParentRegionLocked) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||
const hasNameHint = Boolean(
|
||||
selection.country ||
|
||||
selection.countryName ||
|
||||
selection.province ||
|
||||
selection.provinceName ||
|
||||
selection.city ||
|
||||
selection.cityName ||
|
||||
selection.district ||
|
||||
selection.districtName
|
||||
);
|
||||
if (!adcode && !hasNameHint) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const lookupCodes = this.buildRegionLookupCodes(adcode);
|
||||
return this.fetchRegionDetailByCodes(lookupCodes)
|
||||
.then(detail => {
|
||||
if (detail) {
|
||||
return this.applyRegionDetail(detail);
|
||||
}
|
||||
return this.applyMapRegionByNames(selection);
|
||||
})
|
||||
.catch(() => this.applyMapRegionByNames(selection));
|
||||
},
|
||||
resolveCountryCode(country) {
|
||||
if (!country) {
|
||||
@@ -1251,13 +1514,50 @@ export default {
|
||||
this.$message({ type: 'success', message: '操作成功!' });
|
||||
});
|
||||
},
|
||||
createEmptyPortTerminalForm() {
|
||||
return {
|
||||
category: '港口',
|
||||
dataSource: '手动录入',
|
||||
status: 1,
|
||||
parentId: undefined,
|
||||
parentCode: '',
|
||||
parentName: '',
|
||||
name: '',
|
||||
code: '',
|
||||
terminalCode: '',
|
||||
country: '',
|
||||
countryCode: '',
|
||||
provinceCode: undefined,
|
||||
provinceName: '',
|
||||
cityCode: undefined,
|
||||
city: '',
|
||||
districtCode: undefined,
|
||||
districtName: '',
|
||||
regionCode: '',
|
||||
detailAddress: '',
|
||||
longitude: '',
|
||||
latitude: '',
|
||||
remark: '',
|
||||
};
|
||||
},
|
||||
resetAddForm() {
|
||||
// 新增必须整表重置,避免沿用上一次编辑/查看残留
|
||||
this.form = this.createEmptyPortTerminalForm();
|
||||
this.setProvinceOptions([]);
|
||||
this.setCityOptions([]);
|
||||
this.setDistrictOptions([]);
|
||||
},
|
||||
beforeOpen(done, type) {
|
||||
if (type === 'add') {
|
||||
this.form.category = this.form.category || '港口';
|
||||
this.form.dataSource = this.form.dataSource || '手动录入';
|
||||
this.form.status = this.form.status || 1;
|
||||
this.resetAddForm();
|
||||
this.handleCategoryChange('港口', false);
|
||||
done();
|
||||
this.$nextTick(() => {
|
||||
this.$refs.crud?.clearValidate?.();
|
||||
setTimeout(() => this.disableDialogAutocomplete(), 50);
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.handleCategoryChange(this.form.category || '港口');
|
||||
if (['edit', 'view'].includes(type)) {
|
||||
getDetail(this.form.id).then(res => {
|
||||
this.form = res.data.data;
|
||||
@@ -1292,35 +1592,7 @@ export default {
|
||||
setTimeout(() => this.disableDialogAutocomplete(), 50);
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.resolveCountryCode(this.form.country)
|
||||
.then(countryCode => {
|
||||
this.form.countryCode = countryCode;
|
||||
this.syncCountryName(countryCode);
|
||||
return this.loadProvinceOptions(countryCode);
|
||||
})
|
||||
.then(() => {
|
||||
this.form.provinceCode =
|
||||
String(this.form.provinceCode || '') ||
|
||||
this.resolveProvinceCode(this.form.provinceName);
|
||||
return this.loadCityOptions(this.form.provinceCode);
|
||||
})
|
||||
.then(() => {
|
||||
this.form.cityCode = this.resolveCityCode(this.form.city, this.form.regionCode);
|
||||
return this.loadDistrictOptions(this.form.cityCode);
|
||||
})
|
||||
.then(() => {
|
||||
this.form.districtCode = this.resolveDistrictCode(
|
||||
this.form.districtName,
|
||||
this.form.regionCode
|
||||
);
|
||||
if (!this.form.regionCode) {
|
||||
this.handleDistrictChange(this.form.districtCode);
|
||||
}
|
||||
done();
|
||||
setTimeout(() => this.disableDialogAutocomplete(), 50);
|
||||
});
|
||||
},
|
||||
disableDialogAutocomplete() {
|
||||
this.$nextTick(() => {
|
||||
@@ -1365,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;
|
||||
@@ -1443,42 +1715,14 @@ export default {
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* 港口码头编辑/新增弹窗:对齐 railway-station 统一风格(灰底 + 白卡表单 + 浮动底栏,padding 24px 与全局弹窗标准一致) */
|
||||
//.port-terminal-edit-dialog {
|
||||
// /* 灰底内容区 */
|
||||
// .el-dialog__body {
|
||||
// background-color: #f2f2f2;
|
||||
// padding: 8px;
|
||||
// }
|
||||
// /* 表单白卡(与全局 .el-dialog .avue-form 一致,padding 24px 对齐 customer-archive / railway-station) */
|
||||
// .avue-form {
|
||||
// background: #fff;
|
||||
// border-radius: 6px;
|
||||
// box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
// padding: 24px !important;
|
||||
// margin-bottom: 0;
|
||||
// }
|
||||
// /* 底部按钮区白卡(Avue 弹窗 footer 在 body 内,接续白卡) */
|
||||
// .avue-dialog__footer {
|
||||
// background: #fff;
|
||||
// border-radius: 0 0 6px 6px;
|
||||
// box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
// padding: 16px 24px !important;
|
||||
// display: flex;
|
||||
// justify-content: flex-end;
|
||||
// }
|
||||
// /* label 宽度统一 108px(对齐 railway-station,标签对齐更整齐) */
|
||||
// .el-form-item__label {
|
||||
// width: 108px !important;
|
||||
// }
|
||||
// /* 所有 input 文本框、select 下拉控件统一宽度 250px */
|
||||
// .el-input,
|
||||
// .el-select {
|
||||
// width: 250px !important;
|
||||
// }
|
||||
// /* 备注宽度 85% */
|
||||
// .remark-input {
|
||||
// width: 85% !important;
|
||||
// }
|
||||
//}
|
||||
/* 港口码头编辑/新增弹窗:与全局表单项行距保持一致 */
|
||||
.port-terminal-edit-dialog {
|
||||
.el-form-item,
|
||||
.el-form-item--default.el-form-item,
|
||||
.el-form-item--small.el-form-item,
|
||||
.el-form-item--large.el-form-item,
|
||||
.el-form-item--medium.el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -144,12 +144,13 @@
|
||||
|
||||
<script>
|
||||
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/railway-station';
|
||||
import { getLazyTree } from '@/api/base/region';
|
||||
import { getDetail as getRegionDetail, getLazyTree } from '@/api/base/region';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
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';
|
||||
@@ -171,7 +172,12 @@ export default {
|
||||
}
|
||||
};
|
||||
const validateTelegraphCode = (rule, value, callback) => {
|
||||
if (!/^[A-Z]{3}$/.test(String(value || '').toUpperCase())) {
|
||||
const telegraphCode = String(value || '').trim();
|
||||
if (!telegraphCode) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (!/^[A-Z]{3}$/.test(telegraphCode.toUpperCase())) {
|
||||
callback(new Error('电报略码格式不正确或已存在'));
|
||||
} else {
|
||||
callback();
|
||||
@@ -273,10 +279,7 @@ export default {
|
||||
prop: 'telegraphCode',
|
||||
minWidth: 100,
|
||||
maxlength: 3,
|
||||
rules: [
|
||||
{ required: true, message: '电报略码格式不正确或已存在', trigger: 'blur' },
|
||||
{ validator: validateTelegraphCode, trigger: 'blur' },
|
||||
],
|
||||
rules: [{ validator: validateTelegraphCode, trigger: 'blur' }],
|
||||
change: ({ value }) => this.handleTelegraphChange(value),
|
||||
blur: ({ value }) => this.handleTelegraphChange(value),
|
||||
},
|
||||
@@ -293,6 +296,20 @@ export default {
|
||||
// prop: 'railwayLine',
|
||||
// minWidth: 130,
|
||||
// },
|
||||
{
|
||||
label: '详细地址',
|
||||
prop: 'detailAddress',
|
||||
formslot: true,
|
||||
span: 24,
|
||||
minWidth: 220,
|
||||
overHidden: false,
|
||||
maxlength: 255,
|
||||
showWordLimit: true,
|
||||
rules: [
|
||||
{ required: true, message: '请选择详细地址', trigger: 'change' },
|
||||
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '所属省份',
|
||||
prop: 'provinceCode',
|
||||
@@ -360,20 +377,6 @@ export default {
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '详细地址',
|
||||
prop: 'detailAddress',
|
||||
formslot: true,
|
||||
span: 24,
|
||||
minWidth: 220,
|
||||
overHidden: false,
|
||||
maxlength: 255,
|
||||
showWordLimit: true,
|
||||
rules: [
|
||||
{ required: true, message: '请选择详细地址', trigger: 'change' },
|
||||
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '经度',
|
||||
prop: 'longitude',
|
||||
@@ -549,33 +552,51 @@ export default {
|
||||
return targetColumns.find(item => item.prop === targetProp) || null;
|
||||
},
|
||||
loadProvinceOptions() {
|
||||
getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
|
||||
return getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
|
||||
this.provinceOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||
this.findColumn('provinceCode').dicData = this.provinceOptions;
|
||||
const provinceColumn = this.findColumn('provinceCode');
|
||||
if (provinceColumn) {
|
||||
provinceColumn.dicData = this.provinceOptions;
|
||||
}
|
||||
return this.provinceOptions;
|
||||
});
|
||||
},
|
||||
loadCityOptions(provinceCode) {
|
||||
if (!provinceCode) {
|
||||
this.cityOptions = [];
|
||||
this.findColumn('cityCode').dicData = [];
|
||||
return Promise.resolve();
|
||||
const cityColumn = this.findColumn('cityCode');
|
||||
if (cityColumn) {
|
||||
cityColumn.dicData = [];
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return getLazyTree(provinceCode).then(res => {
|
||||
this.cityOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||
this.findColumn('cityCode').dicData = this.cityOptions;
|
||||
const cityColumn = this.findColumn('cityCode');
|
||||
if (cityColumn) {
|
||||
cityColumn.dicData = this.cityOptions;
|
||||
}
|
||||
return this.cityOptions;
|
||||
});
|
||||
},
|
||||
loadDistrictOptions(cityCode) {
|
||||
if (!cityCode) {
|
||||
this.districtOptions = [];
|
||||
this.findColumn('districtCode').dicData = [];
|
||||
return Promise.resolve();
|
||||
const districtColumn = this.findColumn('districtCode');
|
||||
if (districtColumn) {
|
||||
districtColumn.dicData = [];
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
this.districtLoading = true;
|
||||
return getLazyTree(cityCode)
|
||||
.then(res => {
|
||||
this.districtOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||
this.findColumn('districtCode').dicData = this.districtOptions;
|
||||
const districtColumn = this.findColumn('districtCode');
|
||||
if (districtColumn) {
|
||||
districtColumn.dicData = this.districtOptions;
|
||||
}
|
||||
return this.districtOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.districtLoading = false;
|
||||
@@ -681,28 +702,269 @@ export default {
|
||||
openAddressMapPicker() {
|
||||
this.addressMapPickerVisible = true;
|
||||
},
|
||||
formatMapCoordinate(value) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return '';
|
||||
}
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number)) {
|
||||
return '';
|
||||
}
|
||||
return normalizeCoordinateInput(number.toFixed(6));
|
||||
},
|
||||
patchDialogForm(patch = {}) {
|
||||
Object.assign(this.form, patch);
|
||||
const tableForm = this.$refs.crud?.tableForm;
|
||||
if (tableForm && tableForm !== this.form) {
|
||||
Object.assign(tableForm, patch);
|
||||
}
|
||||
},
|
||||
handleAddressMapConfirm(payload) {
|
||||
const selection = typeof payload === 'string' ? { address: payload } : payload || {};
|
||||
const longitude = this.formatMapCoordinate(selection.longitude);
|
||||
const latitude = this.formatMapCoordinate(selection.latitude);
|
||||
const patch = {};
|
||||
if (selection.address) {
|
||||
this.form.detailAddress = selection.address;
|
||||
patch.detailAddress = selection.address;
|
||||
}
|
||||
if (longitude !== '' && latitude !== '') {
|
||||
patch.longitude = longitude;
|
||||
patch.latitude = latitude;
|
||||
}
|
||||
if (Object.keys(patch).length) {
|
||||
this.patchDialogForm(patch);
|
||||
}
|
||||
this.applyMapRegionSelection(selection);
|
||||
},
|
||||
normalizeAmapCityName(city, province) {
|
||||
if (Array.isArray(city)) {
|
||||
return String(province || '').trim();
|
||||
}
|
||||
const cityName = String(city || '').trim();
|
||||
return cityName || String(province || '').trim();
|
||||
},
|
||||
buildRegionLookupCodes(adcode) {
|
||||
const code = String(adcode || '').trim();
|
||||
if (!code) {
|
||||
return [];
|
||||
}
|
||||
const codes = [code];
|
||||
if (/^\d{6}$/.test(code)) {
|
||||
if (code.endsWith('00')) {
|
||||
const cityCode = code.slice(0, 4);
|
||||
if (cityCode && !codes.includes(cityCode)) {
|
||||
codes.push(cityCode);
|
||||
}
|
||||
}
|
||||
if (code.endsWith('0000')) {
|
||||
const provinceCode = code.slice(0, 2);
|
||||
if (provinceCode && !codes.includes(provinceCode)) {
|
||||
codes.push(provinceCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
return codes;
|
||||
},
|
||||
fetchRegionDetailByCodes(codes = []) {
|
||||
const tryNext = index => {
|
||||
if (index >= codes.length) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return getRegionDetail(codes[index])
|
||||
.then(res => {
|
||||
const detail = res.data && res.data.data;
|
||||
if (detail && detail.code) {
|
||||
return detail;
|
||||
}
|
||||
return tryNext(index + 1);
|
||||
})
|
||||
.catch(() => tryNext(index + 1));
|
||||
};
|
||||
return tryNext(0);
|
||||
},
|
||||
resolveProvinceCode(provinceName) {
|
||||
const name = String(provinceName || '').trim();
|
||||
if (!name) {
|
||||
return '';
|
||||
}
|
||||
const province = this.provinceOptions.find(
|
||||
item => item.title === name || item.id === name || String(item.title).includes(name)
|
||||
);
|
||||
return province ? province.id : '';
|
||||
},
|
||||
resolveCityCode(cityName, regionCode) {
|
||||
const name = String(cityName || '').trim();
|
||||
const code = String(regionCode || '').trim();
|
||||
const city = this.cityOptions.find(
|
||||
item => item.title === name || item.id === code || item.id === name
|
||||
);
|
||||
return city ? city.id : '';
|
||||
},
|
||||
resolveDistrictCode(districtName, regionCode) {
|
||||
const name = String(districtName || '').trim();
|
||||
const code = String(regionCode || '').trim();
|
||||
const district = this.districtOptions.find(
|
||||
item => item.title === name || item.id === code || item.id === name
|
||||
);
|
||||
return district ? district.id : code || '';
|
||||
},
|
||||
applyRegionDetail(detail = {}) {
|
||||
const provinceName = detail.provinceName || '';
|
||||
const cityName = detail.cityName || '';
|
||||
const districtName = detail.districtName || detail.name || '';
|
||||
return this.loadProvinceOptions()
|
||||
.then(() => {
|
||||
const provinceCode =
|
||||
String(detail.provinceCode || '').trim() || this.resolveProvinceCode(provinceName);
|
||||
const province = this.provinceOptions.find(item => item.id === provinceCode);
|
||||
this.patchDialogForm({
|
||||
provinceCode: provinceCode || undefined,
|
||||
provinceName: province ? province.title : provinceName,
|
||||
});
|
||||
if (!provinceCode) {
|
||||
return null;
|
||||
}
|
||||
return this.loadCityOptions(provinceCode);
|
||||
})
|
||||
.then(cities => {
|
||||
if (!cities) {
|
||||
return null;
|
||||
}
|
||||
const cityCode =
|
||||
String(detail.cityCode || '').trim() ||
|
||||
this.resolveCityCode(cityName, detail.districtCode || detail.code);
|
||||
const city = this.cityOptions.find(item => item.id === cityCode);
|
||||
this.patchDialogForm({
|
||||
cityCode: cityCode || undefined,
|
||||
cityName: city ? city.title : cityName,
|
||||
});
|
||||
this.ensureCityOption({
|
||||
cityCode,
|
||||
cityName: city ? city.title : cityName,
|
||||
});
|
||||
if (!cityCode) {
|
||||
return null;
|
||||
}
|
||||
return this.loadDistrictOptions(cityCode);
|
||||
})
|
||||
.then(districts => {
|
||||
if (!districts) {
|
||||
return;
|
||||
}
|
||||
const districtCode = this.resolveDistrictCode(
|
||||
districtName,
|
||||
detail.districtCode || detail.code
|
||||
);
|
||||
this.patchDialogForm({
|
||||
districtCode: districtCode || undefined,
|
||||
});
|
||||
this.handleDistrictChange(districtCode);
|
||||
this.ensureDistrictOption({
|
||||
districtCode,
|
||||
districtName: this.form.districtName || districtName,
|
||||
});
|
||||
});
|
||||
},
|
||||
applyMapRegionByNames(selection = {}) {
|
||||
const provinceName = String(selection.province || selection.provinceName || '').trim();
|
||||
const cityName = this.normalizeAmapCityName(
|
||||
selection.city || selection.cityName,
|
||||
provinceName
|
||||
);
|
||||
const districtName = String(selection.district || selection.districtName || '').trim();
|
||||
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||
// 本页无国家字段:国内 adcode 走中国省份树;无匹配则不强制回填
|
||||
if (
|
||||
selection.longitude !== undefined &&
|
||||
selection.longitude !== null &&
|
||||
selection.longitude !== ''
|
||||
!provinceName &&
|
||||
!cityName &&
|
||||
!districtName &&
|
||||
!(/^\d{6}$/.test(adcode) || String(selection.country || selection.countryName || '').includes('中国'))
|
||||
) {
|
||||
this.form.longitude = Number(selection.longitude).toFixed(6);
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (
|
||||
selection.latitude !== undefined &&
|
||||
selection.latitude !== null &&
|
||||
selection.latitude !== ''
|
||||
) {
|
||||
this.form.latitude = Number(selection.latitude).toFixed(6);
|
||||
return this.loadProvinceOptions()
|
||||
.then(() => {
|
||||
const provinceCode = this.resolveProvinceCode(provinceName);
|
||||
const province = this.provinceOptions.find(item => item.id === provinceCode);
|
||||
this.patchDialogForm({
|
||||
provinceCode: provinceCode || undefined,
|
||||
provinceName: province ? province.title : provinceName,
|
||||
});
|
||||
if (!provinceCode) {
|
||||
return null;
|
||||
}
|
||||
return this.loadCityOptions(provinceCode);
|
||||
})
|
||||
.then(cities => {
|
||||
if (!cities) {
|
||||
return null;
|
||||
}
|
||||
const cityCode = this.resolveCityCode(cityName, adcode);
|
||||
const city = this.cityOptions.find(item => item.id === cityCode);
|
||||
this.patchDialogForm({
|
||||
cityCode: cityCode || undefined,
|
||||
cityName: city ? city.title : cityName,
|
||||
});
|
||||
this.ensureCityOption({
|
||||
cityCode,
|
||||
cityName: city ? city.title : cityName,
|
||||
});
|
||||
if (!cityCode) {
|
||||
return null;
|
||||
}
|
||||
return this.loadDistrictOptions(cityCode);
|
||||
})
|
||||
.then(districts => {
|
||||
if (!districts) {
|
||||
return;
|
||||
}
|
||||
const districtCode = this.resolveDistrictCode(districtName, adcode);
|
||||
this.patchDialogForm({
|
||||
districtCode: districtCode || undefined,
|
||||
});
|
||||
this.handleDistrictChange(districtCode);
|
||||
this.ensureDistrictOption({
|
||||
districtCode,
|
||||
districtName: this.form.districtName || districtName,
|
||||
});
|
||||
});
|
||||
},
|
||||
applyMapRegionSelection(selection = {}) {
|
||||
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||
const hasNameHint = Boolean(
|
||||
selection.country ||
|
||||
selection.countryName ||
|
||||
selection.province ||
|
||||
selection.provinceName ||
|
||||
selection.city ||
|
||||
selection.cityName ||
|
||||
selection.district ||
|
||||
selection.districtName
|
||||
);
|
||||
if (!adcode && !hasNameHint) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.regionInitializing = true;
|
||||
const lookupCodes = this.buildRegionLookupCodes(adcode);
|
||||
return this.fetchRegionDetailByCodes(lookupCodes)
|
||||
.then(detail => {
|
||||
if (detail) {
|
||||
return this.applyRegionDetail(detail);
|
||||
}
|
||||
return this.applyMapRegionByNames(selection);
|
||||
})
|
||||
.catch(() => this.applyMapRegionByNames(selection))
|
||||
.finally(() => {
|
||||
this.$nextTick(() => {
|
||||
window.setTimeout(() => {
|
||||
this.regionInitializing = false;
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
},
|
||||
normalizeRow(row) {
|
||||
row.code = row.tmisCode ? `TL${row.tmisCode}` : row.code;
|
||||
row.telegraphCode = String(row.telegraphCode || '').trim() || undefined;
|
||||
row.regionCode = String(row.regionCode || '').trim();
|
||||
row.regionName = String(row.regionName || '').trim();
|
||||
row.districtCode = String(row.districtCode || '').trim();
|
||||
@@ -735,14 +997,15 @@ export default {
|
||||
return records.some(item => item[prop] === row[prop] && String(item.id || '') !== rowId);
|
||||
});
|
||||
};
|
||||
return Promise.all([
|
||||
hasDuplicate({ tmisCode: row.tmisCode }, 'tmisCode'),
|
||||
hasDuplicate({ telegraphCode: row.telegraphCode }, 'telegraphCode'),
|
||||
]).then(([tmisExists, telegraphExists]) => {
|
||||
if (tmisExists) {
|
||||
const uniqueChecks = [hasDuplicate({ tmisCode: row.tmisCode }, 'tmisCode')];
|
||||
if (row.telegraphCode) {
|
||||
uniqueChecks.push(hasDuplicate({ telegraphCode: row.telegraphCode }, 'telegraphCode'));
|
||||
}
|
||||
return Promise.all(uniqueChecks).then(results => {
|
||||
if (results[0]) {
|
||||
return Promise.reject(new Error('该TMIS编码已存在'));
|
||||
}
|
||||
if (telegraphExists) {
|
||||
if (results[1]) {
|
||||
return Promise.reject(new Error('电报略码格式不正确或已存在'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
@@ -895,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: 9px !important;
|
||||
margin-bottom: 22px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -352,7 +352,8 @@ export default {
|
||||
return result;
|
||||
},
|
||||
normalizeListQuery(params = {}) {
|
||||
const query = { ...params, ...this.query };
|
||||
// 搜索条件优先于缓存的 query,避免默认组织 ID 覆盖用户新选择的组织。
|
||||
const query = { ...this.query, ...params };
|
||||
const rawDeptId = query.deptId ?? query.searchDeptId;
|
||||
delete query.searchDeptId;
|
||||
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
|
||||
|
||||
@@ -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: {
|
||||
@@ -415,19 +418,27 @@ export default {
|
||||
});
|
||||
},
|
||||
buildDeptTree(tree) {
|
||||
return tree.map(item => ({
|
||||
label: item.title || item.deptName || item.name,
|
||||
value: item.id,
|
||||
children:
|
||||
item.children && item.children.length ? this.buildDeptTree(item.children) : undefined,
|
||||
}));
|
||||
return tree.map(item => {
|
||||
// 雪花 ID 必须保持字符串,避免 JS Number 精度丢失导致筛错组织。
|
||||
const id =
|
||||
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
|
||||
return {
|
||||
label: item.title || item.deptName || item.name,
|
||||
value: id,
|
||||
children:
|
||||
item.children && item.children.length ? this.buildDeptTree(item.children) : undefined,
|
||||
};
|
||||
}).filter(item => item.value);
|
||||
},
|
||||
flattenDept(tree, level = 0) {
|
||||
const result = [];
|
||||
tree.forEach(item => {
|
||||
const id =
|
||||
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
|
||||
if (!id) return;
|
||||
result.push({
|
||||
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
||||
value: item.id,
|
||||
value: id,
|
||||
});
|
||||
if (item.children && item.children.length) {
|
||||
result.push(...this.flattenDept(item.children, level + 1));
|
||||
@@ -436,9 +447,58 @@ export default {
|
||||
return result;
|
||||
},
|
||||
defaultDeptId() {
|
||||
return (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
|
||||
const raw = (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
|
||||
return (
|
||||
String(raw)
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)[0] || ''
|
||||
);
|
||||
},
|
||||
normalizeListQuery(params = {}) {
|
||||
// 搜索条件优先于缓存的 query,避免默认组织 ID 覆盖用户新选择的组织。
|
||||
const query = { ...this.query, ...params };
|
||||
// 本次搜索显式带了组织字段时,以本次为准(含清空);否则沿用缓存。
|
||||
const 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() !== '') {
|
||||
// 级联可能回传路径数组,统一取末级并保持字符串,避免雪花 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;
|
||||
}
|
||||
query.allDept = this.isAdmin ? 1 : 0;
|
||||
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;
|
||||
@@ -450,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: '',
|
||||
@@ -906,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();
|
||||
},
|
||||
@@ -924,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();
|
||||
},
|
||||
@@ -979,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;
|
||||
}
|
||||
@@ -999,9 +1071,9 @@ export default {
|
||||
this.onLoad(this.page, this.query);
|
||||
},
|
||||
searchChange(params, done) {
|
||||
this.query = params;
|
||||
this.query = this.normalizeListQuery(params);
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page, params);
|
||||
this.onLoad(this.page, this.query);
|
||||
done();
|
||||
},
|
||||
selectionChange(list) {
|
||||
@@ -1024,12 +1096,10 @@ export default {
|
||||
},
|
||||
onLoad(page, params = {}) {
|
||||
this.loading = true;
|
||||
const query = this.normalizeListQuery(params);
|
||||
this.query = query;
|
||||
api
|
||||
.getList(page.currentPage, page.pageSize, {
|
||||
...params,
|
||||
...this.query,
|
||||
allDept: this.isAdmin ? 1 : 0,
|
||||
})
|
||||
.getList(page.currentPage, page.pageSize, query)
|
||||
.then(res => {
|
||||
const result = res.data.data;
|
||||
this.page.total = result.total;
|
||||
@@ -1042,8 +1112,7 @@ export default {
|
||||
},
|
||||
buildExportParams() {
|
||||
return {
|
||||
...this.query,
|
||||
allDept: this.isAdmin ? 1 : 0,
|
||||
...this.normalizeListQuery(this.query),
|
||||
ids: this.ids,
|
||||
[this.website.tokenHeader]: getToken(),
|
||||
};
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
:label="item.label"
|
||||
:value="item.value" /></el-select></template
|
||||
></el-table-column>
|
||||
<el-table-column label="单价(元)" width="180"
|
||||
<el-table-column :label="unitPriceColumnLabel" width="180"
|
||||
><template #default="{ row }"
|
||||
><div v-if="readonly && usesRangeUnitPrice(row)" class="limit-list">
|
||||
<span v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex">{{
|
||||
@@ -379,6 +379,7 @@ import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import SectionCard from '@/components/section-card/main.vue';
|
||||
import { currencyUnit } from '@/utils/currency';
|
||||
|
||||
const clone = value => JSON.parse(JSON.stringify(value));
|
||||
/** 计费要素与计量单位维度的对应关系(这些要素可选计费单位) */
|
||||
@@ -386,12 +387,15 @@ const BILLING_ELEMENT_DIMENSION_MAP = {
|
||||
按重量: '重量',
|
||||
按体积: '体积',
|
||||
按数量: '数量',
|
||||
按里程: '里程',
|
||||
'按吨·公里': '吨·公里',
|
||||
'固定金额(整单一口价)': '整车',
|
||||
};
|
||||
const BILLING_UNIT_SELECTABLE_ELEMENTS = Object.keys(BILLING_ELEMENT_DIMENSION_MAP);
|
||||
/** 固定计费单位(不可编辑) */
|
||||
const FIXED_BILLING_UNIT_MAP = {
|
||||
/** 对应维度下无计量单位时的默认计费单位 */
|
||||
const DEFAULT_BILLING_UNIT_MAP = {
|
||||
按里程: '公里',
|
||||
'按吨·公里': '吨公里',
|
||||
'按吨·公里': '吨·公里',
|
||||
'固定金额(整单一口价)': '单',
|
||||
};
|
||||
const defaultRule = () => ({
|
||||
@@ -428,6 +432,7 @@ export default {
|
||||
value: { type: Object, default: () => ({}) },
|
||||
index: { type: Number, default: -1 },
|
||||
readonly: Boolean,
|
||||
settlementCurrency: { type: String, default: 'RMB' },
|
||||
},
|
||||
emits: ['update:modelValue', 'save'],
|
||||
data() {
|
||||
@@ -497,6 +502,9 @@ export default {
|
||||
emitPath: true,
|
||||
};
|
||||
},
|
||||
unitPriceColumnLabel() {
|
||||
return `单价(${currencyUnit(this.settlementCurrency)})`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadDictionaries();
|
||||
@@ -652,58 +660,61 @@ export default {
|
||||
}),
|
||||
]).finally(() => {
|
||||
this.unitLoading = false;
|
||||
(this.draft.rules || []).forEach(row => this.syncBillingUnit(row));
|
||||
});
|
||||
},
|
||||
measurementDimension(billingElement) {
|
||||
return BILLING_ELEMENT_DIMENSION_MAP[billingElement] || '';
|
||||
},
|
||||
fixedBillingUnit(billingElement) {
|
||||
return FIXED_BILLING_UNIT_MAP[String(billingElement || '').trim()] || '';
|
||||
defaultBillingUnit(billingElement) {
|
||||
return DEFAULT_BILLING_UNIT_MAP[String(billingElement || '').trim()] || '';
|
||||
},
|
||||
canSelectBillingUnit(row) {
|
||||
return BILLING_UNIT_SELECTABLE_ELEMENTS.includes(String(row?.billingElement || '').trim());
|
||||
},
|
||||
billingUnitPlaceholder(row) {
|
||||
if (!row?.billingElement) return '请先选择计费要素';
|
||||
if (this.fixedBillingUnit(row.billingElement)) return this.fixedBillingUnit(row.billingElement);
|
||||
if (!this.canSelectBillingUnit(row)) return '当前计费要素无需选择';
|
||||
return '请选择';
|
||||
},
|
||||
unitOptionsFor(row) {
|
||||
const fixedUnit = this.fixedBillingUnit(row?.billingElement);
|
||||
if (fixedUnit) {
|
||||
return [{ id: fixedUnit, label: fixedUnit, value: fixedUnit }];
|
||||
}
|
||||
if (!this.canSelectBillingUnit(row)) {
|
||||
return [];
|
||||
}
|
||||
const dimension = this.measurementDimension(row?.billingElement);
|
||||
if (dimension) {
|
||||
return this.measurementUnits
|
||||
.filter(item => String(item.dimension || '').trim() === dimension)
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
label: item.unitName,
|
||||
value: item.unitName,
|
||||
}))
|
||||
.filter(item => item.value);
|
||||
const defaultUnit = this.defaultBillingUnit(row?.billingElement);
|
||||
const options = dimension
|
||||
? this.measurementUnits
|
||||
.filter(item => String(item.dimension || '').trim() === dimension)
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
label: item.unitName,
|
||||
value: item.unitName,
|
||||
}))
|
||||
.filter(item => item.value)
|
||||
: [];
|
||||
// 对应维度未配置计量单位时,下拉仅展示默认值
|
||||
if (!options.length && defaultUnit) {
|
||||
return [{ id: `default-${defaultUnit}`, label: defaultUnit, value: defaultUnit }];
|
||||
}
|
||||
return [];
|
||||
return options;
|
||||
},
|
||||
syncBillingUnit(row) {
|
||||
const fixedUnit = this.fixedBillingUnit(row?.billingElement);
|
||||
if (fixedUnit) {
|
||||
row.billingUnit = fixedUnit;
|
||||
return;
|
||||
}
|
||||
if (!this.canSelectBillingUnit(row)) {
|
||||
row.billingUnit = '';
|
||||
return;
|
||||
}
|
||||
const options = this.unitOptionsFor(row);
|
||||
if (!options.some(item => String(item.value) === String(row.billingUnit || ''))) {
|
||||
row.billingUnit = '';
|
||||
const defaultUnit = this.defaultBillingUnit(row?.billingElement);
|
||||
const current = String(row.billingUnit || '').trim();
|
||||
if (options.some(item => String(item.value) === current)) {
|
||||
return;
|
||||
}
|
||||
if (defaultUnit && options.some(item => String(item.value) === defaultUnit)) {
|
||||
row.billingUnit = defaultUnit;
|
||||
return;
|
||||
}
|
||||
row.billingUnit = options[0]?.value || defaultUnit || '';
|
||||
},
|
||||
feeTypeKey(row) {
|
||||
const option = this.feeCategories.find(
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
<el-option
|
||||
v-for="item in contractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -2065,11 +2065,22 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="200" align="left" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="200"
|
||||
align="left"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ row.originalName || row.name }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ row.originalName || row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220" align="center">
|
||||
@@ -2338,14 +2349,16 @@
|
||||
<div class="business-crud-page__transport-plan-detail-field is-attachments">
|
||||
<span>附件</span>
|
||||
<div class="business-crud-page__transport-plan-detail-attachments">
|
||||
<el-link
|
||||
<span
|
||||
v-for="file in attachmentRows"
|
||||
:key="file.url || file.name || file.originalName"
|
||||
type="primary"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(file)"
|
||||
>
|
||||
{{ attachmentName(file) }}
|
||||
</el-link>
|
||||
@keydown.enter="previewAttachment(file)"
|
||||
@keydown.space.prevent="previewAttachment(file)"
|
||||
>{{ attachmentName(file) }}</span>
|
||||
<span v-if="!attachmentRows.length">-</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2531,14 +2544,16 @@
|
||||
<div class="business-crud-page__waybill-attachments">
|
||||
<span class="business-crud-page__waybill-label">附件</span>
|
||||
<div>
|
||||
<el-link
|
||||
<span
|
||||
v-for="file in attachmentRows"
|
||||
:key="file.url || file.name || file.originalName"
|
||||
type="primary"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(file)"
|
||||
>
|
||||
{{ file.originalName || file.name || '-' }}
|
||||
</el-link>
|
||||
@keydown.enter="previewAttachment(file)"
|
||||
@keydown.space.prevent="previewAttachment(file)"
|
||||
>{{ file.originalName || file.name || '-' }}</span>
|
||||
<span v-if="!attachmentRows.length">-</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2820,11 +2835,21 @@
|
||||
</div>
|
||||
<el-table :data="attachmentRows" empty-text="暂无附件">
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="260">
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="260"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -2966,10 +2991,50 @@
|
||||
<el-table :data="waybillRouteChangeRecords" border>
|
||||
<el-table-column prop="changeTime" label="变更时间" min-width="170" />
|
||||
<el-table-column prop="changeUser" label="变更人" min-width="120" />
|
||||
<el-table-column prop="content" label="变更内容" min-width="360">
|
||||
<el-table-column
|
||||
prop="content"
|
||||
label="变更内容"
|
||||
min-width="360"
|
||||
:show-overflow-tooltip="false"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="business-crud-page__route-change-record-content">
|
||||
{{ row.content || '-' }}
|
||||
<div
|
||||
v-for="(group, groupIndex) in parseRouteChangeContent(row.content)"
|
||||
:key="groupIndex"
|
||||
class="business-crud-page__route-change-group"
|
||||
>
|
||||
<div v-if="group.title" class="business-crud-page__route-change-group-title">
|
||||
{{ group.title }}
|
||||
</div>
|
||||
<div v-if="group.text" class="business-crud-page__route-change-group-text">
|
||||
{{ group.text }}
|
||||
</div>
|
||||
<div
|
||||
v-for="(field, fieldIndex) in group.fields"
|
||||
:key="fieldIndex"
|
||||
class="business-crud-page__route-change-field"
|
||||
>
|
||||
<span class="business-crud-page__route-change-field-label">
|
||||
{{ field.label }}
|
||||
</span>
|
||||
<template v-if="field.changed">
|
||||
<span class="business-crud-page__route-change-before">
|
||||
{{ field.before }}
|
||||
</span>
|
||||
<span class="business-crud-page__route-change-arrow">→</span>
|
||||
<span class="business-crud-page__route-change-after">
|
||||
{{ field.after }}
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="business-crud-page__route-change-single">
|
||||
{{ field.single }}
|
||||
<span v-if="field.unchanged" class="business-crud-page__route-change-flag">
|
||||
未变更
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -3056,7 +3121,7 @@
|
||||
<el-option
|
||||
v-for="item in transportPlanImportContractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -3709,7 +3774,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="剩余数量" min-width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDispatchQuantity(dispatchItemRemainingQuantity(row)) }}
|
||||
{{ formatDispatchQuantity(dispatchItemDisplayRemainingQuantity(row)) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column min-width="130" align="center">
|
||||
@@ -3966,7 +4031,7 @@
|
||||
</el-input>
|
||||
<div class="business-crud-page__dispatch-quantity-hint">
|
||||
剩余数量:{{
|
||||
formatDispatchQuantity(dispatchItemHintRemainingQuantity(dispatchItemForm))
|
||||
formatDispatchQuantity(dispatchItemDisplayRemainingQuantity(dispatchItemForm))
|
||||
}}
|
||||
{{ dispatchItemForm.quantityUnit || '吨' }}
|
||||
</div>
|
||||
@@ -4225,11 +4290,22 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="200" align="center" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="200"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row, dispatchItemAttachmentRows)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
@keydown.enter="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
@keydown.space.prevent="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220" align="center">
|
||||
@@ -5242,8 +5318,11 @@ import { addressTypeOptions } from '@/option/base/common-address';
|
||||
import { packageOptions } from '@/option/business/common';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||
import { parseRouteChangeContent } from '@/utils/route-change';
|
||||
import { applyTableMenuWidth } from '@/utils/table-menu';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
@@ -5661,6 +5740,8 @@ export default {
|
||||
dispatchItemIndex: -1,
|
||||
dispatchItemForm: defaultDispatchRow(),
|
||||
dispatchItemCargoRows: [],
|
||||
// 本次数量变更版本号:驱动同表其他行「剩余数量」同步重算
|
||||
dispatchCargoQuantityVersion: 0,
|
||||
dispatchItemAttachmentRows: [],
|
||||
selectedDispatchItemAttachmentRows: [],
|
||||
attachmentRows: [],
|
||||
@@ -5818,11 +5899,11 @@ export default {
|
||||
'固定金额(整单一口价)': ['固定一口价'],
|
||||
按数量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
|
||||
},
|
||||
settlementTypeOptions: ['月结', '日结', '周结', '半月结', '固定天数周期结算'],
|
||||
settlementTypeOptions: ['月结', '日结', '周结', '固定天数周期结算'],
|
||||
billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'],
|
||||
reconciliationModeOptions: ['明细逐行核对', '按账单汇总核对', '跳过自动核对'],
|
||||
packageOptions,
|
||||
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
|
||||
quantityUnitOptions: [],
|
||||
attachmentFileTypes: [
|
||||
'pdf',
|
||||
'bmp',
|
||||
@@ -5865,7 +5946,6 @@ export default {
|
||||
this.ensureBillingUnitOptions();
|
||||
}
|
||||
if (this.config.enableTaskInfoForm) {
|
||||
this.loadTaskQuantityUnitOptions();
|
||||
this.loadTaskFeeUnitOptions();
|
||||
this.ensureBillingCargoTypeOptions();
|
||||
}
|
||||
@@ -5886,6 +5966,7 @@ export default {
|
||||
} else {
|
||||
this.consumeTemplateCreatePayload();
|
||||
}
|
||||
this.loadTaskQuantityUnitOptions();
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
@@ -6589,6 +6670,8 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
parseRouteChangeContent,
|
||||
initStandaloneFormPage() {
|
||||
if (!this.isStandaloneFormPage) return;
|
||||
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
||||
@@ -7789,8 +7872,6 @@ export default {
|
||||
['contractName', '客户合同'],
|
||||
['planName', '计划名称'],
|
||||
['transportType', '运输方式'],
|
||||
['planStartDate', '计划开始日期'],
|
||||
['planEndDate', '计划结束日期'],
|
||||
['departureAddress', '发货地址'],
|
||||
['arrivalAddress', '收货地址'],
|
||||
];
|
||||
@@ -9270,9 +9351,10 @@ export default {
|
||||
return Promise.resolve(this.taskQuantityUnitOptions);
|
||||
}
|
||||
this.taskQuantityUnitLoading = true;
|
||||
return getDictionary({ code: 'quantity_unit' })
|
||||
.then(res => {
|
||||
this.taskQuantityUnitOptions = this.normalizeDictOptions(res.data.data || []);
|
||||
return loadMeasurementUnitOptions()
|
||||
.then(options => {
|
||||
this.taskQuantityUnitOptions = options;
|
||||
this.quantityUnitOptions = options.map(item => item.value);
|
||||
return this.taskQuantityUnitOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -12696,18 +12778,23 @@ export default {
|
||||
row.cargoName = '';
|
||||
row.specification = '';
|
||||
row.model = '';
|
||||
this.dispatchCargoQuantityVersion += 1;
|
||||
},
|
||||
handleDispatchConfiguredCargoNameChange(row, value) {
|
||||
const option = this.getDispatchConfiguredCargoNameOptions(row).find(
|
||||
item => String(item.value) === String(value || '')
|
||||
);
|
||||
row.cargoName = option?.value || '';
|
||||
if (!option) return;
|
||||
if (!option) {
|
||||
this.dispatchCargoQuantityVersion += 1;
|
||||
return;
|
||||
}
|
||||
row.cargoType = option.cargoTypeLabel || row.cargoType || '';
|
||||
row.cargoTypeCode = option.cargoTypeCode || row.cargoTypeCode || '';
|
||||
row.cargoTypePath = option.cargoTypePath || row.cargoTypePath || [];
|
||||
row.specification = option.specification || row.specification || '';
|
||||
row.model = option.model || row.model || '';
|
||||
this.dispatchCargoQuantityVersion += 1;
|
||||
},
|
||||
openDispatchItemDialog(index = -1, row = defaultDispatchRow()) {
|
||||
const defaultSummaryItem =
|
||||
@@ -13039,6 +13126,7 @@ export default {
|
||||
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.handleTaskFullFreightNumberInput(row, prop, value);
|
||||
@@ -13058,12 +13146,14 @@ export default {
|
||||
const nextRow = this.normalizeTransportCargoRow({ priceUnit: '元/吨' });
|
||||
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 = {}) {
|
||||
const quantity = Number(cargo.quantity || 0);
|
||||
@@ -13137,11 +13227,52 @@ 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();
|
||||
@@ -14111,7 +14242,7 @@ export default {
|
||||
|
||||
&__dispatch-shipping-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14634,7 +14765,7 @@ export default {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-select),
|
||||
@@ -15859,7 +15990,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;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
@@ -16251,7 +16440,7 @@ export default {
|
||||
}
|
||||
|
||||
:global(.business-crud-page__mileage-dialog .business-crud-page__mileage-form .el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:global(.business-crud-page__mileage-dialog .business-crud-page__mileage-form .el-input),
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
class="contract-manage-form__attachment-table"
|
||||
@selection-change="selected = $event"
|
||||
>
|
||||
<el-table-column v-if="!readonly" type="selection" width="55" align="center" />
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column v-if="attachmentType" label="附件类型" min-width="160" align="center">
|
||||
<template #default="{ row, $index }">
|
||||
@@ -45,11 +45,17 @@
|
||||
align="left"
|
||||
header-align="left"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="preview?.(row, rows)">{{
|
||||
attachmentName(row)
|
||||
}}</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="preview?.(row, rows)"
|
||||
@keydown.enter="preview?.(row, rows)"
|
||||
@keydown.space.prevent="preview?.(row, rows)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="description" label="附件描述" min-width="220">
|
||||
@@ -173,13 +179,43 @@ import { downloadFileByUrl } from '@/utils/util';
|
||||
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||||
|
||||
const CONTRACT_ATTACHMENT_TYPE_OTHER = '其他文件';
|
||||
/** 用印材料历史取值兼容别名 */
|
||||
export const CONTRACT_SEAL_MATERIAL_LOCATION = '用印材料';
|
||||
export const CONTRACT_SEAL_MATERIAL_TYPE_ALIASES = {
|
||||
合同文件: CONTRACT_SEAL_MATERIAL_LOCATION,
|
||||
};
|
||||
/** 佐证材料附件位置(历史:其它附件) */
|
||||
export const CONTRACT_SUPPORTING_ATTACHMENT_LOCATION = '附件(佐证材料等)';
|
||||
export const CONTRACT_ATTACHMENT_TYPE_DICT = 'contract_attachment_type';
|
||||
export const CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT = 'contract_supporting_attachment_type';
|
||||
export const isContractSupportingAttachmentLocation = location => {
|
||||
const text = String(location || '');
|
||||
return (
|
||||
text === CONTRACT_SUPPORTING_ATTACHMENT_LOCATION ||
|
||||
text === '其它附件' ||
|
||||
text === '其他附件'
|
||||
);
|
||||
};
|
||||
export const resolveAttachmentTypeDictCode = (location, fallback = CONTRACT_ATTACHMENT_TYPE_DICT) =>
|
||||
isContractSupportingAttachmentLocation(location)
|
||||
? CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT
|
||||
: fallback || CONTRACT_ATTACHMENT_TYPE_DICT;
|
||||
export const defaultContractAttachmentTypeOptions = [
|
||||
{ label: '合同文件', value: '合同文件' },
|
||||
{ label: '用印材料', value: '用印材料' },
|
||||
{ label: '双章归档文件', value: '双章归档文件' },
|
||||
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
|
||||
];
|
||||
/** @deprecated 兼容旧引用,实际选项由业务字典 contract_attachment_type 动态加载 */
|
||||
// 特批附件归口附件(佐证材料等)区上传:高风险/黑名单客商合同必传,后端按 fileType=特批附件 校验并挂特批面板(与后端 HcsSyncConstant.ATTACHMENT_TYPE_SPEC_EXPLANATION 对齐)
|
||||
export const defaultContractSupportingAttachmentTypeOptions = [
|
||||
{ label: '特批附件', value: '特批附件' },
|
||||
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
|
||||
];
|
||||
/** @deprecated 兼容旧引用,实际选项由业务字典动态加载 */
|
||||
export const contractAttachmentTypeOptions = defaultContractAttachmentTypeOptions;
|
||||
export const isContractSealMaterialLocation = location => {
|
||||
const text = String(location || '');
|
||||
return text === CONTRACT_SEAL_MATERIAL_LOCATION || text === '合同文件';
|
||||
};
|
||||
export const attachmentFileTypes = [
|
||||
'pdf',
|
||||
'bmp',
|
||||
@@ -234,8 +270,11 @@ const isOtherAttachmentTypeOption = item => {
|
||||
const attachmentTypeKeywords = fileType => {
|
||||
const text = String(fileType || '');
|
||||
if (text.includes('双章')) return ['双章归档文件', '双章归档', '双章'];
|
||||
if (text.includes('合同') && !text.includes('其他') && !text.includes('其它')) {
|
||||
return ['合同文件', '合同'];
|
||||
if (
|
||||
text.includes('用印') ||
|
||||
(text.includes('合同') && !text.includes('其他') && !text.includes('其它'))
|
||||
) {
|
||||
return ['用印材料', '合同文件', '用印', '合同'];
|
||||
}
|
||||
if (isOtherAttachmentTypeOption({ label: text, value: text })) return [];
|
||||
return text ? [text] : [];
|
||||
@@ -254,9 +293,16 @@ export const resolveContractAttachmentType = (
|
||||
const list = options.length ? options : defaultContractAttachmentTypeOptions;
|
||||
const otherType = resolveOtherAttachmentType(list);
|
||||
const values = list.map(item => item.value);
|
||||
if (values.includes(row.fileType)) return row.fileType;
|
||||
const matchedByLabel = list.find(item => String(item.label) === String(row.fileType));
|
||||
const rawFileType = String(row.fileType || '');
|
||||
const aliasedFileType = CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawFileType] || rawFileType;
|
||||
if (values.includes(aliasedFileType)) return aliasedFileType;
|
||||
if (values.includes(rawFileType)) return rawFileType;
|
||||
const matchedByLabel = list.find(
|
||||
item => String(item.label) === aliasedFileType || String(item.label) === rawFileType
|
||||
);
|
||||
if (matchedByLabel) return matchedByLabel.value;
|
||||
// 行已带类型但字典未命中(如历史保存值、字典缺失)时保留原值,避免重开页面时被降级为其他文件
|
||||
if (rawFileType) return aliasedFileType;
|
||||
const fileName = normalizeAttachmentTypeText(attachmentName(row));
|
||||
if (!fileName) return otherType;
|
||||
const matched = list
|
||||
@@ -283,17 +329,32 @@ export const normalizeContractFileRows = (rows, options = defaultContractAttachm
|
||||
description: row.description || '',
|
||||
}));
|
||||
|
||||
const mapDictOptions = response => {
|
||||
const mapDictOptions = (response, dictCode = CONTRACT_ATTACHMENT_TYPE_DICT) => {
|
||||
const data = response?.data?.data || response?.data || [];
|
||||
const records = Array.isArray(data) ? data : data.records || [];
|
||||
const applySealAlias = dictCode === CONTRACT_ATTACHMENT_TYPE_DICT;
|
||||
return records
|
||||
.map(item => ({
|
||||
label: item.dictValue,
|
||||
value: item.dictKey,
|
||||
}))
|
||||
.map(item => {
|
||||
const rawLabel = String(item.dictValue || '');
|
||||
const rawValue = String(item.dictKey || '');
|
||||
const label = applySealAlias
|
||||
? CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawLabel] ||
|
||||
(rawLabel === '合同文件' ? CONTRACT_SEAL_MATERIAL_LOCATION : rawLabel)
|
||||
: rawLabel;
|
||||
const value = applySealAlias
|
||||
? CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawValue] ||
|
||||
(rawValue === '合同文件' ? CONTRACT_SEAL_MATERIAL_LOCATION : rawValue)
|
||||
: rawValue;
|
||||
return { label, value };
|
||||
})
|
||||
.filter(item => item.label && item.value);
|
||||
};
|
||||
|
||||
const defaultOptionsByDictCode = dictCode =>
|
||||
dictCode === CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT
|
||||
? [...defaultContractSupportingAttachmentTypeOptions]
|
||||
: [...defaultContractAttachmentTypeOptions];
|
||||
|
||||
export default {
|
||||
name: 'ContractAttachmentSection',
|
||||
components: { UploadFilled },
|
||||
@@ -307,8 +368,13 @@ export default {
|
||||
markApprovedOnUpload: Boolean,
|
||||
useUploadDialog: Boolean,
|
||||
dialogOnly: Boolean,
|
||||
attachmentLocation: { type: String, default: '合同文件' },
|
||||
attachmentLocation: { type: String, default: CONTRACT_SEAL_MATERIAL_LOCATION },
|
||||
attachmentLocationOptions: { type: Array, default: null },
|
||||
/** 附件类型业务字典 code,默认用印材料字典;佐证材料传 contract_supporting_attachment_type */
|
||||
attachmentTypeDictCode: {
|
||||
type: String,
|
||||
default: CONTRACT_ATTACHMENT_TYPE_DICT,
|
||||
},
|
||||
preview: { type: Function, default: null },
|
||||
},
|
||||
emits: ['update:rows', 'upload-to-location', 'upload-confirmed'],
|
||||
@@ -317,8 +383,9 @@ export default {
|
||||
selected: [],
|
||||
attachmentFileTypes,
|
||||
contractAttachmentTypeOptions: [...defaultContractAttachmentTypeOptions],
|
||||
activeAttachmentTypeDictCode: CONTRACT_ATTACHMENT_TYPE_DICT,
|
||||
uploadDialogVisible: false,
|
||||
uploadDialogLocation: '合同文件',
|
||||
uploadDialogLocation: CONTRACT_SEAL_MATERIAL_LOCATION,
|
||||
uploadDialogType: resolveOtherAttachmentType(defaultContractAttachmentTypeOptions),
|
||||
uploadDialogFiles: [],
|
||||
uploadDialogFileList: [],
|
||||
@@ -337,48 +404,91 @@ export default {
|
||||
defaultAttachmentType() {
|
||||
return resolveOtherAttachmentType(this.contractAttachmentTypeOptions);
|
||||
},
|
||||
resolvedAttachmentTypeDictCode() {
|
||||
return this.attachmentTypeDictCode || CONTRACT_ATTACHMENT_TYPE_DICT;
|
||||
},
|
||||
resolvedLocationOptions() {
|
||||
const options = (this.attachmentLocationOptions || []).filter(Boolean);
|
||||
if (options.length) return options;
|
||||
return ['合同文件', '其它附件'];
|
||||
if (options.length) {
|
||||
return options.map(item => {
|
||||
if (item === '合同文件') return CONTRACT_SEAL_MATERIAL_LOCATION;
|
||||
if (item === '其它附件' || item === '其他附件') {
|
||||
return CONTRACT_SUPPORTING_ATTACHMENT_LOCATION;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return [CONTRACT_SEAL_MATERIAL_LOCATION, CONTRACT_SUPPORTING_ATTACHMENT_LOCATION];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
attachmentTypeDictCode: {
|
||||
immediate: false,
|
||||
handler() {
|
||||
this.loadAttachmentTypeOptions(this.resolvedAttachmentTypeDictCode, true);
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadAttachmentTypeOptions();
|
||||
this.loadAttachmentTypeOptions(this.resolvedAttachmentTypeDictCode, true);
|
||||
},
|
||||
methods: {
|
||||
attachmentName,
|
||||
loadAttachmentTypeOptions() {
|
||||
return getBizDictionary({ code: 'contract_attachment_type' })
|
||||
currentUploadUserName() {
|
||||
const raw = this.$store.getters.userInfo;
|
||||
const userInfo = raw && !Array.isArray(raw) ? raw : {};
|
||||
// 与顶部栏一致优先 real_name;用印材料 / 佐证材料共用
|
||||
return (
|
||||
userInfo.real_name ||
|
||||
userInfo.realName ||
|
||||
userInfo.nick_name ||
|
||||
userInfo.nickName ||
|
||||
userInfo.user_name ||
|
||||
userInfo.userName ||
|
||||
userInfo.account ||
|
||||
''
|
||||
);
|
||||
},
|
||||
loadAttachmentTypeOptions(dictCode = this.resolvedAttachmentTypeDictCode, alignRows = false) {
|
||||
const code = dictCode || CONTRACT_ATTACHMENT_TYPE_DICT;
|
||||
this.activeAttachmentTypeDictCode = code;
|
||||
const fallbackOptions = defaultOptionsByDictCode(code);
|
||||
return getBizDictionary({ code })
|
||||
.then(res => {
|
||||
const options = mapDictOptions(res);
|
||||
// 切换字典期间若已发起更新请求,忽略过期响应
|
||||
if (this.activeAttachmentTypeDictCode !== code) return;
|
||||
const options = mapDictOptions(res, code);
|
||||
if (options.length) {
|
||||
// 字典缺少「其他文件」时补上,保证未匹配文件名可默认选中
|
||||
this.contractAttachmentTypeOptions = options.some(isOtherAttachmentTypeOption)
|
||||
? options
|
||||
: [...options, { label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER }];
|
||||
if (
|
||||
!this.contractAttachmentTypeOptions.some(
|
||||
item => String(item.value) === String(this.uploadDialogType)
|
||||
)
|
||||
) {
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
}
|
||||
// 字典加载后,把已有行的中文类型名对齐到 dictKey,保证下拉可选中
|
||||
if (this.attachmentType && (this.rows || []).length) {
|
||||
const aligned = (this.rows || []).map(row => ({
|
||||
...row,
|
||||
fileType: resolveContractAttachmentType(row, this.contractAttachmentTypeOptions),
|
||||
}));
|
||||
const changed = aligned.some(
|
||||
(row, index) => row.fileType !== this.rows[index]?.fileType
|
||||
);
|
||||
if (changed) this.update(aligned);
|
||||
}
|
||||
} else {
|
||||
this.contractAttachmentTypeOptions = fallbackOptions;
|
||||
}
|
||||
if (
|
||||
!this.contractAttachmentTypeOptions.some(
|
||||
item => String(item.value) === String(this.uploadDialogType)
|
||||
)
|
||||
) {
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
}
|
||||
// 字典加载后,把已有行的中文类型名对齐到 dictKey,保证下拉可选中
|
||||
if (alignRows && this.attachmentType && (this.rows || []).length) {
|
||||
const aligned = (this.rows || []).map(row => ({
|
||||
...row,
|
||||
fileType: resolveContractAttachmentType(row, this.contractAttachmentTypeOptions),
|
||||
}));
|
||||
const changed = aligned.some(
|
||||
(row, index) => row.fileType !== this.rows[index]?.fileType
|
||||
);
|
||||
if (changed) this.update(aligned);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 字典加载失败时保留默认选项
|
||||
if (this.activeAttachmentTypeDictCode !== code) return;
|
||||
this.contractAttachmentTypeOptions = fallbackOptions;
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
});
|
||||
},
|
||||
isRowLocked(row) {
|
||||
@@ -388,13 +498,14 @@ export default {
|
||||
this.$emit('update:rows', Array.isArray(rows) ? rows.map(item => ({ ...item })) : []);
|
||||
},
|
||||
enrichUploadedRow(row, existing) {
|
||||
const uploadUserName = this.$store.getters.userInfo?.realName || '';
|
||||
const uploadUserName = this.currentUploadUserName();
|
||||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
const next = {
|
||||
...existing,
|
||||
...row,
|
||||
description: row.description || existing?.description || '',
|
||||
uploadUserName: existing?.uploadUserName || row.uploadUserName || uploadUserName,
|
||||
// 新上传行始终写入当前用户;已有行保留原上传人
|
||||
uploadUserName: existing?.uploadUserName || uploadUserName || row.uploadUserName || '',
|
||||
uploadTime: existing?.uploadTime || row.uploadTime || uploadTime,
|
||||
approved: existing?.approved || row.approved || false,
|
||||
};
|
||||
@@ -434,20 +545,38 @@ export default {
|
||||
resolveTypeByLocation(location) {
|
||||
const options = this.contractAttachmentTypeOptions || [];
|
||||
const locationText = String(location || '');
|
||||
if (locationText.includes('其它') || locationText.includes('其他')) {
|
||||
if (
|
||||
isContractSupportingAttachmentLocation(locationText) ||
|
||||
locationText.includes('佐证') ||
|
||||
locationText.includes('其它') ||
|
||||
locationText.includes('其他')
|
||||
) {
|
||||
return this.defaultAttachmentType;
|
||||
}
|
||||
const matched = options.find(
|
||||
item =>
|
||||
String(item.value) === locationText ||
|
||||
String(item.label) === locationText ||
|
||||
String(item.value).includes('用印') ||
|
||||
String(item.label).includes('用印') ||
|
||||
String(item.value).includes('合同') ||
|
||||
String(item.label).includes('合同')
|
||||
);
|
||||
return matched?.value || options[0]?.value || this.defaultAttachmentType;
|
||||
},
|
||||
syncUploadDialogTypeByLocation() {
|
||||
this.uploadDialogType = this.resolveTypeByLocation(this.uploadDialogLocation);
|
||||
const dictCode = resolveAttachmentTypeDictCode(
|
||||
this.uploadDialogLocation,
|
||||
this.resolvedAttachmentTypeDictCode
|
||||
);
|
||||
const applyType = () => {
|
||||
this.uploadDialogType = this.resolveTypeByLocation(this.uploadDialogLocation);
|
||||
};
|
||||
if (dictCode !== this.activeAttachmentTypeDictCode) {
|
||||
this.loadAttachmentTypeOptions(dictCode, false).finally(applyType);
|
||||
return;
|
||||
}
|
||||
applyType();
|
||||
},
|
||||
openUploadDialog() {
|
||||
const options = this.resolvedLocationOptions;
|
||||
@@ -516,7 +645,7 @@ export default {
|
||||
this.contractAttachmentTypeOptions
|
||||
);
|
||||
} else {
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
this.uploadDialogType = this.uploadDialogType || this.defaultAttachmentType;
|
||||
}
|
||||
this.$message.success('上传成功');
|
||||
},
|
||||
@@ -544,13 +673,13 @@ export default {
|
||||
this.$message.warning('请选择附件类型');
|
||||
return;
|
||||
}
|
||||
const uploadUserName = this.$store.getters.userInfo?.realName || '';
|
||||
const uploadUserName = this.currentUploadUserName();
|
||||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
const appended = this.uploadDialogFiles.map(row => ({
|
||||
...row,
|
||||
fileType: this.uploadDialogType,
|
||||
description: row.description || '',
|
||||
uploadUserName: row.uploadUserName || uploadUserName,
|
||||
uploadUserName: uploadUserName || row.uploadUserName || '',
|
||||
uploadTime: row.uploadTime || uploadTime,
|
||||
...(this.markApprovedOnUpload ? { approved: true } : {}),
|
||||
}));
|
||||
@@ -607,7 +736,15 @@ export default {
|
||||
downloadFileByUrl(url, attachmentName(row));
|
||||
},
|
||||
batchDownload() {
|
||||
(this.selected.length ? this.selected : this.rows || []).forEach(this.download);
|
||||
const sourceRows = this.selected.length ? this.selected : this.rows || [];
|
||||
const downloadableRows = sourceRows.filter(row => attachmentUrl(row));
|
||||
if (!downloadableRows.length) {
|
||||
this.$message.warning(this.selected.length ? '所选附件暂无可下载地址' : '暂无可下载附件');
|
||||
return;
|
||||
}
|
||||
downloadableRows.forEach((row, index) => {
|
||||
window.setTimeout(() => this.download(row), index * 400);
|
||||
});
|
||||
},
|
||||
formatSize(size) {
|
||||
const value = Number(size);
|
||||
|
||||
@@ -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>
|
||||
@@ -44,19 +44,22 @@
|
||||
<dt>附件</dt>
|
||||
<dd>
|
||||
<template v-if="attachments.length">
|
||||
<el-link
|
||||
<span
|
||||
v-for="file in attachments"
|
||||
:key="file.url || file.link || file.name || file.originalName"
|
||||
type="primary"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(file)"
|
||||
>{{ attachmentName(file) }}</el-link
|
||||
>
|
||||
@keydown.enter="previewAttachment(file)"
|
||||
@keydown.space.prevent="previewAttachment(file)"
|
||||
>{{ attachmentName(file) }}</span>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</dd>
|
||||
</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">
|
||||
@@ -107,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';
|
||||
@@ -161,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]); },
|
||||
@@ -330,7 +335,7 @@ export default {
|
||||
.transport-flow-tag {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #303133; font-size: 14px; word-break: break-all; } :deep(.el-link) { font-size: 14px; } }
|
||||
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #303133; font-size: 14px; word-break: break-all; } .attachment-file-name { font-size: 14px; max-width: 100%; } }
|
||||
.route-map { display: flex; align-items: flex-start; padding: 16px 24px 20px; border-top: 1px solid #eff1f7; overflow-x: auto; }
|
||||
.route-map__node { display: grid; flex: 0 0 150px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } }
|
||||
.route-map__progress { color: #303133 !important; }
|
||||
|
||||
@@ -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>
|
||||
@@ -266,7 +266,7 @@
|
||||
<el-col :span="6">
|
||||
<el-form-item label="单价">
|
||||
<el-input :model-value="freightGroupUnitPrice(group)" inputmode="decimal" placeholder="请输入" @input="value => handleFreightGroupUnitPriceInput(group, value)" @clear="() => handleFreightGroupUnitPriceInput(group, '')">
|
||||
<template #append><el-select :model-value="freightGroupPriceUnit(group)" class="freight-unit-select" @change="value => handleFreightGroupPriceUnitChange(group, value)"><el-option label="元/吨" value="元/吨" /><el-option label="元/件" value="元/件" /><el-option label="元/方" value="元/方" /></el-select></template>
|
||||
<template #append><el-select :model-value="freightGroupPriceUnit(group)" class="freight-unit-select" @change="value => handleFreightGroupPriceUnitChange(group, value)"><el-option v-for="item in priceUnitOptions(route)" :key="item.value" :label="item.label" :value="item.value" /></el-select></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -317,6 +317,14 @@
|
||||
<script>
|
||||
import * as api from '@/api/business/master-order';
|
||||
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import {
|
||||
currencyUnit,
|
||||
remapPriceUnitCurrency,
|
||||
resolvePriceUnitByQuantityUnit,
|
||||
} from '@/utils/currency';
|
||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||
import { formatRegionAddress } from '@/utils/region-address';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
|
||||
const unwrapRecords = res => {
|
||||
@@ -347,7 +355,8 @@ export default {
|
||||
carrierLoading: false,
|
||||
driverLoading: false,
|
||||
escortLoading: false,
|
||||
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
|
||||
quantityUnitOptions: [],
|
||||
quantityUnitLoading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -389,10 +398,24 @@ export default {
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadQuantityUnitOptions();
|
||||
await Promise.all([this.load(), this.loadCarrierOptions()]);
|
||||
this.applySelfOperatedCarrierDefaults();
|
||||
},
|
||||
methods: {
|
||||
loadQuantityUnitOptions() {
|
||||
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
|
||||
this.quantityUnitLoading = true;
|
||||
this.quantityUnitLoadPromise = loadMeasurementUnitOptions()
|
||||
.then(options => {
|
||||
this.quantityUnitOptions = options.map(item => item.value);
|
||||
return this.quantityUnitOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.quantityUnitLoading = false;
|
||||
});
|
||||
return this.quantityUnitLoadPromise;
|
||||
},
|
||||
async load() {
|
||||
if (!this.id) return;
|
||||
this.loading = true;
|
||||
@@ -412,8 +435,10 @@ export default {
|
||||
},
|
||||
async loadContractCurrency() {
|
||||
const directCurrency = this.master?.settlementCurrency || this.master?.settlementCurrencyCode || this.master?.currency;
|
||||
const previousCurrency = this.master?.settlementCurrency;
|
||||
if (!this.master?.projectId || !this.master?.contractId) {
|
||||
this.master.settlementCurrency = this.normalizeCurrencyValue(directCurrency || 'RMB');
|
||||
this.syncRouteCurrency(previousCurrency, this.master.settlementCurrency);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -427,6 +452,20 @@ export default {
|
||||
this.master.customerContractPartyB = '';
|
||||
this.master.settlementCurrency = 'RMB';
|
||||
}
|
||||
this.syncRouteCurrency(previousCurrency, this.master.settlementCurrency);
|
||||
},
|
||||
syncRouteCurrency(_previousCurrency, nextCurrency) {
|
||||
const currency = this.normalizeCurrencyValue(nextCurrency || 'RMB');
|
||||
(this.routes || []).forEach(route => {
|
||||
route.currency = currency;
|
||||
(route.freightItems || []).forEach(item => {
|
||||
if (item.priceUnit) {
|
||||
item.priceUnit = remapPriceUnitCurrency(item.priceUnit, currency);
|
||||
} else if (item.quantityUnit) {
|
||||
item.priceUnit = resolvePriceUnitByQuantityUnit(item.quantityUnit, currency);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
dispatchRouteNodes() {
|
||||
const routes = Array.isArray(this.master?.routes) ? this.master.routes : [];
|
||||
@@ -601,8 +640,23 @@ export default {
|
||||
const prices = (group.rows || []).map(item => String(item.unitPrice ?? '').trim()).filter(Boolean);
|
||||
return !prices.length || prices.some(price => price !== prices[0]) ? '' : prices[0];
|
||||
},
|
||||
freightGroupPriceUnit(group = {}) { return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : ''); },
|
||||
freightGroupPriceUnit(group = {}) {
|
||||
if (group.rows?.[0]?.priceUnit) return group.rows[0].priceUnit;
|
||||
if (!group.quantityUnit) return '';
|
||||
return (
|
||||
resolvePriceUnitByQuantityUnit(group.quantityUnit, this.master?.settlementCurrency || 'RMB') ||
|
||||
`${currencyUnit(this.master?.settlementCurrency || 'RMB')}/${group.quantityUnit}`
|
||||
);
|
||||
},
|
||||
freightGroupAmount(group = {}) { return (group.rows || []).reduce((sum, item) => sum + this.freightAmount(item), 0); },
|
||||
priceUnitOptions(route = {}) {
|
||||
const unit = currencyUnit(route.currency || this.master?.settlementCurrency || 'RMB');
|
||||
return [
|
||||
{ label: `${unit}/吨`, value: `${unit}/吨` },
|
||||
{ label: `${unit}/件`, value: `${unit}/件` },
|
||||
{ label: `${unit}/方`, value: `${unit}/方` },
|
||||
];
|
||||
},
|
||||
handleFreightGroupUnitPriceInput(group, value) {
|
||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||
const normalized = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||
@@ -612,6 +666,10 @@ export default {
|
||||
handleFreightGroupQuantityUnitChange(route, group, value) {
|
||||
(group.rows || []).forEach(item => {
|
||||
item.quantityUnit = value || '';
|
||||
item.priceUnit = resolvePriceUnitByQuantityUnit(
|
||||
value,
|
||||
route.currency || this.master?.settlementCurrency || 'RMB'
|
||||
) || item.priceUnit;
|
||||
const goods = (route.goods || []).find(row => String(row.sourceIndex) === String(item.sourceIndex));
|
||||
if (goods) goods.quantityUnit = value || '';
|
||||
});
|
||||
@@ -634,7 +692,7 @@ export default {
|
||||
})),
|
||||
});
|
||||
},
|
||||
currencyLabel(value) { return ({ CNY: '元', RMB: '元', USD: '美元', EUR: '欧元' })[value] || value || '元'; },
|
||||
currencyLabel(value) { return currencyUnit(value); },
|
||||
quantityNumber(value) { return Number(value || 0); },
|
||||
goodsKey(goods = {}, includeUnit = true) {
|
||||
return [
|
||||
@@ -794,7 +852,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 === '计划单' ? '计划结束日期' : '预计完成日期'; },
|
||||
@@ -824,6 +887,7 @@ export default {
|
||||
removeGoods(route, index) { route.goods.splice(index, 1); this.syncFreightItems(route); },
|
||||
syncFreightItems(route) {
|
||||
const oldItems = route.freightItems || [];
|
||||
const currency = route.currency || this.master?.settlementCurrency || 'RMB';
|
||||
route.freightItems = (route.goods || [])
|
||||
.filter(item => item.sourceIndex !== undefined && item.sourceIndex !== '')
|
||||
.map(item => {
|
||||
@@ -835,7 +899,10 @@ export default {
|
||||
quantity: item.dispatchQuantity || '',
|
||||
quantityUnit: item.quantityUnit || '',
|
||||
unitPrice: old.unitPrice || '',
|
||||
priceUnit: old.priceUnit || `元/${item.quantityUnit || '吨'}`,
|
||||
priceUnit:
|
||||
old.priceUnit ||
|
||||
resolvePriceUnitByQuantityUnit(item.quantityUnit || '吨', currency) ||
|
||||
`${currencyUnit(currency)}/${item.quantityUnit || '吨'}`,
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -888,9 +955,11 @@ export default {
|
||||
},
|
||||
carrierOptionLabel(item) {
|
||||
const duplicateCount = this.carrierOptions.filter(option => option.carrierName === item.carrierName).length;
|
||||
return duplicateCount > 1
|
||||
? `${item.carrierName}(${item.contractName || item.contractId})`
|
||||
: item.carrierName;
|
||||
if (duplicateCount <= 1) {
|
||||
return item.carrierName;
|
||||
}
|
||||
const contractLabel = formatContractOptionLabel(item);
|
||||
return contractLabel ? `${item.carrierName}(${contractLabel})` : item.carrierName;
|
||||
},
|
||||
driverOptionLabel(item) { return item.driverName || item.name || ''; },
|
||||
driverOptionKey(item) { return item.id || item.driverId || this.driverOptionLabel(item) || item.mobile; },
|
||||
@@ -1197,7 +1266,7 @@ export default {
|
||||
.route-overview__line { flex: 0 1 56px; min-width: 24px; height: 32px; margin-top: 0; border-bottom: 6px solid #e4e7ed; }
|
||||
.segment-header { min-height: 46px; padding: 0 20px; border-bottom: 1px solid #eff1f7; font-weight: 600; em { color: #409eff; font-style: normal; } }
|
||||
.segment-content { padding: 16px 24px; }
|
||||
.dispatch-form { :deep(.el-form-item) { margin-bottom: 14px; } :deep(.el-input), :deep(.el-select), :deep(.el-date-editor), :deep(.el-input-number) { width: 100%; } :deep(.el-input-group__append .el-select) { width: auto; } }
|
||||
.dispatch-form { :deep(.el-form-item) { margin-bottom: 24px; } :deep(.el-input), :deep(.el-select), :deep(.el-date-editor), :deep(.el-input-number) { width: 100%; } :deep(.el-input-group__append .el-select) { width: auto; } }
|
||||
/* 短标签按自身宽度,避免被「预计发货日期」等长标签把左侧撑开 */
|
||||
.dispatch-form__compact-label {
|
||||
:deep(.el-form-item__label) {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
><el-option
|
||||
v-for="contract in contracts"
|
||||
:key="contract.id"
|
||||
:label="contract.contractName"
|
||||
:label="formatContractOptionLabel(contract)"
|
||||
:value="contract.id" /></el-select></el-form-item></el-col
|
||||
><el-col :span="6"
|
||||
><el-form-item label="总单号" required
|
||||
@@ -261,11 +261,22 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="240" align="center" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="240"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220" align="center">
|
||||
@@ -589,6 +600,7 @@ import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||
import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
|
||||
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
||||
import { exportBlob, importBlob } from '@/api/common';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { OfficeBuilding, Search } from '@element-plus/icons-vue';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
@@ -597,10 +609,10 @@ import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from
|
||||
import '@open-file-viewer/core/style.css';
|
||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||
|
||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||
const quantityUnitOptions = ['吨', '件', '方'];
|
||||
const attachmentViewerPlugins = [
|
||||
imagePlugin(),
|
||||
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
|
||||
@@ -703,10 +715,11 @@ export default {
|
||||
'zip',
|
||||
],
|
||||
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
|
||||
quantityUnitOptions,
|
||||
quantityUnitOptions: [],
|
||||
quantityUnitLoading: false,
|
||||
form: {
|
||||
routes: [{ segmentNo: '段1', transportType: '' }],
|
||||
goods: [{ quantityUnit: quantityUnitOptions[0] }],
|
||||
goods: [{ quantityUnit: '吨' }],
|
||||
attachmentsJson: '[]',
|
||||
transportOrganizationType: '多式联运',
|
||||
},
|
||||
@@ -773,6 +786,7 @@ export default {
|
||||
async mounted() {
|
||||
this.loading = true;
|
||||
try {
|
||||
await this.loadQuantityUnitOptions();
|
||||
await Promise.all([this.loadProjects(), this.loadCargoTypeOptions(), this.loadRegionOptions()]);
|
||||
if (this.id) {
|
||||
await this.loadDetail(this.id);
|
||||
@@ -782,6 +796,23 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
defaultQuantityUnit() {
|
||||
return this.quantityUnitOptions[0] || '吨';
|
||||
},
|
||||
loadQuantityUnitOptions() {
|
||||
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
|
||||
this.quantityUnitLoading = true;
|
||||
this.quantityUnitLoadPromise = loadMeasurementUnitOptions()
|
||||
.then(options => {
|
||||
this.quantityUnitOptions = options.map(item => item.value);
|
||||
return this.quantityUnitOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.quantityUnitLoading = false;
|
||||
});
|
||||
return this.quantityUnitLoadPromise;
|
||||
},
|
||||
responseData(response) {
|
||||
return response?.data?.data || response?.data || response || {};
|
||||
},
|
||||
@@ -958,7 +989,7 @@ export default {
|
||||
finalTransportType: hasFinalSegment
|
||||
? finalSegment.transportType
|
||||
: detail.finalTransportType || '',
|
||||
goods: detail.goods || [{ quantityUnit: quantityUnitOptions[0] }],
|
||||
goods: detail.goods || [{ quantityUnit: this.defaultQuantityUnit() }],
|
||||
attachmentsJson: detail.attachmentsJson || '[]',
|
||||
transportOrganizationType: '多式联运',
|
||||
};
|
||||
@@ -1659,7 +1690,7 @@ export default {
|
||||
'',
|
||||
cargoTypePath,
|
||||
quantity: row.quantity || '',
|
||||
quantityUnit: row.quantityUnit || quantityUnitOptions[0],
|
||||
quantityUnit: row.quantityUnit || this.defaultQuantityUnit(),
|
||||
packageType: row.packageType || row.package || '',
|
||||
brand: row.brand || '',
|
||||
specification: row.specification || row.spec || '',
|
||||
@@ -1785,7 +1816,7 @@ export default {
|
||||
},
|
||||
addGoods(index) {
|
||||
this.form.goods.splice(index + 1, 0, {
|
||||
quantityUnit: quantityUnitOptions[0],
|
||||
quantityUnit: this.defaultQuantityUnit(),
|
||||
materialCode: '',
|
||||
deviceCode: '',
|
||||
remark: '',
|
||||
@@ -2047,7 +2078,7 @@ export default {
|
||||
font-size: 16px;
|
||||
}
|
||||
.el-form-item {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
.master-editor__quantity-field {
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<div v-if="groups.length" class="punch-record-photos">
|
||||
<section v-for="group in groups" :key="group.type" class="punch-record-photos__group">
|
||||
<div class="punch-record-photos__head">
|
||||
<span class="punch-record-photos__title">{{ group.type }}</span>
|
||||
<span v-if="editable" class="punch-record-photos__count">{{ countText(group) }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="punch-record-photos__grid"
|
||||
:class="{ 'punch-record-photos__grid--edit': editable }"
|
||||
>
|
||||
<div
|
||||
v-for="(item, groupIndex) in group.items"
|
||||
:key="`${item.photo.url}-${item.index}`"
|
||||
class="punch-record-photos__item"
|
||||
@click="handlePreview(item.index)"
|
||||
>
|
||||
<el-image
|
||||
:src="item.photo.url"
|
||||
:preview-src-list="nativePreview ? previewUrls : []"
|
||||
:initial-index="nativePreview ? item.index : 0"
|
||||
:preview-teleported="nativePreview"
|
||||
:lazy="lazy"
|
||||
fit="cover"
|
||||
/>
|
||||
<!-- 主图 = 该类型的第一张,按上传顺序取;没有独立字段,也不可切换 -->
|
||||
<div v-if="groupIndex === 0" class="punch-record-photos__badge">主图</div>
|
||||
<el-link
|
||||
v-if="editable"
|
||||
class="punch-record-photos__remove"
|
||||
type="danger"
|
||||
:underline="false"
|
||||
@click.stop="$emit('remove', item.index)"
|
||||
>删除</el-link
|
||||
>
|
||||
</div>
|
||||
<el-upload
|
||||
v-if="editable && !group.full"
|
||||
class="punch-record-photos__upload"
|
||||
:action="uploadAction"
|
||||
name="file"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:before-upload="file => handleBeforeUpload(file, group.type)"
|
||||
:on-success="res => $emit('upload-success', res, group.type)"
|
||||
:on-error="error => $emit('upload-error', error, group.type)"
|
||||
>
|
||||
<div class="punch-record-photos__add">+ 添加</div>
|
||||
</el-upload>
|
||||
<div v-else-if="editable" class="punch-record-photos__add punch-record-photos__add--full">
|
||||
已满 {{ maxPerType }} 张
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
// 打卡照片是每个打卡节点必有的一个照片类型,不由过程配置的凭证类型决定;
|
||||
// 它与各凭证类型各自最多 9 张,同一类型下第一张即界面上的「主图」。
|
||||
const PUNCH_PHOTO_TYPE = '打卡照片';
|
||||
// 照片没带类型时的兜底名称,与后端落库时的兜底口径一致
|
||||
const PHOTO_TYPE_FALLBACK = '凭证';
|
||||
const MAX_PHOTOS_PER_TYPE = 9;
|
||||
|
||||
const props = defineProps({
|
||||
// 扁平照片数组:管理端回显用 voucherType,司机端回显与旧数据用 type
|
||||
photos: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 恒定展示的照片类型:「打卡照片」+ 该节点过程配置勾选的凭证类型。
|
||||
// 留空则只按照片自身带的类型分组(只读回显用)。
|
||||
groupTypes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 编辑态:每个类型自带「添加」格与删除链接,并显示已选/上限
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 每个类型的张数上限;0 表示不限制(在途打卡只有一个照片字段,张数由「只保留第一张」约束)
|
||||
maxPerType: {
|
||||
type: Number,
|
||||
default: MAX_PHOTOS_PER_TYPE,
|
||||
},
|
||||
// 编辑态用 el-image 自带预览;只读态不发预览事件,把点击交给调用方开自己的看图器
|
||||
nativePreview: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
uploadAction: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
uploadHeaders: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 返回 false 即中止本次上传;类型、体积、张数校验都在调用方
|
||||
beforeUpload: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['preview', 'remove', 'upload-success', 'upload-error']);
|
||||
|
||||
const photoType = photo =>
|
||||
String(photo?.voucherType || photo?.type || '').trim() || PHOTO_TYPE_FALLBACK;
|
||||
|
||||
const groups = computed(() => {
|
||||
const photos = Array.isArray(props.photos) ? props.photos : [];
|
||||
const names = [];
|
||||
const buckets = new Map();
|
||||
const ensure = name => {
|
||||
if (!buckets.has(name)) {
|
||||
buckets.set(name, []);
|
||||
names.push(name);
|
||||
}
|
||||
return buckets.get(name);
|
||||
};
|
||||
// 恒定展示的类型先占位:一张照片都没有的类型也要出现,否则编辑态下没有添加入口
|
||||
props.groupTypes.forEach(type => {
|
||||
const name = String(type || '').trim();
|
||||
if (name) ensure(name);
|
||||
});
|
||||
// 按上传顺序入组,组间顺序即类型首次出现的顺序,不重排照片
|
||||
photos.forEach((photo, index) => {
|
||||
if (!photo?.url) return;
|
||||
ensure(photoType(photo)).push({ photo, index });
|
||||
});
|
||||
const list = names.map(type => {
|
||||
const items = buckets.get(type);
|
||||
return {
|
||||
type,
|
||||
items,
|
||||
full: props.editable && props.maxPerType > 0 && items.length >= props.maxPerType,
|
||||
};
|
||||
});
|
||||
// 编辑态至少要有一个分组,否则记录里还没有照片时就没有添加入口
|
||||
if (props.editable && !list.length) {
|
||||
return [{ type: props.groupTypes[0] || PUNCH_PHOTO_TYPE, items: [], full: false }];
|
||||
}
|
||||
return list;
|
||||
});
|
||||
|
||||
// 预览列表按原始下标对齐,删除/分组都不改变点击某张时打开的是哪一张
|
||||
const previewUrls = computed(() =>
|
||||
(Array.isArray(props.photos) ? props.photos : []).map(photo => photo?.url || '')
|
||||
);
|
||||
|
||||
const handlePreview = index => {
|
||||
if (props.nativePreview) return;
|
||||
emit('preview', index);
|
||||
};
|
||||
|
||||
const handleBeforeUpload = (file, type) => {
|
||||
if (typeof props.beforeUpload !== 'function') return true;
|
||||
return props.beforeUpload(file, type);
|
||||
};
|
||||
|
||||
const countText = group =>
|
||||
props.maxPerType > 0 ? `${group.items.length}/${props.maxPerType}` : String(group.items.length);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.punch-record-photos {
|
||||
// 弹窗里本组件挂在 el-form-item__content 下,那是个 flex 容器:
|
||||
// 不给宽度就会收缩成内容宽,里面的网格跟着塌成一小块
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 4px;
|
||||
|
||||
&__group {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
// 只读回显:撑满容器宽度的五列网格
|
||||
&__grid {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
// 编辑态沿用弹窗原本的固定 96px 方图 + 自动换行:尺寸不随容器宽窄变化
|
||||
&__grid--edit {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
.el-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border: 1px solid #eff1f7;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
}
|
||||
|
||||
&__grid--edit &__item {
|
||||
width: 96px;
|
||||
|
||||
.el-image {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
aspect-ratio: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0 6px;
|
||||
border-radius: 4px 0 4px 0;
|
||||
background: rgba(64, 158, 255, 0.9);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&__remove {
|
||||
align-self: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__grid--edit &__upload {
|
||||
width: 96px;
|
||||
}
|
||||
|
||||
&__add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__grid--edit &__add {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
aspect-ratio: auto;
|
||||
}
|
||||
|
||||
&__add--full {
|
||||
color: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -113,7 +113,7 @@
|
||||
<el-option
|
||||
v-for="item in contractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -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
|
||||
@@ -454,11 +460,20 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column type="index" label="序号" width="70" />
|
||||
<el-table-column label="文件名" min-width="220"
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
><template #default="{ row }"
|
||||
><el-link type="primary" @click="previewAttachment(row)">{{
|
||||
attachmentName(row)
|
||||
}}</el-link></template
|
||||
><span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column label="附件描述" min-width="220"
|
||||
@@ -721,11 +736,21 @@
|
||||
</div>
|
||||
<el-table :data="detailAttachmentRows" border empty-text="暂无附件">
|
||||
<el-table-column type="index" label="序号" width="70" />
|
||||
<el-table-column label="文件名" min-width="220">
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">{{
|
||||
attachmentName(row)
|
||||
}}</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220">
|
||||
@@ -1098,10 +1123,16 @@ import {
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { packageOptions } from '@/option/business/common';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import {
|
||||
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 {
|
||||
@@ -1237,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 },
|
||||
@@ -1252,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,
|
||||
@@ -1278,7 +1317,8 @@ export default {
|
||||
cargoTypeRequest: null,
|
||||
transportCargoRows: [],
|
||||
packageOptions,
|
||||
quantityUnitOptions: ['吨', '千克', '立方米', '立方', '公里', '单', '件', '车', '箱', '托盘'],
|
||||
quantityUnitOptions: [],
|
||||
quantityUnitLoading: false,
|
||||
feeUnitOptions: [],
|
||||
currencyOptions: [],
|
||||
currencyLoading: false,
|
||||
@@ -1372,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
|
||||
@@ -1435,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),
|
||||
@@ -1501,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 ||
|
||||
@@ -1521,15 +1584,42 @@ export default {
|
||||
this.handleTransportTypeChange(value);
|
||||
this.syncFreightItems();
|
||||
},
|
||||
'form.templateType'() {
|
||||
// 切换到运单模板时立即按货物数量单位生成公路运费明细。
|
||||
this.$nextTick(() => this.syncFreightItems());
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadProjectOptions();
|
||||
this.ensureCargoTypeOptions();
|
||||
this.loadQuantityUnitOptions();
|
||||
this.loadFeeUnitOptions();
|
||||
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;
|
||||
this.quantityUnitLoadPromise = loadMeasurementUnitOptions()
|
||||
.then(options => {
|
||||
this.quantityUnitOptions = options.map(item => item.value);
|
||||
return this.quantityUnitOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.quantityUnitLoading = false;
|
||||
});
|
||||
return this.quantityUnitLoadPromise;
|
||||
},
|
||||
buildOption(option) {
|
||||
const next = { ...option, column: (option.column || []).map(column => ({ ...column })) };
|
||||
const tableOrder = this.config.tableColumnOrder || [];
|
||||
@@ -1547,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';
|
||||
@@ -1732,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;
|
||||
@@ -1777,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?.());
|
||||
},
|
||||
@@ -1793,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?.());
|
||||
},
|
||||
@@ -1839,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()
|
||||
@@ -1961,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;
|
||||
@@ -1989,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?.();
|
||||
@@ -2250,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() {
|
||||
@@ -3054,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(
|
||||
@@ -3087,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, '');
|
||||
@@ -3119,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(
|
||||
@@ -3384,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;
|
||||
@@ -3506,7 +3692,7 @@ export default {
|
||||
}
|
||||
.shipping-template-page__detail-form :deep(.el-form-item) {
|
||||
min-width: 0;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.shipping-template-page__detail-form :deep(.el-form-item__label) {
|
||||
color: #909399;
|
||||
@@ -3695,7 +3881,7 @@ export default {
|
||||
}
|
||||
|
||||
:global(.shipping-template-form-page-dialog .avue-group .el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:global(.shipping-template-form-page-dialog .avue-form__menu) {
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
<el-option
|
||||
v-for="item in contractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -492,11 +492,22 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="200" align="left" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="200"
|
||||
align="left"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ row.originalName || row.name }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ row.originalName || row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220" align="center">
|
||||
@@ -673,14 +684,16 @@
|
||||
<div class="transport-plan-page__transport-plan-detail-field is-attachments">
|
||||
<span>附件</span>
|
||||
<div class="transport-plan-page__transport-plan-detail-attachments">
|
||||
<el-link
|
||||
<span
|
||||
v-for="file in attachmentRows"
|
||||
:key="file.url || file.name || file.originalName"
|
||||
type="primary"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(file)"
|
||||
>
|
||||
{{ attachmentName(file) }}
|
||||
</el-link>
|
||||
@keydown.enter="previewAttachment(file)"
|
||||
@keydown.space.prevent="previewAttachment(file)"
|
||||
>{{ attachmentName(file) }}</span>
|
||||
<span v-if="!attachmentRows.length">-</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -855,11 +868,21 @@
|
||||
</div>
|
||||
<el-table :data="attachmentRows" empty-text="暂无附件">
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="260">
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="260"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -980,7 +1003,7 @@
|
||||
<el-option
|
||||
v-for="item in transportPlanImportContractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -1654,7 +1677,7 @@
|
||||
{{
|
||||
isDispatchPlanQuantityUnlimited(row)
|
||||
? '-'
|
||||
: formatDispatchQuantity(dispatchItemRemainingQuantity(row))
|
||||
: formatDispatchQuantity(dispatchItemDisplayRemainingQuantity(row))
|
||||
}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -1744,11 +1767,11 @@
|
||||
><el-select
|
||||
:model-value="dispatchFreightGroupPriceUnit(group)"
|
||||
class="transport-plan-page__dispatch-unit-select"
|
||||
placeholder="元/吨"
|
||||
:placeholder="defaultPriceUnit"
|
||||
@change="value => handleDispatchFreightGroupPriceUnitChange(group, value)"
|
||||
@visible-change="visible => visible && loadTaskFeeUnitOptions()"
|
||||
><el-option
|
||||
v-for="item in taskFeeUnitOptions"
|
||||
v-for="item in taskFeeUnitSelectOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value" /></el-select
|
||||
@@ -1838,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="请输入"
|
||||
@@ -1851,7 +1874,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="dispatchItemForm.carrierType === '承运商' ? '手机号' : '司机手机号'"
|
||||
:required="dispatchItemForm.carrierType !== '承运商'"
|
||||
required
|
||||
>
|
||||
<el-input v-model="dispatchItemForm.driverPhone" placeholder="请输入" />
|
||||
</el-form-item>
|
||||
@@ -1932,7 +1955,7 @@
|
||||
class="transport-plan-page__dispatch-quantity-hint"
|
||||
>
|
||||
剩余数量:{{
|
||||
formatDispatchQuantity(dispatchItemHintRemainingQuantity(dispatchItemForm))
|
||||
formatDispatchQuantity(dispatchItemDisplayRemainingQuantity(dispatchItemForm))
|
||||
}}
|
||||
{{ dispatchItemForm.quantityUnit || '吨' }}
|
||||
</div>
|
||||
@@ -1969,17 +1992,17 @@
|
||||
<el-select
|
||||
v-model="dispatchItemForm.priceUnit"
|
||||
class="transport-plan-page__dispatch-unit-select"
|
||||
placeholder="元/吨"
|
||||
:placeholder="defaultPriceUnit"
|
||||
:loading="taskFeeUnitLoading"
|
||||
@visible-change="visible => visible && loadTaskFeeUnitOptions()"
|
||||
>
|
||||
<el-option
|
||||
v-if="!taskFeeUnitOptions.some(item => item.value === '元/吨')"
|
||||
label="元/吨"
|
||||
value="元/吨"
|
||||
v-if="!taskFeeUnitSelectOptions.some(item => item.value === defaultPriceUnit)"
|
||||
:label="defaultPriceUnit"
|
||||
:value="defaultPriceUnit"
|
||||
/>
|
||||
<el-option
|
||||
v-for="item in taskFeeUnitOptions"
|
||||
v-for="item in taskFeeUnitSelectOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
@@ -2073,11 +2096,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="请输入"
|
||||
@@ -2088,11 +2107,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
|
||||
@@ -2201,11 +2216,22 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="200" align="center" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="200"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row, dispatchItemAttachmentRows)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
@keydown.enter="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
@keydown.space.prevent="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220" align="center">
|
||||
@@ -2741,7 +2767,15 @@ import { addressTypeOptions } from '@/option/base/common-address';
|
||||
import { packageOptions } from '@/option/business/common';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import {
|
||||
currencyUnit,
|
||||
mapFeeUnitOptions,
|
||||
remapPriceUnitCurrency,
|
||||
resolvePriceUnitByQuantityUnit,
|
||||
} from '@/utils/currency';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||
import { applyTableMenuWidth } from '@/utils/table-menu';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
@@ -2765,8 +2799,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),
|
||||
@@ -3030,6 +3067,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: {},
|
||||
@@ -3070,10 +3111,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';
|
||||
@@ -3108,6 +3150,8 @@ export default {
|
||||
dispatchItemIndex: -1,
|
||||
dispatchItemForm: defaultDispatchRow(),
|
||||
dispatchItemCargoRows: [],
|
||||
// 本次数量变更版本号:驱动同表其他行「剩余数量」同步重算
|
||||
dispatchCargoQuantityVersion: 0,
|
||||
dispatchItemAttachmentRows: [],
|
||||
selectedDispatchItemAttachmentRows: [],
|
||||
attachmentRows: [],
|
||||
@@ -3206,7 +3250,7 @@ export default {
|
||||
billingCargoTypeLoading: false,
|
||||
billingCargoTypeRequest: null,
|
||||
packageOptions,
|
||||
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
|
||||
quantityUnitOptions: [],
|
||||
attachmentFileTypes: [
|
||||
'pdf',
|
||||
'bmp',
|
||||
@@ -3245,6 +3289,13 @@ export default {
|
||||
} else {
|
||||
this.consumeTemplateCreatePayload();
|
||||
}
|
||||
this.loadTaskQuantityUnitOptions();
|
||||
},
|
||||
// keep-alive 重新激活列表时强制刷新,避免缓存脏数据;标记位用于兼容旧跳转逻辑。
|
||||
activated() {
|
||||
if (this.isFormPageInstance) return;
|
||||
sessionStorage.removeItem(transportPlanListRefreshKey);
|
||||
this.$nextTick(() => this.onLoad(this.page, this.query));
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
@@ -3257,10 +3308,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) {
|
||||
@@ -3609,6 +3670,22 @@ export default {
|
||||
const value = this.dispatchItemForm.freightCurrency || 'RMB';
|
||||
return this.currencyRemark(value);
|
||||
},
|
||||
taskFeeUnitSelectOptions() {
|
||||
const currency =
|
||||
this.dispatchItemForm.freightCurrency ||
|
||||
this.dispatchContractCurrency ||
|
||||
this.form.settlementCurrency ||
|
||||
'RMB';
|
||||
return mapFeeUnitOptions(this.taskFeeUnitOptions, currency);
|
||||
},
|
||||
defaultPriceUnit() {
|
||||
return `${currencyUnit(
|
||||
this.dispatchItemForm.freightCurrency ||
|
||||
this.dispatchContractCurrency ||
|
||||
this.form.settlementCurrency ||
|
||||
'RMB'
|
||||
)}/吨`;
|
||||
},
|
||||
isAdmin() {
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
@@ -3693,6 +3770,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 ||
|
||||
@@ -3719,6 +3808,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
initStandaloneFormPage() {
|
||||
if (!this.isStandaloneFormPage) return;
|
||||
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
||||
@@ -4307,8 +4397,6 @@ export default {
|
||||
['contractName', '客户合同'],
|
||||
['planName', '计划名称'],
|
||||
['transportType', '运输方式'],
|
||||
['planStartDate', '计划开始日期'],
|
||||
['planEndDate', '计划结束日期'],
|
||||
['departureAddress', '发货地址'],
|
||||
['arrivalAddress', '收货地址'],
|
||||
];
|
||||
@@ -4426,6 +4514,9 @@ export default {
|
||||
loading();
|
||||
}
|
||||
},
|
||||
markTransportPlanListNeedRefresh() {
|
||||
sessionStorage.setItem(transportPlanListRefreshKey, '1');
|
||||
},
|
||||
rowSave(row, done, loading) {
|
||||
const submitRow = this.normalizeRow(row);
|
||||
if (!submitRow) {
|
||||
@@ -4434,6 +4525,7 @@ export default {
|
||||
}
|
||||
this.getSaveRequest()(submitRow).then(
|
||||
() => {
|
||||
this.markTransportPlanListNeedRefresh();
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '操作成功!' });
|
||||
done();
|
||||
@@ -4453,6 +4545,7 @@ export default {
|
||||
}
|
||||
this.getSaveRequest()(submitRow).then(
|
||||
() => {
|
||||
this.markTransportPlanListNeedRefresh();
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '操作成功!' });
|
||||
done();
|
||||
@@ -4482,7 +4575,7 @@ export default {
|
||||
.submit(submitRow)
|
||||
.then(() => {
|
||||
this.$message.success(action === 'draft' ? '暂存成功' : '创建成功');
|
||||
this.onLoad(this.page, this.query);
|
||||
this.markTransportPlanListNeedRefresh();
|
||||
this.closeCrudDialog();
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -4490,12 +4583,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;
|
||||
}
|
||||
@@ -4804,19 +4899,49 @@ export default {
|
||||
contract.customerNames ||
|
||||
this.form.customerName ||
|
||||
'';
|
||||
const nextCurrency = this.getContractCurrency(contract);
|
||||
this.remapTransportPlanPriceUnits(nextCurrency);
|
||||
this.$refs.crud?.validateField?.('contractName');
|
||||
},
|
||||
remapTransportPlanPriceUnits(currency) {
|
||||
if (this.form.priceUnit) {
|
||||
this.form.priceUnit = remapPriceUnitCurrency(this.form.priceUnit, currency);
|
||||
}
|
||||
if (this.dispatchItemForm.priceUnit) {
|
||||
this.dispatchItemForm.priceUnit = remapPriceUnitCurrency(
|
||||
this.dispatchItemForm.priceUnit,
|
||||
currency
|
||||
);
|
||||
}
|
||||
this.dispatchItemForm.freightCurrency = currency;
|
||||
(this.dispatchItemCargoRows || []).forEach(cargo => {
|
||||
if (cargo.priceUnit) {
|
||||
cargo.priceUnit = remapPriceUnitCurrency(cargo.priceUnit, currency);
|
||||
}
|
||||
});
|
||||
(this.transportCargoRows || []).forEach(cargo => {
|
||||
if (cargo.priceUnit) {
|
||||
cargo.priceUnit = remapPriceUnitCurrency(cargo.priceUnit, currency);
|
||||
}
|
||||
});
|
||||
},
|
||||
loadDispatchContractCurrency(plan = {}) {
|
||||
const findContract = () =>
|
||||
this.contractOptions.find(item => String(item.id) === String(plan.contractId)) ||
|
||||
this.contractOptions.find(item => item.contractName === plan.contractName);
|
||||
const applyCurrency = contract => {
|
||||
const previousCurrency = this.dispatchContractCurrency;
|
||||
this.dispatchContractCurrency = this.getContractCurrency(
|
||||
contract &&
|
||||
(contract.settlementCurrency || contract.settlementCurrencyCode || contract.currency)
|
||||
? contract
|
||||
: { settlementCurrency: plan.settlementCurrency }
|
||||
);
|
||||
if (previousCurrency && previousCurrency !== this.dispatchContractCurrency) {
|
||||
this.remapTransportPlanPriceUnits(this.dispatchContractCurrency);
|
||||
} else {
|
||||
this.dispatchItemForm.freightCurrency = this.dispatchContractCurrency;
|
||||
}
|
||||
return this.dispatchContractCurrency;
|
||||
};
|
||||
const current = findContract();
|
||||
@@ -5076,15 +5201,40 @@ export default {
|
||||
return Promise.resolve(this.taskQuantityUnitOptions);
|
||||
}
|
||||
this.taskQuantityUnitLoading = true;
|
||||
return getDictionary({ code: 'quantity_unit' })
|
||||
.then(res => {
|
||||
this.taskQuantityUnitOptions = this.normalizeDictOptions(res.data.data || []);
|
||||
return loadMeasurementUnitOptions()
|
||||
.then(options => {
|
||||
this.taskQuantityUnitOptions = options;
|
||||
this.quantityUnitOptions = options.map(item => item.value);
|
||||
this.ensureQuantityUnitOptions([
|
||||
this.dispatchItemForm?.quantityUnit,
|
||||
...(this.dispatchItemCargoRows || []).map(row => row.quantityUnit),
|
||||
...(this.transportCargoRows || []).map(row => row.quantityUnit),
|
||||
]);
|
||||
return this.taskQuantityUnitOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.taskQuantityUnitLoading = false;
|
||||
});
|
||||
},
|
||||
ensureQuantityUnitOptions(units = []) {
|
||||
const pending = [...new Set(
|
||||
(Array.isArray(units) ? units : [units])
|
||||
.map(item => String(item || '').trim())
|
||||
.filter(Boolean)
|
||||
)];
|
||||
if (!pending.length) return;
|
||||
pending.forEach(unit => {
|
||||
if (!this.quantityUnitOptions.includes(unit)) {
|
||||
this.quantityUnitOptions = [...this.quantityUnitOptions, unit];
|
||||
}
|
||||
if (!this.taskQuantityUnitOptions.some(item => item.value === unit)) {
|
||||
this.taskQuantityUnitOptions = [
|
||||
...this.taskQuantityUnitOptions,
|
||||
{ label: unit, value: unit },
|
||||
];
|
||||
}
|
||||
});
|
||||
},
|
||||
loadTaskFeeUnitOptions() {
|
||||
if (this.taskFeeUnitOptions.length || this.taskFeeUnitLoading) {
|
||||
return Promise.resolve(this.taskFeeUnitOptions);
|
||||
@@ -5146,6 +5296,9 @@ export default {
|
||||
});
|
||||
},
|
||||
normalizeTransportCargoRow(row = {}) {
|
||||
const quantityUnit = String(
|
||||
row.quantityUnit || row.goodsQuantityUnit || row.unit || ''
|
||||
).trim();
|
||||
const cargo = {
|
||||
...defaultTransportCargo(),
|
||||
...row,
|
||||
@@ -5162,10 +5315,14 @@ export default {
|
||||
packageType: row.packageType || row.package || '',
|
||||
specification: row.specification || row.spec || '',
|
||||
freightAmount: row.freightAmount ?? row.amount ?? row.totalAmount ?? '',
|
||||
quantityUnit,
|
||||
};
|
||||
['quantity', 'mileage', 'unitPrice', 'freightAmount'].forEach(prop => {
|
||||
if (Number(cargo[prop]) === -1) cargo[prop] = '';
|
||||
});
|
||||
if (quantityUnit) {
|
||||
this.ensureQuantityUnitOptions(quantityUnit);
|
||||
}
|
||||
return cargo;
|
||||
},
|
||||
hasMultipleConfiguredGoods(source = {}, rows = []) {
|
||||
@@ -5183,7 +5340,7 @@ export default {
|
||||
},
|
||||
addTransportCargoRow(index = -1, row = {}) {
|
||||
const nextRow = this.normalizeTransportCargoRow({
|
||||
priceUnit: this.form.priceUnit || '元/吨',
|
||||
priceUnit: this.form.priceUnit || this.defaultPriceUnit,
|
||||
...row,
|
||||
});
|
||||
if (
|
||||
@@ -6514,20 +6671,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;
|
||||
});
|
||||
@@ -7532,6 +7702,7 @@ export default {
|
||||
row.cargoName = '';
|
||||
row.specification = '';
|
||||
row.model = '';
|
||||
this.dispatchCargoQuantityVersion += 1;
|
||||
},
|
||||
handleDispatchConfiguredCargoNameChange(row, value) {
|
||||
const nextValue = String(value ?? row.cargoName ?? '').trim();
|
||||
@@ -7540,6 +7711,7 @@ export default {
|
||||
);
|
||||
if (!option) {
|
||||
row.cargoName = nextValue;
|
||||
this.dispatchCargoQuantityVersion += 1;
|
||||
return;
|
||||
}
|
||||
const previousType = String(row.cargoType || '').trim();
|
||||
@@ -7554,6 +7726,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 =
|
||||
@@ -7594,6 +7767,9 @@ export default {
|
||||
...baseRow,
|
||||
...(index >= 0 ? row : {}),
|
||||
};
|
||||
this.dispatchItemForm.quantityUnit =
|
||||
this.dispatchItemForm.quantityUnit || defaultUnit || TRANSPORT_PLAN_QUANTITY_UNIT;
|
||||
this.ensureQuantityUnitOptions(this.dispatchItemForm.quantityUnit);
|
||||
this.dispatchItemForm.otherFeeTotal = this.normalizeDispatchFeeValue(
|
||||
this.dispatchItemForm.otherFeeTotal
|
||||
);
|
||||
@@ -7603,7 +7779,11 @@ export default {
|
||||
});
|
||||
const goodsRows = this.parseJsonArray(this.dispatchItemForm.goodsJson);
|
||||
this.dispatchItemCargoRows = (goodsRows.length ? goodsRows : [this.dispatchItemForm]).map(
|
||||
item => this.normalizeTransportCargoRow(item)
|
||||
item =>
|
||||
this.normalizeTransportCargoRow({
|
||||
...item,
|
||||
quantityUnit: item.quantityUnit || this.dispatchItemForm.quantityUnit || defaultUnit,
|
||||
})
|
||||
);
|
||||
const hasMultiplePlanGoods = this.dispatchPlanGoodsRows.length > 1;
|
||||
const shouldAutoFillFullGoods =
|
||||
@@ -7613,11 +7793,18 @@ export default {
|
||||
const unscheduledGoodsRows = this.getDispatchUnscheduledGoodsRows();
|
||||
this.dispatchItemForm.taskEntryMode = 'full';
|
||||
this.dispatchItemForm.quantity = '';
|
||||
this.dispatchItemCargoRows = unscheduledGoodsRows.length
|
||||
? unscheduledGoodsRows
|
||||
: this.dispatchPlanGoodsRows.map(goods =>
|
||||
this.normalizeTransportCargoRow({ ...goods, quantity: '' })
|
||||
);
|
||||
this.dispatchItemCargoRows = (
|
||||
unscheduledGoodsRows.length
|
||||
? unscheduledGoodsRows
|
||||
: this.dispatchPlanGoodsRows.map(goods =>
|
||||
this.normalizeTransportCargoRow({ ...goods, quantity: '' })
|
||||
)
|
||||
).map(goods =>
|
||||
this.normalizeTransportCargoRow({
|
||||
...goods,
|
||||
quantityUnit: goods.quantityUnit || defaultUnit || TRANSPORT_PLAN_QUANTITY_UNIT,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!this.dispatchItemCargoRows.length) {
|
||||
this.dispatchItemCargoRows = [this.normalizeTransportCargoRow()];
|
||||
@@ -7632,7 +7819,10 @@ export default {
|
||||
this.dispatchItemCargoRows = this.dispatchItemCargoRows.map((cargo, itemIndex) => ({
|
||||
...cargo,
|
||||
unitPrice: cargo.unitPrice || freightInfo.freightItems[itemIndex]?.unitPrice || '',
|
||||
priceUnit: cargo.priceUnit || freightInfo.freightItems[itemIndex]?.priceUnit || '元/吨',
|
||||
priceUnit:
|
||||
cargo.priceUnit ||
|
||||
freightInfo.freightItems[itemIndex]?.priceUnit ||
|
||||
this.defaultPriceUnit,
|
||||
freightAmount:
|
||||
cargo.freightAmount || freightInfo.freightItems[itemIndex]?.freightAmount || '',
|
||||
}));
|
||||
@@ -7865,6 +8055,7 @@ export default {
|
||||
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);
|
||||
@@ -7881,15 +8072,17 @@ export default {
|
||||
return value;
|
||||
},
|
||||
addDispatchCargoRow(index = -1) {
|
||||
const nextRow = this.normalizeTransportCargoRow({ priceUnit: '元/吨' });
|
||||
const nextRow = this.normalizeTransportCargoRow({ priceUnit: this.defaultPriceUnit });
|
||||
if (index > -1) this.dispatchItemCargoRows.splice(index + 1, 0, nextRow);
|
||||
else this.dispatchItemCargoRows.push(nextRow);
|
||||
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) {
|
||||
@@ -7925,7 +8118,14 @@ export default {
|
||||
return prices[0];
|
||||
},
|
||||
dispatchFreightGroupPriceUnit(group = {}) {
|
||||
return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : '');
|
||||
if (group.rows?.[0]?.priceUnit) return group.rows[0].priceUnit;
|
||||
if (!group.quantityUnit) return '';
|
||||
return (
|
||||
resolvePriceUnitByQuantityUnit(
|
||||
group.quantityUnit,
|
||||
this.dispatchItemForm.freightCurrency || this.dispatchContractCurrency || 'RMB'
|
||||
) || `${currencyUnit(this.dispatchContractCurrency || 'RMB')}/${group.quantityUnit}`
|
||||
);
|
||||
},
|
||||
dispatchFreightGroupAmount(group = {}) {
|
||||
const amount = (group.rows || []).reduce(
|
||||
@@ -8032,11 +8232,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();
|
||||
@@ -8134,13 +8373,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]);
|
||||
@@ -8309,7 +8552,10 @@ export default {
|
||||
this.dispatchItemForm.cargoName ||
|
||||
'',
|
||||
quantity: quantityText,
|
||||
quantityUnit: firstGoods.quantityUnit || this.dispatchItemForm.quantityUnit || '',
|
||||
quantityUnit:
|
||||
firstGoods.quantityUnit ||
|
||||
this.dispatchItemForm.quantityUnit ||
|
||||
TRANSPORT_PLAN_QUANTITY_UNIT,
|
||||
unitPrice: firstGoods.unitPrice || '',
|
||||
priceUnit: firstGoods.priceUnit || '',
|
||||
freightTotal: this.dispatchItemFreightTotal,
|
||||
@@ -8324,7 +8570,7 @@ export default {
|
||||
unitPrice: cargo.unitPrice || '',
|
||||
priceUnit: cargo.priceUnit || '',
|
||||
quantity: cargo.quantity || '',
|
||||
quantityUnit: cargo.quantityUnit || '',
|
||||
quantityUnit: cargo.quantityUnit || firstGoods.quantityUnit || TRANSPORT_PLAN_QUANTITY_UNIT,
|
||||
freightAmount: this.dispatchCargoFreightAmount(cargo),
|
||||
})),
|
||||
}),
|
||||
@@ -8413,26 +8659,40 @@ export default {
|
||||
return {
|
||||
id: this.dispatchRow.id,
|
||||
mode,
|
||||
waybills: rows.map(row => ({
|
||||
...row,
|
||||
taskEntryMode: row.taskEntryMode || 'simple',
|
||||
taskRemark: row.remark || '',
|
||||
estimatedStartTime: this.formatDispatchDate(row.estimatedStartTime),
|
||||
estimatedEndTime: this.formatDispatchDate(row.estimatedEndTime),
|
||||
goodsJson:
|
||||
row.goodsJson ||
|
||||
JSON.stringify([
|
||||
waybills: rows.map(row => {
|
||||
const quantityUnit = this.getDispatchQuantityUnit(row);
|
||||
let goodsJson = row.goodsJson;
|
||||
const goodsRows = this.parseJsonArray(goodsJson);
|
||||
if (goodsRows.length) {
|
||||
goodsJson = JSON.stringify(
|
||||
goodsRows.map(goods => ({
|
||||
...goods,
|
||||
quantityUnit: goods.quantityUnit || quantityUnit,
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
goodsJson = JSON.stringify([
|
||||
{
|
||||
cargoName: row.cargoName,
|
||||
cargoType: row.cargoType,
|
||||
quantity: row.quantity,
|
||||
quantityUnit: row.quantityUnit,
|
||||
quantityUnit,
|
||||
specification: row.specification,
|
||||
model: row.model,
|
||||
packageType: row.packageType,
|
||||
},
|
||||
]),
|
||||
})),
|
||||
]);
|
||||
}
|
||||
return {
|
||||
...row,
|
||||
quantityUnit,
|
||||
taskEntryMode: row.taskEntryMode || 'simple',
|
||||
taskRemark: row.remark || '',
|
||||
estimatedStartTime: this.formatDispatchDate(row.estimatedStartTime),
|
||||
estimatedEndTime: this.formatDispatchDate(row.estimatedEndTime),
|
||||
goodsJson,
|
||||
};
|
||||
}),
|
||||
};
|
||||
},
|
||||
formatDispatchDate(value) {
|
||||
@@ -8680,7 +8940,7 @@ export default {
|
||||
|
||||
&__dispatch-shipping-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9871,7 +10131,7 @@ export default {
|
||||
}
|
||||
|
||||
:global(.transport-plan-form-page-dialog .avue-group .el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:global(.transport-plan-form-page-dialog .avue-group .dialog-section-title) {
|
||||
|
||||
@@ -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
|
||||
@@ -270,15 +266,22 @@
|
||||
prop="mileage"
|
||||
label="里程(km)"
|
||||
width="100"
|
||||
/><el-table-column prop="unitPrice" label="单价" width="90" /><el-table-column
|
||||
prop="freight"
|
||||
label="运费"
|
||||
width="100"
|
||||
/><el-table-column prop="otherFeeTotal" label="其他费用合计" width="120" /><el-table-column
|
||||
prop="freightTotal"
|
||||
label="运费合计"
|
||||
width="100"
|
||||
/><el-table-column
|
||||
>
|
||||
<template #default="{ row }">{{ formatOptionalNumber(row.mileage) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="unitPrice" label="单价" width="90">
|
||||
<template #default="{ row }">{{ formatOptionalNumber(row.unitPrice) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="freight" label="运费" width="100">
|
||||
<template #default="{ row }">{{ formatOptionalNumber(row.freight) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="otherFeeTotal" label="其他费用合计" width="120">
|
||||
<template #default="{ row }">{{ formatOptionalNumber(row.otherFeeTotal) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="freightTotal" label="运费合计" width="100">
|
||||
<template #default="{ row }">{{ formatOptionalNumber(row.freightTotal) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="actualStartDate"
|
||||
label="实际发货时间"
|
||||
width="160"
|
||||
@@ -374,7 +377,7 @@
|
||||
<el-option
|
||||
v-for="item in contracts"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -610,6 +613,7 @@ import { getList as getProjectList } from '@/api/business/project-apply';
|
||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import * as api from '@/api/business/waybill-manage';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import SectionCard from '@/components/section-card/main.vue';
|
||||
|
||||
@@ -633,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: '更新时间' },
|
||||
@@ -879,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] : [];
|
||||
@@ -1079,19 +1085,40 @@ const transportTypeLabel = value => {
|
||||
);
|
||||
return matched?.dictValue || matched?.label || named || text || '-';
|
||||
};
|
||||
const buildBatchQueryParams = () => ({
|
||||
batchNo: query.batchNo || undefined,
|
||||
carrierId: query.carrierId || undefined,
|
||||
createUser: query.createUser || undefined,
|
||||
createTimeStart: query.createTimeRange?.[0] || undefined,
|
||||
createTimeEnd: query.createTimeRange?.[1] || undefined,
|
||||
current: page.current,
|
||||
size: page.size,
|
||||
});
|
||||
const loadListOptions = async () => {
|
||||
const res = await api.getImportOptions?.();
|
||||
const data = res?.data?.data || {};
|
||||
queryCarriers.value = data.carriers || [];
|
||||
creators.value = data.creators || [];
|
||||
};
|
||||
const loadBatches = async () => {
|
||||
const res = await api.getImportBatches({ ...query, current: page.current, size: page.size });
|
||||
const res = await api.getImportBatches(buildBatchQueryParams());
|
||||
batches.value = res.data?.data?.records || [];
|
||||
page.total = res.data?.data?.total || 0;
|
||||
};
|
||||
onMounted(() => {
|
||||
if (props.standalone) loadBatches();
|
||||
if (props.standalone) {
|
||||
loadListOptions();
|
||||
loadBatches();
|
||||
}
|
||||
if (props.createPage) openCreatePage();
|
||||
});
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
value => {
|
||||
if (value) loadBatches();
|
||||
if (value) {
|
||||
loadListOptions();
|
||||
loadBatches();
|
||||
}
|
||||
}
|
||||
);
|
||||
const resetQuery = () => {
|
||||
@@ -1106,7 +1133,7 @@ const loadDetails = async () => {
|
||||
current: detailPage.current,
|
||||
size: detailPage.size,
|
||||
});
|
||||
details.value = res.data?.data?.records || [];
|
||||
details.value = (res.data?.data?.records || []).map(normalizeDetailRow);
|
||||
detailPage.total = res.data?.data?.total || 0;
|
||||
};
|
||||
const openCreate = () => prepareCreateForm(true);
|
||||
@@ -1155,7 +1182,7 @@ const loadBatchRows = async batchId => {
|
||||
if (!batchId) return;
|
||||
const res = await api.getImportDetails({ batchId, current: 1, size: 9999 });
|
||||
rows.value = extractRecords(res).map(({ id, ...item }, index) => ({
|
||||
...item,
|
||||
...normalizeDetailRow(item),
|
||||
_key: `${id || 'row'}-${index}`,
|
||||
batchNo: item.batchNo || form.batchNo,
|
||||
}));
|
||||
@@ -1234,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));
|
||||
@@ -1288,15 +1353,60 @@ 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 || '';
|
||||
};
|
||||
const firstNotEmpty = (...values) => values.find(value => String(value ?? '').trim()) ?? '';
|
||||
const OPTIONAL_NUMBER_EMPTY_SENTINELS = [-1];
|
||||
const OPTIONAL_NUMBER_PROPS = ['mileage', 'unitPrice', 'freight', 'otherFeeTotal', 'freightTotal'];
|
||||
const normalizeOptionalImportNumber = (value, emptySentinels = OPTIONAL_NUMBER_EMPTY_SENTINELS) => {
|
||||
if (!String(value ?? '').trim()) return null;
|
||||
return emptySentinels.some(sentinel => Number(value) === sentinel) ? null : value;
|
||||
};
|
||||
const formatOptionalNumber = value => {
|
||||
const normalized = normalizeOptionalImportNumber(value);
|
||||
return normalized === null || normalized === undefined ? '' : normalized;
|
||||
};
|
||||
const normalizeDetailRow = row => {
|
||||
const next = { ...(row || {}) };
|
||||
OPTIONAL_NUMBER_PROPS.forEach(prop => {
|
||||
next[prop] = formatOptionalNumber(next[prop]);
|
||||
});
|
||||
return next;
|
||||
};
|
||||
const normalizeImportRow = row => ({
|
||||
...row,
|
||||
mileage: normalizeOptionalImportNumber(
|
||||
firstNotEmpty(row.mileage, row['里程'], row['里程(km)'], row['里程(公里)'])
|
||||
),
|
||||
otherFeeTotal: normalizeOptionalImportNumber(
|
||||
firstNotEmpty(row.otherFeeTotal, row['其他费用合计'])
|
||||
),
|
||||
unitPrice: normalizeOptionalImportNumber(firstNotEmpty(row.unitPrice, row['单价'])),
|
||||
freight: normalizeOptionalImportNumber(firstNotEmpty(row.freight, row['运费'])),
|
||||
freightTotal: normalizeOptionalImportNumber(firstNotEmpty(row.freightTotal, row['运费合计'])),
|
||||
transportType: firstNotEmpty(
|
||||
row.transportType,
|
||||
row['运输类型'],
|
||||
row['*运输类型'],
|
||||
row['运输方式'],
|
||||
row['*运输方式']
|
||||
),
|
||||
});
|
||||
const fileChange = async (file, list) => {
|
||||
if (!projectInfoReady.value) {
|
||||
ElMessage.warning('请先选择项目、客户合同和承运信息,再导入运单明细');
|
||||
@@ -1370,6 +1480,9 @@ const fileChange = async (file, list) => {
|
||||
item[key] ??
|
||||
'';
|
||||
});
|
||||
['mileage', 'unitPrice', 'freight', 'otherFeeTotal', 'freightTotal'].forEach(prop => {
|
||||
row[prop] = normalizeOptionalImportNumber(row[prop]);
|
||||
});
|
||||
row.actualStartDate = normalizeImportDate(row.actualStartDate);
|
||||
row.actualEndDate = normalizeImportDate(row.actualEndDate);
|
||||
row.planStartDate = normalizeImportDate(row.planStartDate);
|
||||
@@ -1618,6 +1731,7 @@ const downloadTemplate = async () => {
|
||||
ElMessage.error('模板下载失败');
|
||||
}
|
||||
};
|
||||
const isValidMobile = value => /^\d{11}$/.test(String(value || '').trim());
|
||||
const validateImportRows = () => {
|
||||
if (!rows.value.length) {
|
||||
ElMessage.warning('请上传至少一条运单明细');
|
||||
@@ -1629,6 +1743,11 @@ const validateImportRows = () => {
|
||||
ElMessage.warning(`第${index + 1}行${missingField.label}不能为空`);
|
||||
return false;
|
||||
}
|
||||
const driverPhone = String(row.driverPhone ?? '').trim();
|
||||
if (driverPhone && !isValidMobile(driverPhone)) {
|
||||
ElMessage.warning(`第${index + 1}行司机/船长手机号格式不正确,应为11位数字`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@@ -1656,31 +1775,6 @@ const removeSelectedRows = () => {
|
||||
rowSelection.value = [];
|
||||
refreshDuplicateFlags();
|
||||
};
|
||||
const firstNotEmpty = (...values) => values.find(value => String(value ?? '').trim()) ?? '';
|
||||
const normalizeOptionalImportNumber = (value, emptySentinels = []) => {
|
||||
if (!String(value ?? '').trim()) return null;
|
||||
return emptySentinels.some(sentinel => Number(value) === sentinel) ? null : value;
|
||||
};
|
||||
const normalizeImportRow = row => ({
|
||||
...row,
|
||||
mileage: normalizeOptionalImportNumber(
|
||||
firstNotEmpty(row.mileage, row['里程'], row['里程(km)'], row['里程(公里)']),
|
||||
[-1]
|
||||
),
|
||||
otherFeeTotal: normalizeOptionalImportNumber(
|
||||
firstNotEmpty(row.otherFeeTotal, row['其他费用合计'])
|
||||
),
|
||||
unitPrice: normalizeOptionalImportNumber(firstNotEmpty(row.unitPrice, row['单价'])),
|
||||
freight: normalizeOptionalImportNumber(firstNotEmpty(row.freight, row['运费'])),
|
||||
freightTotal: normalizeOptionalImportNumber(firstNotEmpty(row.freightTotal, row['运费合计'])),
|
||||
transportType: firstNotEmpty(
|
||||
row.transportType,
|
||||
row['运输类型'],
|
||||
row['*运输类型'],
|
||||
row['运输方式'],
|
||||
row['*运输方式']
|
||||
),
|
||||
});
|
||||
const normalizeOptionalPlanId = value => {
|
||||
if (value === undefined || value === null || value === '') return null;
|
||||
return Number(value) === -1 || Number(value) <= 0 ? null : value;
|
||||
@@ -1688,11 +1782,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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,7 @@
|
||||
<el-form-item label="项目">
|
||||
<el-input v-model="form.projectName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属组织">
|
||||
<el-form-item label="合同负责部门">
|
||||
<el-input v-model="form.organizationName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="甲方">
|
||||
@@ -63,6 +63,15 @@
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期">
|
||||
<el-date-picker
|
||||
v-model="form.applyDate"
|
||||
type="date"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同期限">
|
||||
<div class="contract-basic-section__date-range">
|
||||
<el-date-picker
|
||||
@@ -91,12 +100,13 @@
|
||||
<template #suffix>天</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同金额">
|
||||
<el-form-item label="合同金额" prop="contractAmount">
|
||||
<el-input
|
||||
:model-value="contractAmountDisplay"
|
||||
placeholder="请输入"
|
||||
clearable
|
||||
@input="handleContractAmountInput"
|
||||
@blur="formatContractAmountField"
|
||||
@clear="form.contractAmount = null"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -154,7 +164,7 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否需要加盖法人章">
|
||||
<el-select v-model="form.legalSealFlag" placeholder="请选择">
|
||||
<el-select v-model="form.legalSealFlag" placeholder="请选择" clearable>
|
||||
<el-option label="否" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
@@ -173,20 +183,35 @@
|
||||
<el-input v-model="form.handlerUserName" disabled />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="备注" class="contract-basic-section__remark">
|
||||
<el-form-item label="合同主要内容" class="contract-basic-section__remark">
|
||||
<el-input
|
||||
v-model="form.mainContent"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入合同主要内容"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="其他事项说明" class="contract-basic-section__remark">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入备注"
|
||||
placeholder="请输入其他事项说明"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 风险客商提示:合同已推送合规并留痕高风险/黑名单时展示,变更提交同样要求特批附件 -->
|
||||
<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="合同文件"
|
||||
title="用印材料"
|
||||
description
|
||||
attachment-type
|
||||
:attachment-location-options="changeAttachmentLocationOptions"
|
||||
@@ -197,7 +222,12 @@
|
||||
/>
|
||||
|
||||
<section class="change-section">
|
||||
<div class="section-head"><div class="dialog-section-title">计费信息</div><el-button type="primary" plain @click="addPlan">添加</el-button></div>
|
||||
<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>
|
||||
@@ -251,13 +281,20 @@
|
||||
<el-button type="primary" plain @click="addPaymentRatioRow">添加</el-button>
|
||||
</section>
|
||||
|
||||
<billing-plan-editor v-model="planDialogVisible" :value="planEditor" :index="planEditorIndex" @save="savePlan" />
|
||||
<billing-plan-editor
|
||||
v-model="planDialogVisible"
|
||||
:value="planEditor"
|
||||
:index="planEditorIndex"
|
||||
:settlement-currency="form.settlementCurrency"
|
||||
@save="savePlan"
|
||||
/>
|
||||
|
||||
<contract-attachment-section
|
||||
title="其它附件"
|
||||
title="附件(佐证材料等)"
|
||||
description
|
||||
attachment-type
|
||||
attachment-location="其它附件"
|
||||
attachment-location="附件(佐证材料等)"
|
||||
attachment-type-dict-code="contract_supporting_attachment_type"
|
||||
:attachment-location-options="changeAttachmentLocationOptions"
|
||||
:rows="attachments"
|
||||
:preview="previewAttachment"
|
||||
@@ -305,9 +342,11 @@
|
||||
|
||||
<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,
|
||||
isContractSupportingAttachmentLocation,
|
||||
normalizeContractFileRows,
|
||||
} from './components/contract-attachment-section.vue';
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
@@ -358,6 +397,19 @@ const normalizeOptionalAmount = value => {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : null;
|
||||
};
|
||||
const formatContractAmountInput = (value, emptyValue = null) => {
|
||||
if (
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
Number(value) === -1 ||
|
||||
String(value) === '-1'
|
||||
) {
|
||||
return emptyValue;
|
||||
}
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number >= 0 ? number.toFixed(2) : emptyValue;
|
||||
};
|
||||
|
||||
|
||||
const normalizeCustomPeriods = (periods = []) =>
|
||||
@@ -398,7 +450,7 @@ export default {
|
||||
},
|
||||
preSettlementConfig: {},
|
||||
formalSettlementConfig: {},
|
||||
settlementTypeOptions: ['月结', '日结', '周结', '半月结', '固定天数周期结算'],
|
||||
settlementTypeOptions: ['月结', '日结', '周结', '固定天数周期结算'],
|
||||
billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'],
|
||||
billCutoffDayOptions: Array.from({ length: 31 }, (_, index) => ({
|
||||
label: `${index + 1}日`,
|
||||
@@ -424,6 +476,21 @@ export default {
|
||||
],
|
||||
rules: {
|
||||
contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
|
||||
contractAmount: [
|
||||
{ required: true, message: '请输入合同金额', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return callback(new Error('请输入合同金额'));
|
||||
}
|
||||
if (Number(value) < 0) {
|
||||
return callback(new Error('合同金额不能小于0'));
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
changeReason: [{ max: 500, message: '变更原因不能超过500个字符', trigger: 'blur' }],
|
||||
},
|
||||
};
|
||||
@@ -432,8 +499,19 @@ 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年以上及其他);被执行人信息;买卖合同纠纷;劳动纠纷;重大纠纷。提交变更前须上传「特批附件」类型附件`;
|
||||
},
|
||||
changeAttachmentLocationOptions() {
|
||||
return ['合同文件', '其它附件', '变更材料'];
|
||||
return ['用印材料', '附件(佐证材料等)', '变更材料'];
|
||||
},
|
||||
contractAmountDisplay() {
|
||||
const value = this.form.contractAmount;
|
||||
@@ -498,6 +576,9 @@ export default {
|
||||
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
|
||||
this.form.contractAmount = normalized === '' ? null : normalized;
|
||||
},
|
||||
formatContractAmountField() {
|
||||
this.form.contractAmount = formatContractAmountInput(this.form.contractAmount);
|
||||
},
|
||||
loadDictionaries() {
|
||||
Promise.all([
|
||||
getSystemDictionary({ code: 'currency_type' }),
|
||||
@@ -540,7 +621,7 @@ export default {
|
||||
copyCount: normalizeOptionalPositiveInteger(data.copyCount),
|
||||
invoiceCycle: normalizeOptionalPositiveInteger(data.invoiceCycle),
|
||||
paymentDays: normalizeOptionalPositiveInteger(data.paymentDays),
|
||||
contractAmount: normalizeOptionalAmount(data.contractAmount),
|
||||
contractAmount: formatContractAmountInput(data.contractAmount),
|
||||
templateFlag: data.templateFlag ?? 0,
|
||||
electronicSealFlag: data.electronicSealFlag ?? 0,
|
||||
archiveStatus: data.archiveStatus || '未归档',
|
||||
@@ -601,7 +682,7 @@ export default {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
// 审核通过后上一次的变更材料归集到「其它附件」,按文件地址/文件名去重,避免重复展示
|
||||
// 审核通过后上一次的变更材料归集到「附件(佐证材料等)」,按文件地址/文件名去重,避免重复展示
|
||||
mergeChangeAttachments(attachments = [], changeAttachments = []) {
|
||||
const key = item => this.attachmentUrl(item) || this.attachmentName(item);
|
||||
const exists = new Set(attachments.map(key));
|
||||
@@ -655,11 +736,11 @@ export default {
|
||||
previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; },
|
||||
handleAttachmentUploadToLocation({ location, rows = [] }) {
|
||||
if (!rows.length) return;
|
||||
if (location === '合同文件') {
|
||||
if (isContractSealMaterialLocation(location)) {
|
||||
this.contractFileRows = [...(this.contractFileRows || []), ...rows];
|
||||
return;
|
||||
}
|
||||
if (location === '其它附件') {
|
||||
if (isContractSupportingAttachmentLocation(location)) {
|
||||
this.attachments = [...(this.attachments || []), ...rows];
|
||||
return;
|
||||
}
|
||||
@@ -678,7 +759,7 @@ export default {
|
||||
removeCustomPeriodRow(index) { if (index <= 0) return; const periods = [...(this.settlementRule.customPeriods || [])]; periods.splice(index, 1); this.settlementRule.customPeriods = normalizeCustomPeriods(periods); },
|
||||
validateCustomPeriods(periods = [], label) { const rows = normalizeCustomPeriods(periods); if (!rows.length) { this.$message.warning(`${label}:请至少配置一段自定义周期`); return false; } for (let index = 0; index < rows.length; index += 1) { const row = rows[index]; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间开始日`); return false; } if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间结束日`); return false; } if (endDay < startDay) { this.$message.warning(`${label}:第${index + 1}行结束日不能早于开始日`); return false; } if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) { this.$message.warning(`${label}:自定义多周期区间必须连续,不允许重叠或存在日期缺口`); return false; } } return true; },
|
||||
validateSettlementRule(rule, label) { if (Number(rule.autoGenerate) !== 1) return true; if (!rule.billStartDate || !rule.settlementType) { this.$message.warning(`${label}:请完整填写账单起始日和结算类型`); return false; } if (rule.settlementType === '月结' && !rule.billCycleType) { this.$message.warning(`${label}:请选择结算周期`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '固定截单日' && !rule.billCutoffDay) { this.$message.warning(`${label}:请选择账单截单日`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') return this.validateCustomPeriods(rule.customPeriods, label); if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) { this.$message.warning(`${label}:请选择周期天数`); return false; } return true; },
|
||||
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); await submitMkApprovalFlow({ bizType: 'contract-manage', bizId: this.form.id, subjectName: this.form.contractName || '', approvalStatus: this.form.approvalStatus || 'change_rejected' }); this.$message.success('变更已提交'); this.$router.back(); },
|
||||
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); await submitMkApprovalFlow({ bizType: 'contract-manage', bizId: this.form.id, subjectName: this.form.contractName || '', approvalStatus: this.form.approvalStatus || 'change_rejected', mkEnabled: await loadMkEnabled().catch(() => false) }); this.$message.success('变更已提交'); this.$router.back(); },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -692,11 +773,22 @@ export default {
|
||||
.contract-basic-section > .dialog-section-title { margin-bottom: 20px; color: #303133; }
|
||||
.contract-basic-section__grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: 48px; }
|
||||
.contract-basic-section__change-type { grid-column: 1 / -1; }
|
||||
.contract-basic-section__remark { width: 100%; margin-bottom: 0 !important; }
|
||||
.contract-basic-section__remark {
|
||||
width: 100%;
|
||||
margin-bottom: 23px !important;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
.contract-basic-section__date-range { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 8px; width: 360px; max-width: 100%; }
|
||||
.contract-basic-section__date-range span { color: #606266; }
|
||||
// 风险客商提示条(基本信息备注后):橙黄背景 + 红色文字,与合同新增页一致
|
||||
.contract-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: 16px; }
|
||||
.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),
|
||||
|
||||
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