Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 96b675b3be | |||
| 6067d24e3b | |||
| e051327f2d | |||
| f7759fd9e7 | |||
| 4287133f84 | |||
| 4b01316f64 | |||
| 0b170fe5c1 | |||
| 4f29b1e0eb | |||
| 6db599400e | |||
| c5a74acd00 | |||
| f23d65e41e | |||
| 6b0cb37699 | |||
| c867c5d7a5 | |||
| 903e0a4d7b | |||
| 6461c3d80f | |||
| 8380351529 | |||
| f8a238d0c0 | |||
| 5c6fcbe951 | |||
| 7997089b16 | |||
| 089b74479b | |||
| cf542313f7 | |||
| a19097d1be | |||
| 87aa64d0c1 | |||
| fbad28b717 | |||
| 19972aa8d8 | |||
| 5569f373c5 | |||
| 7887ae2342 | |||
| 9919fb9c89 | |||
| 6eab96b620 | |||
| b39501291c | |||
| 9315431273 | |||
| 2c9ce33c10 | |||
| 85602ff98f | |||
| 749b314ceb | |||
| 74780b8777 | |||
| d8f4f22b9c | |||
| b077ba5be4 | |||
| b9b579dc14 | |||
| 6ae23bb959 | |||
| 4294c92e43 | |||
| b840530fe9 | |||
| e6a1370de2 | |||
| 65296618a7 | |||
| c4ca347f0e | |||
| b4ecc90470 | |||
| 0484c2359d | |||
| 7fca74907f | |||
| d7ddeacbbf | |||
| 3d59203dbe | |||
| 3700f5d82b | |||
| 05b16bbea0 | |||
| dc28bc2afa | |||
| 730c4e2a6a | |||
| 9b42551bcd | |||
| 66944bdaf9 | |||
| 09da05c75e | |||
| 8bc312502f | |||
| 9b40800aab | |||
| f49f36fb60 | |||
| 7fd8140761 | |||
| aabe67e4d6 | |||
| e63c7fdd42 | |||
| ec56ee07be | |||
| a5eeda6152 | |||
| 5c45e9b2c9 | |||
| 331a39b37d | |||
| aa0aec3704 | |||
| 622705325b | |||
| 4657fc29bd | |||
| 884de893db | |||
| cc73d544ff | |||
| 10fecb5a54 | |||
| bab6bdba27 | |||
| a554ccf38e | |||
| a748e52551 | |||
| e89e78b527 | |||
| 2a03991297 | |||
| 884cc4355f | |||
| 58aaf79445 | |||
| f067518c0e | |||
| 11460f3378 | |||
| 7f543937fb |
@@ -1,5 +1,6 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
|
pnpm-lock.yaml
|
||||||
/dist
|
/dist
|
||||||
|
|
||||||
/tests/e2e/videos/
|
/tests/e2e/videos/
|
||||||
|
|||||||
@@ -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,等用户确认。
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
- `searchIndex: 4` + `searchIcon: true` → 默认只显示前 4 个搜索项 + "展开/收起"(columnLen > searchIndex 时才出现该按钮)。
|
- `searchIndex: 4` + `searchIcon: true` → 默认只显示前 4 个搜索项 + "展开/收起"(columnLen > searchIndex 时才出现该按钮)。
|
||||||
- **搜索项 label 与表格列 label 不同**(列「收款方」/搜索「客户名称」):单独定义 hide 列 + `searchProp` 指向后端字段(avue 在 search-change 里自动改名)。
|
- **搜索项 label 与表格列 label 不同**(列「收款方」/搜索「客户名称」):单独定义 hide 列 + `searchProp` 指向后端字段(avue 在 search-change 里自动改名)。
|
||||||
- 搜索专用列加 `hide: true` + `showColumn: false`,否则会混进列显隐抽屉。
|
- 搜索专用列加 `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 这种"既搜索又展示"的列要放回表格列的正确位置。
|
- ⚠️ **column 数组顺序 = 表格列顺序**:搜索专用列别插在表格列中间(会把表格列顶到前面);deptName 这种"既搜索又展示"的列要放回表格列的正确位置。
|
||||||
- 分页:`page: { currentPage, pageSize, pageSizes, total }`;`on-load` 由 avue 内部(table-page)在 created / 翻页 / 改页容量时触发,`current-change`/`size-change` 里**只改 page 不要再调 onLoad**(否则双请求)。
|
- 分页:`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 进查询参数)。
|
- `onLoad(page)` 只用 `this.query`(avue 传的 page 对象含 total/pageSizes 等分页字段,别 spread 进查询参数)。
|
||||||
@@ -57,11 +58,15 @@
|
|||||||
- 按钮层级:次要=无 type;中间步骤=`primary plain`;主操作=`primary`(禁绿、禁两个蓝实心)。顺序 `[辅助][取消][保存草稿][提交]`。
|
- 按钮层级:次要=无 type;中间步骤=`primary plain`;主操作=`primary`(禁绿、禁两个蓝实心)。顺序 `[辅助][取消][保存草稿][提交]`。
|
||||||
- 独立页标题 `.archive-page-form__title`;分组用全局 `<section-card>`;上传证件区 `width:100%; max-width:240px` + `.el-upload{height:151px}`。
|
- 独立页标题 `.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。
|
- ⚠️ `<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 须带字段名。
|
- placeholder 只写 `请输入`/`请选择`;例外:`请输入或选择车辆`、日期区间 `起`/`止`、示例值。校验 message 须带字段名。
|
||||||
- `business-crud-page.vue`:option 经 `cloneOption` 克隆,独立表单页走 `PageAvueForm`。
|
- `business-crud-page.vue`:option 经 `cloneOption` 克隆,独立表单页走 `PageAvueForm`。
|
||||||
- 自检:`curl "localhost:2889/src/xxx.vue"` 与 `"...?vue&type=style&index=0&scoped=true&lang.scss"`(200 / 500)。`vite build` 在 `facelogin.vue` 被沙箱中断,不能作唯一校验。dev `VITE_APP_API=/api` 代理 `172.16.203.228:8000`。
|
- 自检:`curl "localhost:2888/src/xxx.vue"` 与 `"...?vue&type=style&index=0&scoped=true&lang.scss"`(200 / 500)。`vite build` 在 `facelogin.vue` 被沙箱中断,不能作唯一校验。dev `VITE_APP_API=/api` 代理 `172.16.203.228:8000`。
|
||||||
- 导出:前端 `exportColumns` 后端未用,以 `XxxExportExcel.java` 为准;BladeX `BeanUtil` 类型不兼容静默跳过(`Date` → `LocalDateTime` 丢值,用 `DateUtil.fromDate`)。
|
- 导出:前端 `exportColumns` 后端未用,以 `XxxExportExcel.java` 为准;BladeX `BeanUtil` 类型不兼容静默跳过(`Date` → `LocalDateTime` 丢值,用 `DateUtil.fromDate`)。
|
||||||
|
|
||||||
## Element Plus 图标两种用法的机制差异(易误判)
|
## Element Plus 图标两种用法的机制差异(易误判)
|
||||||
@@ -69,3 +74,10 @@
|
|||||||
但 `:icon="OfficeBuilding"` 是**表达式**,编译成 `_ctx.OfficeBuilding`,只会从 data / props / computed / setup 返回值里取 —— **组件对象必须放进 `data()`**;放进 `components: {}` 注册里模板取不到,会静默渲染不出图标(不报错,只是空白)。
|
但 `:icon="OfficeBuilding"` 是**表达式**,编译成 `_ctx.OfficeBuilding`,只会从 data / props / computed / setup 返回值里取 —— **组件对象必须放进 `data()`**;放进 `components: {}` 注册里模板取不到,会静默渲染不出图标(不报错,只是空白)。
|
||||||
项目既有惯例:需要 `:icon` 的图标一律 `import` 后在 `data()` 里 `return { Xxx }`(transport-plan-page、waybill-manage-page、business-crud-page 都是这么写的)。
|
项目既有惯例:需要 `:icon` 的图标一律 `import` 后在 `data()` 里 `return { Xxx }`(transport-plan-page、waybill-manage-page、business-crud-page 都是这么写的)。
|
||||||
注:标签形式(如 `<Location />`)不依赖 data,放不放 data 都能显示;仅 `:icon` 必须放 data。
|
注:标签形式(如 `<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"`),改表格列时先看这两条。
|
||||||
|
|||||||
+5
-2
@@ -6,7 +6,8 @@
|
|||||||
"prod": "vite --mode production",
|
"prod": "vite --mode production",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:prod": "vite build --mode production",
|
"build:prod": "vite build --mode production",
|
||||||
"serve": "vite preview --host"
|
"serve": "vite preview --host",
|
||||||
|
"typecheck": "vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
@@ -47,9 +48,11 @@
|
|||||||
"prettier": "^2.8.7",
|
"prettier": "^2.8.7",
|
||||||
"sass": "^1.85.1",
|
"sass": "^1.85.1",
|
||||||
"terser": "^5.31.1",
|
"terser": "^5.31.1",
|
||||||
|
"typescript": "^7.0.2",
|
||||||
"unplugin-auto-import": "^0.11.2",
|
"unplugin-auto-import": "^0.11.2",
|
||||||
"vite": "^5.4.19",
|
"vite": "^5.4.19",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-vue-setup-extend": "^0.4.0"
|
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||||
|
"vue-tsc": "^3.3.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+415
-95
File diff suppressed because it is too large
Load Diff
@@ -58,3 +58,14 @@ export const changeStatus = (id, status) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getBfiExchangeRate = (currencyCode, effectdate) => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-transport/bfi/exchange-rate',
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
currencyCode,
|
||||||
|
effectdate,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const approve = data => request({ url: `${baseUrl}/approve`, method: 'pos
|
|||||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||||
export const syncKingdee = id =>
|
export const syncKingdee = id =>
|
||||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
|
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
|
||||||
|
|
||||||
export const invoiceTypeOptions = [
|
export const invoiceTypeOptions = [
|
||||||
{ label: '电子专票', value: 'electronic_special' },
|
{ label: '电子专票', value: 'electronic_special' },
|
||||||
|
|||||||
@@ -5,8 +5,17 @@ const baseUrl = '/blade-transport/invoice-receipt';
|
|||||||
export const getList = (current, size, params) =>
|
export const getList = (current, size, params) =>
|
||||||
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
|
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
|
||||||
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
|
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
|
||||||
export const getInvoicePool = keyword =>
|
export const queryInvoicePool = params =>
|
||||||
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params: { keyword } });
|
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params });
|
||||||
|
export const downloadInvoicePoolAttachment = invoiceNo =>
|
||||||
|
// 金蝶票池发票文件下载(后端代理 A801-2 流式回源),耗时较普通接口长,放宽到 120s
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/invoice-pool/attachment/download`,
|
||||||
|
method: 'get',
|
||||||
|
params: { invoiceNo },
|
||||||
|
responseType: 'blob',
|
||||||
|
timeout: 120000,
|
||||||
|
});
|
||||||
export const getSettlementCandidates = (keyword, receiptId) =>
|
export const getSettlementCandidates = (keyword, receiptId) =>
|
||||||
request({
|
request({
|
||||||
url: `${baseUrl}/settlement-candidates`,
|
url: `${baseUrl}/settlement-candidates`,
|
||||||
@@ -25,8 +34,8 @@ export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post'
|
|||||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||||
export const syncKingdee = id =>
|
export const pushKingdee = ids =>
|
||||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
|
request({ url: `${baseUrl}/push-kingdee`, method: 'post', timeout: 30000, data: ids });
|
||||||
|
|
||||||
export const approvalStatusOptions = [
|
export const approvalStatusOptions = [
|
||||||
{ label: '草稿', value: 'draft' },
|
{ label: '草稿', value: 'draft' },
|
||||||
@@ -36,8 +45,9 @@ export const approvalStatusOptions = [
|
|||||||
{ label: '已作废', value: 'voided' },
|
{ label: '已作废', value: 'voided' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const kingdeeStatusOptions = [
|
export const kingdeePushStatusOptions = [
|
||||||
{ label: '未同步', value: 'unsynced' },
|
{ label: '未推送', value: 'unsynced' },
|
||||||
{ label: '已同步', value: 'synced' },
|
{ label: '已保存收票单', value: 'pushed' },
|
||||||
{ label: '同步失败', value: 'failed' },
|
{ label: '已指定应付', value: 'assigned' },
|
||||||
|
{ label: '推送失败', value: 'failed' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -10,16 +10,28 @@ export const getReferenceAmount = (paymentType, referenceId, excludeId) =>
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
params: { paymentType, referenceId, excludeId: excludeId || undefined },
|
params: { paymentType, referenceId, excludeId: excludeId || undefined },
|
||||||
});
|
});
|
||||||
|
/** 按合同查询可选项目预付核销列表(审批通过且仍有可核销余额) */
|
||||||
|
export const getProjectAdvanceWriteOffOptions = params =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/project-advance-write-off-options`,
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
|
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
|
||||||
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||||
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
||||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||||
|
// 同步金蝶涉及多次金蝶远程接口调用(A103-2 预检 + A103-1/A103-3 推送),单笔可达 20s+,覆盖全局 10s 超时
|
||||||
export const syncKingdee = id =>
|
export const syncKingdee = id =>
|
||||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
|
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
|
||||||
|
// 批量同步逐单单据级事务串行推送,耗时随单量线性增长
|
||||||
export const syncKingdeeBatch = ids =>
|
export const syncKingdeeBatch = ids =>
|
||||||
request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', data: ids });
|
request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', timeout: 30000, data: ids });
|
||||||
|
// 回写任务按 paymentNo 分批远程查询 A103-2/A103-4,同样可能超过全局超时
|
||||||
|
export const syncKingdeeResult = () =>
|
||||||
|
request({ url: `${baseUrl}/sync-kingdee-result`, method: 'post', timeout: 30000 });
|
||||||
|
|
||||||
export const paymentTypeOptions = [
|
export const paymentTypeOptions = [
|
||||||
{ label: '项目预付', value: 'project_advance' },
|
{ label: '项目预付', value: 'project_advance' },
|
||||||
@@ -36,5 +48,8 @@ export const approvalStatusOptions = [
|
|||||||
export const kingdeeStatusOptions = [
|
export const kingdeeStatusOptions = [
|
||||||
{ label: '未生成', value: 'unsynced' },
|
{ label: '未生成', value: 'unsynced' },
|
||||||
{ label: '已生成', value: 'synced' },
|
{ label: '已生成', value: 'synced' },
|
||||||
|
{ label: '付款中', value: 'paying' },
|
||||||
|
{ label: '已付款', value: 'paid' },
|
||||||
|
{ label: '已关闭', value: 'closed' },
|
||||||
{ label: '生成失败', value: 'failed' },
|
{ label: '生成失败', value: 'failed' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -8,14 +8,24 @@ export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get'
|
|||||||
export const updateAttachments = data =>
|
export const updateAttachments = data =>
|
||||||
request({ url: `${baseUrl}/attachments`, method: 'post', data });
|
request({ url: `${baseUrl}/attachments`, method: 'post', data });
|
||||||
export const voidClaim = id => request({ url: `${baseUrl}/void`, method: 'post', params: { id } });
|
export const voidClaim = id => request({ url: `${baseUrl}/void`, method: 'post', params: { id } });
|
||||||
|
export const queryKingdeeResult = () =>
|
||||||
|
request({ url: `${baseUrl}/query-kingdee-result`, method: 'post' });
|
||||||
|
|
||||||
export const claimStatusOptions = [
|
export const claimStatusOptions = [
|
||||||
{ label: '已认领', value: 'claimed' },
|
{ label: '已认领', value: 'claimed' },
|
||||||
{ label: '已作废', value: 'voided' },
|
{ label: '已作废', value: 'voided' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const pushStatusOptions = [
|
||||||
|
{ label: '未推送', value: 'none' },
|
||||||
|
{ label: '已推送', value: 'pushed' },
|
||||||
|
{ label: '推送失败', value: 'failed' },
|
||||||
|
];
|
||||||
|
|
||||||
export const kingdeeBillStatusOptions = [
|
export const kingdeeBillStatusOptions = [
|
||||||
{ label: '未生成', value: 'none' },
|
{ label: '未生成', value: 'none' },
|
||||||
|
{ label: '已推送', value: 'pushed' },
|
||||||
{ label: '审核通过', value: 'approved' },
|
{ label: '审核通过', value: 'approved' },
|
||||||
|
{ label: '已收款', value: 'received' },
|
||||||
{ label: '处理失败', value: 'failed' },
|
{ label: '处理失败', value: 'failed' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -12,10 +12,25 @@ export const getSettlementCandidates = (keyword, flowId) =>
|
|||||||
params: { keyword, flowId },
|
params: { keyword, flowId },
|
||||||
});
|
});
|
||||||
export const claim = data => request({ url: `${baseUrl}/claim`, method: 'post', data });
|
export const claim = data => request({ url: `${baseUrl}/claim`, method: 'post', data });
|
||||||
export const sync = data => request({ url: `${baseUrl}/sync`, method: 'post', data });
|
export const sync = () => request({ url: `${baseUrl}/sync`, method: 'post' });
|
||||||
|
export const pushKingdee = flowId =>
|
||||||
|
request({ url: `${baseUrl}/push-kingdee`, method: 'post', params: { flowId } });
|
||||||
|
|
||||||
export const claimStatusOptions = [
|
export const claimStatusOptions = [
|
||||||
{ label: '未认领', value: 'unclaimed' },
|
{ label: '未认领', value: 'unclaimed' },
|
||||||
{ label: '部分认领', value: 'partial' },
|
{ label: '部分认领', value: 'partial' },
|
||||||
{ label: '认领完成', value: 'claimed' },
|
{ label: '认领完成', value: 'claimed' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const pushStatusOptions = [
|
||||||
|
{ label: '未推送', value: 'none' },
|
||||||
|
{ label: '已推送', value: 'pushed' },
|
||||||
|
{ label: '推送失败', value: 'failed' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const kingdeeBillStatusOptions = [
|
||||||
|
{ label: '已推送', value: 'pushed' },
|
||||||
|
{ label: '审核通过', value: 'approved' },
|
||||||
|
{ label: '已收款', value: 'received' },
|
||||||
|
{ label: '处理失败', value: 'failed' },
|
||||||
|
];
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post'
|
|||||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||||
|
// 同步金蝶涉及多次金蝶远程接口调用(A102-4 预检 + A102-2 保存),可达 20s+,覆盖全局 10s 超时
|
||||||
export const syncKingdee = id =>
|
export const syncKingdee = id =>
|
||||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
|
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
|
||||||
export const getDetailFees = detailId =>
|
export const getDetailFees = detailId =>
|
||||||
request({ url: `${baseUrl}/detail-fees`, method: 'get', params: { detailId } });
|
request({ url: `${baseUrl}/detail-fees`, method: 'get', params: { detailId } });
|
||||||
export const getReceiptClaims = formalSettlementId =>
|
export const getReceiptClaims = formalSettlementId =>
|
||||||
@@ -48,3 +49,5 @@ export const applyPayments = data =>
|
|||||||
request({ url: `${baseUrl}/apply-payments`, method: 'post', data });
|
request({ url: `${baseUrl}/apply-payments`, method: 'post', data });
|
||||||
export const claimInvoices = data =>
|
export const claimInvoices = data =>
|
||||||
request({ url: `${baseUrl}/claim-invoices`, method: 'post', data });
|
request({ url: `${baseUrl}/claim-invoices`, method: 'post', data });
|
||||||
|
export const queryInvoicePool = params =>
|
||||||
|
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params });
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export const getContractList = (current, size, params) =>
|
|||||||
|
|
||||||
export const getFeeOptions = () => request({ url: `${baseUrl}/fee-options`, method: 'get' });
|
export const getFeeOptions = () => request({ url: `${baseUrl}/fee-options`, method: 'get' });
|
||||||
|
|
||||||
|
export const getNextNo = () => request({ url: `${baseUrl}/next-no`, method: 'get' });
|
||||||
|
|
||||||
export const getCandidateDetails = (current, size, params) =>
|
export const getCandidateDetails = (current, size, params) =>
|
||||||
request({
|
request({
|
||||||
url: `${baseUrl}/candidate-details`,
|
url: `${baseUrl}/candidate-details`,
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import request from '@/axios';
|
||||||
|
|
||||||
|
const baseUrl = '/blade-transport/settlement-import';
|
||||||
|
|
||||||
|
export const getOptions = () => request({ url: `${baseUrl}/options`, method: 'get' });
|
||||||
|
|
||||||
|
export const getNextCode = () => request({ url: `${baseUrl}/next-code`, method: 'get' });
|
||||||
|
|
||||||
|
export const getList = (current, size, params) =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/list`,
|
||||||
|
method: 'get',
|
||||||
|
params: { current, size, ...params },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getDetail = id =>
|
||||||
|
request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
|
||||||
|
|
||||||
|
export const exportTemplate = () =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/export-template`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const parseFile = file => {
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('file', file);
|
||||||
|
return request({
|
||||||
|
url: `${baseUrl}/parse`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
timeout: 120000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validate = data =>
|
||||||
|
request({ url: `${baseUrl}/validate`, method: 'post', data, timeout: 120000 });
|
||||||
|
|
||||||
|
export const exportFailures = data =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/export-failures`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
responseType: 'blob',
|
||||||
|
timeout: 120000,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const saveDraft = data =>
|
||||||
|
request({ url: `${baseUrl}/draft`, method: 'post', data, timeout: 120000 });
|
||||||
|
|
||||||
|
export const confirm = data =>
|
||||||
|
request({ url: `${baseUrl}/confirm`, method: 'post', data, timeout: 120000 });
|
||||||
|
|
||||||
|
export const voidBatch = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||||
@@ -152,6 +152,28 @@ export const getScoreTemplate = quantificationId => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const queryKingdeeCustomer = creditCode => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-transport/bfi/customer/query-by-credit-code',
|
||||||
|
method: 'get',
|
||||||
|
timeout: 30000,
|
||||||
|
params: {
|
||||||
|
creditCode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const syncKingdee = id => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-transport/customer-archive/sync-kingdee',
|
||||||
|
method: 'post',
|
||||||
|
timeout: 30000,
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const recognizeBusinessLicenseOcr = imageUrl => {
|
export const recognizeBusinessLicenseOcr = imageUrl => {
|
||||||
return request({
|
return request({
|
||||||
url: '/blade-transport/baidu-ocr/recognize-url',
|
url: '/blade-transport/baidu-ocr/recognize-url',
|
||||||
|
|||||||
+9
-1
@@ -271,10 +271,18 @@ axios.interceptors.response.use(
|
|||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
|
// 网络级失败(HTTP 状态 >500、网关错误、超时、断网等 validateStatus 放行之外的响应)
|
||||||
|
// 原为静默 reject,接口报错页面无任何感知;统一补错误提示,取消请求与刷新令牌请求除外
|
||||||
|
if (!axios.isCancel(error) && !isRefreshTokenRequest(error?.config)) {
|
||||||
|
ElMessage({
|
||||||
|
message: error?.response?.data?.msg || '请求失败,请稍后重试',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(error));
|
||||||
// 保留原始 axios 错误对象:下游(如导入失败明细解析)需要 error.response
|
// 保留原始 axios 错误对象:下游(如导入失败明细解析)需要 error.response
|
||||||
// 读取响应体。若包装成 new Error(error),response 会丢失、message 退化成
|
// 读取响应体。若包装成 new Error(error),response 会丢失、message 退化成
|
||||||
// "[object Object]",用户只能看到无意义的通用提示。
|
// "[object Object]",用户只能看到无意义的通用提示。
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,434 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visible" :title="title" width="92%" append-to-body destroy-on-close @open="handleOpen">
|
||||||
|
<el-form :model="query" inline label-width="130px" @submit.prevent>
|
||||||
|
<el-form-item label="发票号码">
|
||||||
|
<el-input v-model="query.invoiceNo" clearable @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="开票日期">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="query.invoiceDateRange"
|
||||||
|
type="daterange"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="开票单位">
|
||||||
|
<el-input v-model="query.salerName" clearable @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="受票单位">
|
||||||
|
<el-input v-model="query.buyerName" clearable @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="开票单位(销方)">
|
||||||
|
<el-select
|
||||||
|
v-model="query.salerTaxNo"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
reserve-keyword
|
||||||
|
:remote-method="keyword => searchCustomers(keyword, 'saler')"
|
||||||
|
:loading="customerLoading.saler"
|
||||||
|
placeholder="输入名称搜索或直接粘贴税号"
|
||||||
|
style="width: 220px"
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in customerOptions.saler"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="受票单位(购方)">
|
||||||
|
<el-select
|
||||||
|
v-model="query.buyerTaxNo"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
reserve-keyword
|
||||||
|
:remote-method="keyword => searchCustomers(keyword, 'buyer')"
|
||||||
|
:loading="customerLoading.buyer"
|
||||||
|
placeholder="输入名称搜索或直接粘贴税号"
|
||||||
|
style="width: 220px"
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in customerOptions.buyer"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="发票类型">
|
||||||
|
<el-select
|
||||||
|
v-model="query.invoiceType"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
placeholder="不限"
|
||||||
|
style="width: 230px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="option in invoiceTypeOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="认领状态">
|
||||||
|
<el-select v-model="query.claimed" clearable placeholder="不限" style="width: 120px">
|
||||||
|
<el-option label="未认领" :value="false" />
|
||||||
|
<el-option label="已认领" :value="true" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="登记状态">
|
||||||
|
<el-select v-model="query.registered" clearable placeholder="不限" style="width: 120px">
|
||||||
|
<el-option label="未登记" :value="false" />
|
||||||
|
<el-option label="已登记" :value="true" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button :disabled="loading" @click="resetQuery">重置</el-button>
|
||||||
|
<el-button type="primary" :disabled="loading" @click="handleQuery">查询</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-table
|
||||||
|
ref="poolTable"
|
||||||
|
v-loading="loading"
|
||||||
|
:data="rows"
|
||||||
|
:row-key="rowKey"
|
||||||
|
border
|
||||||
|
height="520"
|
||||||
|
:highlight-current-row="!multiple"
|
||||||
|
@selection-change="selection => (selected = selection)"
|
||||||
|
@row-dblclick="handleRowDblclick"
|
||||||
|
>
|
||||||
|
<el-table-column v-if="multiple" type="selection" width="52" fixed="left" align="center" reserve-selection />
|
||||||
|
<el-table-column v-else width="52" align="center" fixed="left">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-radio
|
||||||
|
:model-value="current && current.invoiceNo"
|
||||||
|
:value="row.invoiceNo"
|
||||||
|
@change="current = row"
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</el-radio>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="invoiceNo" label="发票号码" min-width="180" />
|
||||||
|
<el-table-column prop="invoiceDate" label="开票日期" min-width="120" />
|
||||||
|
<el-table-column prop="invoiceType" label="发票类型" min-width="140" />
|
||||||
|
<el-table-column prop="issuerName" label="开票单位" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="receiverName" label="受票单位" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column label="金额(含税)" min-width="130" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="税率" min-width="90" align="center">
|
||||||
|
<template #default="{ row }">{{ formatRate(row.taxRate) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="税额" min-width="120" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="剩余可用金额" min-width="130" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.availableTaxAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<!--<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="210" show-overflow-tooltip />-->
|
||||||
|
<el-table-column label="认领状态" min-width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.claimed" type="danger" size="small">已认领</el-tag>
|
||||||
|
<el-tag v-else type="info" size="small">未认领</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="登记状态" min-width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.registered" type="warning" size="small">已登记</el-tag>
|
||||||
|
<el-tag v-else type="info" size="small">未登记</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="附件" width="120" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link
|
||||||
|
type="primary"
|
||||||
|
:underline="false"
|
||||||
|
:disabled="attachmentBusy === row.invoiceNo"
|
||||||
|
@click.stop="viewAttachment(row)"
|
||||||
|
>查看</el-link
|
||||||
|
>
|
||||||
|
<el-link
|
||||||
|
type="primary"
|
||||||
|
:underline="false"
|
||||||
|
:disabled="attachmentBusy === row.invoiceNo"
|
||||||
|
@click.stop="downloadAttachment(row)"
|
||||||
|
>下载</el-link
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column v-if="!multiple" label="操作" width="90" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link type="primary" @click.stop="confirmRows([row])">选择</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="kingdee-pool-dialog__pager">
|
||||||
|
<span class="kingdee-pool-dialog__pager-tip">按名称检索范围为已同步票,未找到请改用发票号码或税号精确查找</span>
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="page.current"
|
||||||
|
v-model:page-size="page.size"
|
||||||
|
:total="page.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:disabled="loading"
|
||||||
|
@current-change="load"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleConfirm">{{ confirmText }}</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from '@/axios';
|
||||||
|
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
|
||||||
|
import {
|
||||||
|
downloadKingdeeInvoiceFile,
|
||||||
|
isKingdeePathAttachment,
|
||||||
|
viewKingdeeInvoiceFile,
|
||||||
|
} from '@/utils/kingdee-attachment';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'KingdeeInvoicePoolDialog',
|
||||||
|
props: {
|
||||||
|
modelValue: { type: Boolean, required: true },
|
||||||
|
title: { type: String, default: '查询金蝶票据池' },
|
||||||
|
confirmText: { type: String, default: '确定' },
|
||||||
|
multiple: { type: Boolean, default: false },
|
||||||
|
initialInvoiceNo: { type: String, default: '' },
|
||||||
|
queryApi: { type: Function, required: true },
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue', 'confirm'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
// 与后端 BfiConstant.INVOICE_TYPE_NAMES 对齐:value 为发票类型编码(镜像按 invoice_type_code 精确匹配)
|
||||||
|
// 选票场景只开放数电票(普票/专票)
|
||||||
|
invoiceTypeOptions: [
|
||||||
|
{ value: '26', label: '数电发票(普通发票)' },
|
||||||
|
{ value: '27', label: '数电发票(增值税专用发票)' },
|
||||||
|
],
|
||||||
|
rows: [],
|
||||||
|
selected: [],
|
||||||
|
current: null,
|
||||||
|
page: { current: 1, size: 10, total: 0 },
|
||||||
|
// 正在查看/下载附件的发票号(行级 loading 守卫)
|
||||||
|
attachmentBusy: null,
|
||||||
|
// 销/购方客商下拉:远程按名称搜客商档案,选中带出税号查询;手动输入的裸税号直接作为值过滤
|
||||||
|
customerOptions: { saler: [], buyer: [] },
|
||||||
|
customerLoading: { saler: false, buyer: false },
|
||||||
|
query: this.emptyQuery(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
visible: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('update:modelValue', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
emptyQuery() {
|
||||||
|
return {
|
||||||
|
invoiceNo: '',
|
||||||
|
// 默认查近一个月,随请求传后端并在页面回显
|
||||||
|
invoiceDateRange: [this.$dayjs().subtract(1, 'month').format('YYYY-MM-DD'), this.$dayjs().format('YYYY-MM-DD')],
|
||||||
|
salerName: '',
|
||||||
|
buyerName: '',
|
||||||
|
salerTaxNo: '',
|
||||||
|
buyerTaxNo: '',
|
||||||
|
invoiceType: '',
|
||||||
|
claimed: null,
|
||||||
|
registered: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// 镜像行主键为 id,回源入库前的响应仍可能无 id,用发票号码兜底
|
||||||
|
rowKey(row) {
|
||||||
|
return row.id || row.invoiceNo;
|
||||||
|
},
|
||||||
|
// 票池行附件字段为复数 attachmentsJson,行内首项即发票文件({source,path,name})
|
||||||
|
rowAttachment(row = {}) {
|
||||||
|
const value = row.attachmentsJson;
|
||||||
|
if (!value) return null;
|
||||||
|
let parsed = value;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.isArray(parsed) ? parsed[0] || null : parsed || null;
|
||||||
|
},
|
||||||
|
attachmentName(row) {
|
||||||
|
const attachment = this.rowAttachment(row);
|
||||||
|
return attachment?.name || `${row.invoiceNo || '发票'}.pdf`;
|
||||||
|
},
|
||||||
|
async viewAttachment(row) {
|
||||||
|
const attachment = this.rowAttachment(row);
|
||||||
|
if (!isKingdeePathAttachment(attachment)) {
|
||||||
|
this.$message.warning('当前发票暂无可查看附件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.attachmentBusy = row.invoiceNo;
|
||||||
|
try {
|
||||||
|
await viewKingdeeInvoiceFile(row.invoiceNo, this.attachmentName(row));
|
||||||
|
} finally {
|
||||||
|
this.attachmentBusy = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async downloadAttachment(row) {
|
||||||
|
const attachment = this.rowAttachment(row);
|
||||||
|
if (!isKingdeePathAttachment(attachment)) {
|
||||||
|
this.$message.warning('当前发票暂无可下载附件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.attachmentBusy = row.invoiceNo;
|
||||||
|
try {
|
||||||
|
await downloadKingdeeInvoiceFile(row.invoiceNo, this.attachmentName(row));
|
||||||
|
} finally {
|
||||||
|
this.attachmentBusy = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleOpen() {
|
||||||
|
this.query = this.emptyQuery();
|
||||||
|
this.query.invoiceNo = this.initialInvoiceNo || '';
|
||||||
|
this.current = null;
|
||||||
|
this.selected = [];
|
||||||
|
this.$refs.poolTable?.clearSelection();
|
||||||
|
this.page.current = 1;
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
handleQuery() {
|
||||||
|
this.page.current = 1;
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
handleSizeChange() {
|
||||||
|
this.page.current = 1;
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.query = this.emptyQuery();
|
||||||
|
this.handleQuery();
|
||||||
|
},
|
||||||
|
// 客商名称远程搜索:含 18 位社会信用代码特征(数字+大写字母)时按裸税号直查;
|
||||||
|
// 否则搜客商档案,取客商发票信息(blade_customer_invoice_info)的抬头/税号作选项——票池税号是开票税号,非统一信用代码
|
||||||
|
async searchCustomers(keyword, side) {
|
||||||
|
const text = String(keyword || '').trim();
|
||||||
|
if (!text) return;
|
||||||
|
if (/^[0-9A-Z]{15,20}$/.test(text)) {
|
||||||
|
this.customerOptions[side] = [{ label: `${text}(手输税号)`, value: text }];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.customerLoading[side] = true;
|
||||||
|
try {
|
||||||
|
const data = this.unwrap(await getCustomerList(1, 20, { fullName: text }));
|
||||||
|
const customers = (data.records || []).filter(item => item.id);
|
||||||
|
if (!customers.length) {
|
||||||
|
this.customerOptions[side] = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 列表接口不带发票信息,逐户取 detail(detail 组装 invoices,每户一张发票信息)
|
||||||
|
const options = await Promise.all(
|
||||||
|
customers.map(async customer => {
|
||||||
|
const detail = this.unwrap(await request({ url: '/blade-transport/customer-archive/detail', method: 'get', params: { id: customer.id } }));
|
||||||
|
return (detail?.invoices || [])
|
||||||
|
.filter(invoice => invoice.taxNo)
|
||||||
|
.map(invoice => ({ label: `${invoice.invoiceTitle || customer.fullName}(${invoice.taxNo})`, value: invoice.taxNo }));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.customerOptions[side] = options.flat().filter(
|
||||||
|
(option, index, list) => list.findIndex(item => item.value === option.value) === index
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.customerLoading[side] = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unwrap(response) {
|
||||||
|
const body = response?.data || response || {};
|
||||||
|
return body?.data || body;
|
||||||
|
},
|
||||||
|
async load() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const range = this.query.invoiceDateRange || [];
|
||||||
|
const data = this.unwrap(
|
||||||
|
await this.queryApi({
|
||||||
|
current: this.page.current,
|
||||||
|
size: this.page.size,
|
||||||
|
invoiceNo: this.query.invoiceNo || undefined,
|
||||||
|
invoiceDateStart: range[0] || undefined,
|
||||||
|
invoiceDateEnd: range[1] || undefined,
|
||||||
|
salerName: this.query.salerName || undefined,
|
||||||
|
buyerName: this.query.buyerName || undefined,
|
||||||
|
salerTaxNo: this.query.salerTaxNo || undefined,
|
||||||
|
buyerTaxNo: this.query.buyerTaxNo || undefined,
|
||||||
|
invoiceType: this.query.invoiceType || undefined,
|
||||||
|
claimed: this.query.claimed ?? undefined,
|
||||||
|
registered: this.query.registered ?? undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.rows = data.records || [];
|
||||||
|
this.page.total = Number(data.total || 0);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleRowDblclick(row) {
|
||||||
|
if (!this.multiple) {
|
||||||
|
this.confirmRows([row]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleConfirm() {
|
||||||
|
const picked = this.multiple ? this.selected : this.current ? [this.current] : [];
|
||||||
|
this.confirmRows(picked);
|
||||||
|
},
|
||||||
|
confirmRows(rows) {
|
||||||
|
if (!rows.length) {
|
||||||
|
this.$message.warning(this.multiple ? '请至少选择一条发票' : '请选择一张发票');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$emit('confirm', rows);
|
||||||
|
this.visible = false;
|
||||||
|
},
|
||||||
|
formatMoney(value) {
|
||||||
|
const number = Number(value);
|
||||||
|
return Number.isFinite(number) ? number.toFixed(2) : '-';
|
||||||
|
},
|
||||||
|
formatRate(value) {
|
||||||
|
const number = Number(value);
|
||||||
|
return Number.isFinite(number) ? `${number}%` : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.kingdee-pool-dialog__pager {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 12px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.kingdee-pool-dialog__pager-tip {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -43,9 +43,23 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
||||||
<el-table-column prop="uploadTime" label="上传时间" width="180" align="center" sortable />
|
<el-table-column prop="uploadTime" label="上传时间" width="180" align="center" sortable />
|
||||||
<el-table-column v-if="!readonly" label="操作" width="100" align="center" fixed="right">
|
<el-table-column
|
||||||
<template #default="{ $index }">
|
v-if="!readonly || showViewActions"
|
||||||
<el-link type="danger" @click="removeAttachment($index)">删除</el-link>
|
label="操作"
|
||||||
|
:width="readonly ? 120 : 170"
|
||||||
|
align="center"
|
||||||
|
fixed="right"
|
||||||
|
>
|
||||||
|
<template #default="{ row, $index }">
|
||||||
|
<div class="vehicle-attachment-table__actions">
|
||||||
|
<el-link type="primary" :disabled="kingdeeBusy" @click="previewAttachment(row)"
|
||||||
|
>查看</el-link
|
||||||
|
>
|
||||||
|
<el-link type="primary" :disabled="kingdeeBusy" @click="downloadAttachment(row)"
|
||||||
|
>下载</el-link
|
||||||
|
>
|
||||||
|
<el-link v-if="!readonly" type="danger" @click="removeAttachment($index)">删除</el-link>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -71,6 +85,7 @@
|
|||||||
width="90%"
|
width="90%"
|
||||||
top="4vh"
|
top="4vh"
|
||||||
class="vehicle-attachment-table__viewer-dialog"
|
class="vehicle-attachment-table__viewer-dialog"
|
||||||
|
@closed="handlePreviewClosed"
|
||||||
>
|
>
|
||||||
<pdf-preview
|
<pdf-preview
|
||||||
v-if="documentPreviewVisible && previewFile.url && previewFile.isPdf"
|
v-if="documentPreviewVisible && previewFile.url && previewFile.isPdf"
|
||||||
@@ -111,6 +126,11 @@ import '@open-file-viewer/core/style.css';
|
|||||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||||
import PdfPreview from '@/components/pdf-preview/main.vue';
|
import PdfPreview from '@/components/pdf-preview/main.vue';
|
||||||
import { downloadFileByUrl } from '@/utils/util';
|
import { downloadFileByUrl } from '@/utils/util';
|
||||||
|
import {
|
||||||
|
downloadKingdeeInvoiceFile,
|
||||||
|
isKingdeePathAttachment,
|
||||||
|
requestKingdeeFile,
|
||||||
|
} from '@/utils/kingdee-attachment';
|
||||||
|
|
||||||
const attachmentFileTypes = [
|
const attachmentFileTypes = [
|
||||||
'pdf',
|
'pdf',
|
||||||
@@ -156,6 +176,16 @@ export default {
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 500,
|
default: 500,
|
||||||
},
|
},
|
||||||
|
// 金蝶侧附件(piaozone path 型)代理下载所依赖的发票号;行内自带 invoiceNo 时优先用行内值
|
||||||
|
kingdeeInvoiceNo: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
// 查看态也展示「查看/下载」操作列(编辑态始终展示,含删除)
|
||||||
|
showViewActions: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
data() {
|
data() {
|
||||||
@@ -167,6 +197,10 @@ export default {
|
|||||||
imagePreviewIndex: 0,
|
imagePreviewIndex: 0,
|
||||||
documentPreviewVisible: false,
|
documentPreviewVisible: false,
|
||||||
previewFile: {},
|
previewFile: {},
|
||||||
|
// 金蝶侧附件代理请求进行中(行级按钮共享 loading 守卫,防重复触发)
|
||||||
|
kingdeeBusy: false,
|
||||||
|
// 金蝶附件预览用的 objectURL,弹窗关闭时回收
|
||||||
|
kingdeeObjectUrl: '',
|
||||||
viewerPlugins,
|
viewerPlugins,
|
||||||
viewerToolbar: {
|
viewerToolbar: {
|
||||||
download: true,
|
download: true,
|
||||||
@@ -201,6 +235,12 @@ export default {
|
|||||||
attachmentUrl(row = {}) {
|
attachmentUrl(row = {}) {
|
||||||
return row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || row.src || '';
|
return row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || row.src || '';
|
||||||
},
|
},
|
||||||
|
kingdeeInvoiceNoOf(row = {}) {
|
||||||
|
return row.invoiceNo || this.kingdeeInvoiceNo || '';
|
||||||
|
},
|
||||||
|
isKingdeeRow(row = {}) {
|
||||||
|
return isKingdeePathAttachment(row) && Boolean(this.kingdeeInvoiceNoOf(row));
|
||||||
|
},
|
||||||
attachmentExtension(row = {}) {
|
attachmentExtension(row = {}) {
|
||||||
const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0];
|
const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0];
|
||||||
const index = source.lastIndexOf('.');
|
const index = source.lastIndexOf('.');
|
||||||
@@ -248,6 +288,10 @@ export default {
|
|||||||
return `${(value / 1024 / 1024).toFixed(1)}MB`;
|
return `${(value / 1024 / 1024).toFixed(1)}MB`;
|
||||||
},
|
},
|
||||||
downloadAttachment(row) {
|
downloadAttachment(row) {
|
||||||
|
// 金蝶侧附件(piaozone path 型)经后端代理 A801-2 下载,OSS 直链维持前端直连下载
|
||||||
|
if (this.isKingdeeRow(row)) {
|
||||||
|
return this.downloadKingdeeAttachment(row);
|
||||||
|
}
|
||||||
const url = this.attachmentUrl(row);
|
const url = this.attachmentUrl(row);
|
||||||
if (!url) {
|
if (!url) {
|
||||||
this.$message.warning('附件地址为空,无法下载');
|
this.$message.warning('附件地址为空,无法下载');
|
||||||
@@ -255,20 +299,47 @@ export default {
|
|||||||
}
|
}
|
||||||
downloadFileByUrl(url, this.attachmentName(row));
|
downloadFileByUrl(url, this.attachmentName(row));
|
||||||
},
|
},
|
||||||
|
async downloadKingdeeAttachment(row) {
|
||||||
|
if (this.kingdeeBusy) return;
|
||||||
|
this.kingdeeBusy = true;
|
||||||
|
try {
|
||||||
|
await downloadKingdeeInvoiceFile(this.kingdeeInvoiceNoOf(row), this.attachmentName(row));
|
||||||
|
} finally {
|
||||||
|
this.kingdeeBusy = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
batchDownload() {
|
batchDownload() {
|
||||||
const rows = this.selectedRows.length ? this.selectedRows : this.rows;
|
const rows = this.selectedRows.length ? this.selectedRows : this.rows;
|
||||||
const downloadableRows = rows.filter(row => this.attachmentUrl(row));
|
const kingdeeRows = rows.filter(row => this.isKingdeeRow(row));
|
||||||
if (!downloadableRows.length) {
|
const urlRows = rows.filter(row => !this.isKingdeeRow(row) && this.attachmentUrl(row));
|
||||||
|
if (!kingdeeRows.length && !urlRows.length) {
|
||||||
this.$message.warning(
|
this.$message.warning(
|
||||||
this.selectedRows.length ? '所选附件暂无可下载地址' : '暂无可下载附件'
|
this.selectedRows.length ? '所选附件暂无可下载地址' : '暂无可下载附件'
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
downloadableRows.forEach((row, index) => {
|
urlRows.forEach((row, index) => {
|
||||||
window.setTimeout(() => this.downloadAttachment(row), index * 500);
|
window.setTimeout(() => this.downloadAttachment(row), index * 500);
|
||||||
});
|
});
|
||||||
|
// 金蝶侧附件走代理接口,串行触发避免并发重复回源
|
||||||
|
this.downloadKingdeeRows(kingdeeRows);
|
||||||
|
},
|
||||||
|
async downloadKingdeeRows(rows) {
|
||||||
|
if (this.kingdeeBusy) return;
|
||||||
|
this.kingdeeBusy = true;
|
||||||
|
try {
|
||||||
|
for (const row of rows) {
|
||||||
|
await downloadKingdeeInvoiceFile(this.kingdeeInvoiceNoOf(row), this.attachmentName(row));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.kingdeeBusy = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
previewAttachment(row) {
|
previewAttachment(row) {
|
||||||
|
// 金蝶侧附件经 A801-2 代理取流后进弹窗预览,OSS 直链维持原逻辑
|
||||||
|
if (this.isKingdeeRow(row)) {
|
||||||
|
return this.previewKingdeeAttachment(row);
|
||||||
|
}
|
||||||
const url = this.attachmentUrl(row);
|
const url = this.attachmentUrl(row);
|
||||||
if (!url) {
|
if (!url) {
|
||||||
this.$message.warning('附件地址为空,无法预览');
|
this.$message.warning('附件地址为空,无法预览');
|
||||||
@@ -291,6 +362,42 @@ export default {
|
|||||||
};
|
};
|
||||||
this.documentPreviewVisible = true;
|
this.documentPreviewVisible = true;
|
||||||
},
|
},
|
||||||
|
async previewKingdeeAttachment(row) {
|
||||||
|
// OFD 版式文件浏览器无法在线渲染,直接提示下载,避免拉流后预览失败
|
||||||
|
if (this.attachmentExtension(row) === 'ofd') {
|
||||||
|
this.$message.warning('OFD 版式文件不支持在线预览,请使用下载获取后查看');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.kingdeeBusy) return;
|
||||||
|
this.kingdeeBusy = true;
|
||||||
|
try {
|
||||||
|
const blob = await requestKingdeeFile(this.kingdeeInvoiceNoOf(row));
|
||||||
|
this.releaseKingdeeObjectUrl();
|
||||||
|
const isPdf = this.isPdf(row) || String(blob.type || '').includes('application/pdf');
|
||||||
|
this.kingdeeObjectUrl = URL.createObjectURL(blob);
|
||||||
|
this.previewFile = {
|
||||||
|
name: this.attachmentName(row),
|
||||||
|
url: this.kingdeeObjectUrl,
|
||||||
|
mimeType: isPdf ? 'application/pdf' : blob.type || '',
|
||||||
|
isPdf,
|
||||||
|
};
|
||||||
|
this.documentPreviewVisible = true;
|
||||||
|
} catch (error) {
|
||||||
|
if (!error?.silent) this.$message.error(error?.message || '附件预览失败');
|
||||||
|
} finally {
|
||||||
|
this.kingdeeBusy = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handlePreviewClosed() {
|
||||||
|
this.releaseKingdeeObjectUrl();
|
||||||
|
this.previewFile = {};
|
||||||
|
},
|
||||||
|
releaseKingdeeObjectUrl() {
|
||||||
|
if (this.kingdeeObjectUrl) {
|
||||||
|
URL.revokeObjectURL(this.kingdeeObjectUrl);
|
||||||
|
this.kingdeeObjectUrl = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
handlePreviewUnsupported() {
|
handlePreviewUnsupported() {
|
||||||
this.$message.warning('当前文件暂不支持在线预览');
|
this.$message.warning('当前文件暂不支持在线预览');
|
||||||
},
|
},
|
||||||
@@ -311,6 +418,27 @@ export default {
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__file-name {
|
||||||
|
color: #409eff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__file-name:hover,
|
||||||
|
&__file-name:focus-visible {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
:deep(.el-link) {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__upload {
|
&__upload {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export const createOption = () => ({
|
|||||||
delBtn: false,
|
delBtn: false,
|
||||||
selection: true,
|
selection: true,
|
||||||
dialogClickModal: false,
|
dialogClickModal: false,
|
||||||
menuWidth: 240,
|
|
||||||
menuFixed: 'right',
|
menuFixed: 'right',
|
||||||
column: [
|
column: [
|
||||||
{
|
{
|
||||||
@@ -112,6 +111,7 @@ export const createOption = () => ({
|
|||||||
{
|
{
|
||||||
label: '创建人',
|
label: '创建人',
|
||||||
prop: 'createUserName',
|
prop: 'createUserName',
|
||||||
|
minWidth: 110,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
viewDisplay: false,
|
viewDisplay: false,
|
||||||
@@ -120,6 +120,7 @@ export const createOption = () => ({
|
|||||||
{
|
{
|
||||||
label: '更新人',
|
label: '更新人',
|
||||||
prop: 'updateUserName',
|
prop: 'updateUserName',
|
||||||
|
minWidth: 110,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
viewDisplay: false,
|
viewDisplay: false,
|
||||||
@@ -131,6 +132,7 @@ export const createOption = () => ({
|
|||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
format: 'YYYY-MM-DD HH:mm:ss',
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
width: 180,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
@@ -142,6 +144,7 @@ export const createOption = () => ({
|
|||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
format: 'YYYY-MM-DD HH:mm:ss',
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
width: 180,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export const option = createCrudOption([
|
|||||||
searchPlaceholder: '请输入',
|
searchPlaceholder: '请输入',
|
||||||
searchOrder: 8,
|
searchOrder: 8,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
rules: textRule('平台公司', 50, true),
|
rules: textRule('平台公司', 500, true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '平台公司ID',
|
label: '平台公司ID',
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export const invoiceApplicationTableColumns = [
|
|||||||
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
|
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
|
||||||
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
|
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
|
||||||
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
|
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
|
||||||
|
{ prop: 'kingdeeLockStatus', label: '金蝶锁状态', minWidth: 150 },
|
||||||
|
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const invoiceApplicationDetailColumns = [
|
export const invoiceApplicationDetailColumns = [
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export const invoiceReceiptTableColumns = [
|
|||||||
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
|
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
|
||||||
{ prop: 'createUserName', label: '创建人', minWidth: 110 },
|
{ prop: 'createUserName', label: '创建人', minWidth: 110 },
|
||||||
{ prop: 'createTime', label: '创建时间', minWidth: 170 },
|
{ prop: 'createTime', label: '创建时间', minWidth: 170 },
|
||||||
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
|
{ prop: 'kingdeeReceivedInvoiceNo', label: '金蝶单据号', minWidth: 150 },
|
||||||
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
|
{ prop: 'kingdeePushStatusName', label: '金蝶状态', minWidth: 130 },
|
||||||
|
{ prop: 'kingdeePushMsg', label: '失败原因', minWidth: 160 },
|
||||||
|
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -161,5 +161,7 @@ export const paymentApplicationOption = ({
|
|||||||
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 120, formatter: textFormatter },
|
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 120, formatter: textFormatter },
|
||||||
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 130, formatter: textFormatter },
|
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 130, formatter: textFormatter },
|
||||||
{ label: '金蝶单据状态', prop: 'kingdeeStatusName', minWidth: 130, formatter: textFormatter },
|
{ label: '金蝶单据状态', prop: 'kingdeeStatusName', minWidth: 130, formatter: textFormatter },
|
||||||
|
{ label: '金蝶锁状态', prop: 'kingdeeLockStatus', slot: true, minWidth: 150 },
|
||||||
|
{ label: '附件同步', prop: 'attachmentSyncStatusName', minWidth: 100, formatter: textFormatter },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,5 +8,11 @@ export const receiptFlowTableColumns = [
|
|||||||
{ prop: 'summary', label: '摘要', minWidth: 160 },
|
{ prop: 'summary', label: '摘要', minWidth: 160 },
|
||||||
{ prop: 'transactionTime', label: '交易时间', minWidth: 170 },
|
{ prop: 'transactionTime', label: '交易时间', minWidth: 170 },
|
||||||
{ prop: 'claimStatusName', label: '认领状态', minWidth: 110, status: true },
|
{ prop: 'claimStatusName', label: '认领状态', minWidth: 110, status: true },
|
||||||
|
{ prop: 'kingdeeClaimStatusName', label: '公告状态', minWidth: 110 },
|
||||||
{ prop: 'claimedAmount', label: '已认领金额', minWidth: 130, money: true },
|
{ prop: 'claimedAmount', label: '已认领金额', minWidth: 130, money: true },
|
||||||
|
{ prop: 'unclaimedAmount', label: '金蝶未认领金额', minWidth: 140, money: true },
|
||||||
|
{ prop: 'pushStatusName', label: '推送状态', minWidth: 110, status: true },
|
||||||
|
{ prop: 'pushFailReason', label: '推送失败原因', minWidth: 160 },
|
||||||
|
{ prop: 'kingdeeBillNo', label: '金蝶收款单号', minWidth: 150 },
|
||||||
|
{ prop: 'kingdeeBillStatusName', label: '金蝶状态', minWidth: 110 },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export const formalSettlementTableColumns = [
|
|||||||
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
|
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
|
||||||
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 },
|
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 },
|
||||||
{ label: '金蝶单据状态', prop: 'kingdeeSyncStatus', minWidth: 135 },
|
{ label: '金蝶单据状态', prop: 'kingdeeSyncStatus', minWidth: 135 },
|
||||||
|
{ label: '金蝶锁状态', prop: 'kingdeeLockStatus', minWidth: 150 },
|
||||||
|
{ label: '附件同步', prop: 'attachmentSyncStatusName', minWidth: 100 },
|
||||||
{ label: '创建人', prop: 'createUserName', minWidth: 110 },
|
{ label: '创建人', prop: 'createUserName', minWidth: 110 },
|
||||||
{ label: '创建时间', prop: 'createTime', minWidth: 170 },
|
{ label: '创建时间', prop: 'createTime', minWidth: 170 },
|
||||||
];
|
];
|
||||||
@@ -33,8 +35,8 @@ export const sourceColumns = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const detailColumns = [
|
export const detailColumns = [
|
||||||
{ label: '单据号', prop: 'documentNo', minWidth: 180 },
|
{ label: '单据号', prop: 'documentNo', minWidth: 180, link: true },
|
||||||
{ label: '运单号', prop: 'waybillNo', minWidth: 160 },
|
{ label: '运单号', prop: 'waybillNo', minWidth: 160, link: true },
|
||||||
{ label: '车号', prop: 'vehicleNo', minWidth: 130 },
|
{ label: '车号', prop: 'vehicleNo', minWidth: 130 },
|
||||||
{ label: '发货地址', prop: 'departureAddress', minWidth: 180 },
|
{ label: '发货地址', prop: 'departureAddress', minWidth: 180 },
|
||||||
{ label: '到货地址', prop: 'arrivalAddress', minWidth: 180 },
|
{ label: '到货地址', prop: 'arrivalAddress', minWidth: 180 },
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const settlementSummaryColumns = [
|
|||||||
|
|
||||||
export const settlementDetailColumns = [
|
export const settlementDetailColumns = [
|
||||||
{ label: '单据号', prop: 'documentNo', minWidth: 180, link: true },
|
{ label: '单据号', prop: 'documentNo', minWidth: 180, link: true },
|
||||||
{ label: '运单号', prop: 'waybillNo', minWidth: 160 },
|
{ label: '运单号', prop: 'waybillNo', minWidth: 160, link: true },
|
||||||
{ label: '车号', prop: 'vehicleNo', minWidth: 130 },
|
{ label: '车号', prop: 'vehicleNo', minWidth: 130 },
|
||||||
{ label: '发货地址', prop: 'departureAddress', minWidth: 180 },
|
{ label: '发货地址', prop: 'departureAddress', minWidth: 180 },
|
||||||
{ label: '到货地址', prop: 'arrivalAddress', minWidth: 180 },
|
{ label: '到货地址', prop: 'arrivalAddress', minWidth: 180 },
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ export const updateFeeFormFields = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const transferSearchFields = [
|
export const transferSearchFields = [
|
||||||
|
{ label: '单据号', prop: 'documentNo', type: 'input' },
|
||||||
|
{ label: '合同编码', prop: 'contractNo', type: 'input' },
|
||||||
{ label: '合同名称', prop: 'contractName', type: 'input' },
|
{ label: '合同名称', prop: 'contractName', type: 'input' },
|
||||||
{ label: '批次号', prop: 'batchNo', type: 'input' },
|
{ label: '批次号', prop: 'batchNo', type: 'input' },
|
||||||
{ label: '生成日期', prop: 'generateDateRange', type: 'daterange' },
|
{ label: '生成日期', prop: 'generateDateRange', type: 'daterange' },
|
||||||
|
|||||||
@@ -119,6 +119,21 @@ export default [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/settlement/settlement-import/form',
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: '新增导入结算单',
|
||||||
|
meta: {
|
||||||
|
keepAlive: false,
|
||||||
|
activeMenu: '/settlement/settlement-import',
|
||||||
|
},
|
||||||
|
component: loadView(() => import('@/views/settlement/settlement-import-form.vue')),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/settlement/transport-reconciliation/form',
|
path: '/settlement/transport-reconciliation/form',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* 金蝶侧发票附件查看/下载公共工具
|
||||||
|
*
|
||||||
|
* 票池发票附件为 {source:'piaozone', path, name}:path 是金蝶侧文件 id(非 http 直链),
|
||||||
|
* 须由后端代理 A801-2 流式回源;OSS 直链附件(url/link/fileUrl)仍由前端直连下载/预览。
|
||||||
|
* 入口共用:票池弹窗、正式结算认领发票行、付款申请发票附件行、收票登记附件表。
|
||||||
|
*/
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { downloadFile } from '@/utils/util';
|
||||||
|
import { downloadInvoicePoolAttachment } from '@/api/payment/invoiceReceipt';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 金蝶侧附件判定:source=piaozone,或有 path 且非 http(s) 直链
|
||||||
|
* @param {Object|Null} attachment 附件对象(attachmentsJson/attachmentJson 行内首项)
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export const isKingdeePathAttachment = attachment => {
|
||||||
|
if (!attachment) return false;
|
||||||
|
if (attachment.source === 'piaozone') return true;
|
||||||
|
return Boolean(attachment.path) && !/^https?:\/\//i.test(attachment.path);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求金蝶文件流:成功返回 Blob;业务失败为 HTTP 200 + R.fail JSON(响应通道未写流时后端即失败),
|
||||||
|
* 按 Content-Type 识别并抛出带 msg 的错误;网络级失败拦截器已提示,标记 silent 避免重复提示。
|
||||||
|
* 供查看(弹窗内预览)/下载两类入口复用
|
||||||
|
* @param {String} invoiceNo 发票号码
|
||||||
|
* @returns {Promise<Blob>}
|
||||||
|
*/
|
||||||
|
export const requestKingdeeFile = async invoiceNo => {
|
||||||
|
let res;
|
||||||
|
try {
|
||||||
|
res = await downloadInvoicePoolAttachment(invoiceNo);
|
||||||
|
} catch (error) {
|
||||||
|
throw Object.assign(new Error(error?.message || '附件下载失败'), { silent: true });
|
||||||
|
}
|
||||||
|
const blob = res?.data;
|
||||||
|
if (!(blob instanceof Blob)) {
|
||||||
|
throw new Error('响应不是文件流');
|
||||||
|
}
|
||||||
|
if (blob.type && blob.type.includes('application/json')) {
|
||||||
|
let message = '附件下载失败';
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(await blob.text());
|
||||||
|
message = parsed.msg || parsed.message || message;
|
||||||
|
} catch {
|
||||||
|
// 错误体非 JSON 时按默认提示
|
||||||
|
}
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
return blob;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载金蝶侧发票附件(经 A801-2 后端代理),失败统一 ElMessage 提示
|
||||||
|
* @param {String} invoiceNo 发票号码
|
||||||
|
* @param {String} fileName 下载文件名(缺省按 发票号.pdf)
|
||||||
|
*/
|
||||||
|
export const downloadKingdeeInvoiceFile = async (invoiceNo, fileName) => {
|
||||||
|
if (!invoiceNo) {
|
||||||
|
ElMessage.warning('当前发票缺少发票号码,无法下载金蝶侧附件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const blob = await requestKingdeeFile(invoiceNo);
|
||||||
|
downloadFile(blob, fileName || `${invoiceNo}.pdf`);
|
||||||
|
} catch (error) {
|
||||||
|
if (!error?.silent) ElMessage.error(error?.message || '金蝶附件下载失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新窗口预览金蝶侧发票附件:拉取 Blob 转 objectURL(PDF 可预览;OFD 等浏览器不可预览的格式由浏览器触发下载)
|
||||||
|
* @param {String} invoiceNo 发票号码
|
||||||
|
* @param {String} fileName 预览文件名(缺省按 发票号.pdf)
|
||||||
|
*/
|
||||||
|
export const viewKingdeeInvoiceFile = async (invoiceNo, fileName) => {
|
||||||
|
if (!invoiceNo) {
|
||||||
|
ElMessage.warning('当前发票缺少发票号码,无法预览金蝶侧附件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const blob = await requestKingdeeFile(invoiceNo);
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
window.open(url, '_blank');
|
||||||
|
// 延迟释放 objectURL:立即回收可能导致新窗口尚未加载完成即失效
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
||||||
|
} catch (error) {
|
||||||
|
if (!error?.silent) ElMessage.error(error?.message || '金蝶附件预览失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
+65
-13
@@ -61,9 +61,18 @@ export const MK_BIZ = {
|
|||||||
|
|
||||||
const MK_SWITCH_NAME = '开启MK';
|
const MK_SWITCH_NAME = '开启MK';
|
||||||
|
|
||||||
async function loadMkTemplateList() {
|
/** 进程内缓存,避免每次提交都打字典;登录后同一会话开关一般不变 */
|
||||||
|
let mkEnabledCache = null;
|
||||||
|
let mkEnabledPromise = null;
|
||||||
|
let mkTemplateListCache = null;
|
||||||
|
|
||||||
|
async function loadMkTemplateList(force = false) {
|
||||||
|
if (!force && mkTemplateListCache) {
|
||||||
|
return mkTemplateListCache;
|
||||||
|
}
|
||||||
const res = await getDictionary({ code: 'mk_template' });
|
const res = await getDictionary({ code: 'mk_template' });
|
||||||
return res?.data?.data || [];
|
mkTemplateListCache = res?.data?.data || [];
|
||||||
|
return mkTemplateListCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 业务字典 mk_template:名称「开启MK」且键值为 1 时才请求 MK */
|
/** 业务字典 mk_template:名称「开启MK」且键值为 1 时才请求 MK */
|
||||||
@@ -72,10 +81,37 @@ export function isMkEnabled(list = []) {
|
|||||||
return String(matched?.dictKey ?? '').trim() === '1';
|
return String(matched?.dictKey ?? '').trim() === '1';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 加载业务字典并判断是否开启 MK */
|
/** 清除 MK 开关缓存(字典变更后可调用) */
|
||||||
export async function loadMkEnabled() {
|
export function clearMkEnabledCache() {
|
||||||
const list = await loadMkTemplateList();
|
mkEnabledCache = null;
|
||||||
return isMkEnabled(list);
|
mkEnabledPromise = null;
|
||||||
|
mkTemplateListCache = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载业务字典并判断是否开启 MK
|
||||||
|
* @param {boolean} [force=false] 是否强制刷新缓存
|
||||||
|
*/
|
||||||
|
export async function loadMkEnabled(force = false) {
|
||||||
|
if (!force && mkEnabledCache !== null) {
|
||||||
|
return mkEnabledCache;
|
||||||
|
}
|
||||||
|
if (!force && mkEnabledPromise) {
|
||||||
|
return mkEnabledPromise;
|
||||||
|
}
|
||||||
|
mkEnabledPromise = loadMkTemplateList(force)
|
||||||
|
.then(list => {
|
||||||
|
mkEnabledCache = isMkEnabled(list);
|
||||||
|
return mkEnabledCache;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
mkEnabledCache = false;
|
||||||
|
throw error;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
mkEnabledPromise = null;
|
||||||
|
});
|
||||||
|
return mkEnabledPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveMkTemplateCode(dictName, list) {
|
export async function resolveMkTemplateCode(dictName, list) {
|
||||||
@@ -99,19 +135,22 @@ export function isMkApprovedStatus(bizType, approvalStatus) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交 MK 审核流
|
* 提交 MK 审核流。未开启 MK 时直接跳过,不请求流程实例相关接口。
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {string} options.bizType 业务类型
|
* @param {string} options.bizType 业务类型
|
||||||
* @param {string|number} options.bizId 业务主键(后端据此生成/复用 formInstanceId)
|
* @param {string|number} options.bizId 业务主键
|
||||||
* @param {string} [options.subjectName] 标题后缀
|
* @param {string} [options.subjectName] 标题后缀
|
||||||
* @param {string} [options.approvalStatus] 提交前的审批状态(须在业务 submit 改状态前取值;通过态强制新建实例)
|
* @param {string} [options.approvalStatus] 提交前的审批状态
|
||||||
|
* @param {boolean} [options.mkEnabled] 调用方已知开关时传入,false 时立即跳过
|
||||||
|
* @returns {Promise<boolean>} true=已发起/复用 MK 流程,false=未开启或已跳过
|
||||||
*/
|
*/
|
||||||
export async function submitMkApprovalFlow({
|
export async function submitMkApprovalFlow({
|
||||||
bizType,
|
bizType,
|
||||||
bizId,
|
bizId,
|
||||||
subjectName = '',
|
subjectName = '',
|
||||||
approvalStatus = '',
|
approvalStatus = '',
|
||||||
}) {
|
mkEnabled,
|
||||||
|
} = {}) {
|
||||||
const conf = MK_BIZ[bizType];
|
const conf = MK_BIZ[bizType];
|
||||||
if (!conf) {
|
if (!conf) {
|
||||||
return Promise.reject(new Error(`不支持的MK业务类型:${bizType}`));
|
return Promise.reject(new Error(`不支持的MK业务类型:${bizType}`));
|
||||||
@@ -119,17 +158,29 @@ export async function submitMkApprovalFlow({
|
|||||||
if (bizId == null || bizId === '') {
|
if (bizId == null || bizId === '') {
|
||||||
return Promise.reject(new Error('业务id不能为空'));
|
return Promise.reject(new Error('业务id不能为空'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 调用方显式关闭,或字典未开启:不请求 hasMkProcessInstance / processSubmit 等 MK 接口
|
||||||
|
if (mkEnabled === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const enabled = mkEnabled === true ? true : await loadMkEnabled();
|
||||||
|
if (!enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const list = await loadMkTemplateList();
|
const list = await loadMkTemplateList();
|
||||||
if (!isMkEnabled(list)) {
|
if (!isMkEnabled(list)) {
|
||||||
return;
|
mkEnabledCache = false;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bizIdText = String(bizId);
|
const bizIdText = String(bizId);
|
||||||
const forceNewFormInstance = isMkApprovedStatus(bizType, approvalStatus);
|
const forceNewFormInstance = isMkApprovedStatus(bizType, approvalStatus);
|
||||||
// 审核通过后再提交必须新建流程;其余场景已有有效实例则跳过
|
// 审核通过后再提交必须新建流程;其余场景已有有效实例则跳过
|
||||||
if (!forceNewFormInstance) {
|
if (!forceNewFormInstance) {
|
||||||
const existsRes = await hasMkProcessInstance({ bizId: bizIdText });
|
const existsRes = await hasMkProcessInstance({ bizId: bizIdText });
|
||||||
if (existsRes?.data?.data) {
|
if (existsRes?.data?.data) {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((conf.rejected || []).includes(approvalStatus)) {
|
if ((conf.rejected || []).includes(approvalStatus)) {
|
||||||
@@ -139,11 +190,12 @@ export async function submitMkApprovalFlow({
|
|||||||
const subject = subjectName
|
const subject = subjectName
|
||||||
? `${conf.subjectPrefix}:${subjectName}`
|
? `${conf.subjectPrefix}:${subjectName}`
|
||||||
: `${conf.subjectPrefix}:${bizIdText}`;
|
: `${conf.subjectPrefix}:${bizIdText}`;
|
||||||
return processSubmit({
|
await processSubmit({
|
||||||
templateCode,
|
templateCode,
|
||||||
bizId: bizIdText,
|
bizId: bizIdText,
|
||||||
subject,
|
subject,
|
||||||
bizType,
|
bizType,
|
||||||
forceNewFormInstance,
|
forceNewFormInstance,
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {}) => {
|
export const normalizeSearchRangeParams = (params = {}, rangeMap = {}) => {
|
||||||
const values = { ...params };
|
const values = { ...params };
|
||||||
|
|
||||||
|
|||||||
@@ -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 '';
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -150,6 +150,7 @@ import { mapGetters } from 'vuex';
|
|||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
|
import { omitInvalidListStatus } from '@/utils/search-range';
|
||||||
import { getToken, isAdministrator } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
||||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||||
@@ -1182,7 +1183,7 @@ export default {
|
|||||||
},
|
},
|
||||||
onLoad(page, params = {}) {
|
onLoad(page, params = {}) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
getList(page.currentPage, page.pageSize, { ...params, ...this.query })
|
getList(page.currentPage, page.pageSize, omitInvalidListStatus({ ...params, ...this.query }))
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const data = res.data.data;
|
const data = res.data.data;
|
||||||
this.page.total = data.total;
|
this.page.total = data.total;
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ import { mapGetters } from 'vuex';
|
|||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
|
import { omitInvalidListStatus } from '@/utils/search-range';
|
||||||
import { getToken } from '@/utils/auth';
|
import { getToken } from '@/utils/auth';
|
||||||
import NProgress from 'nprogress';
|
import NProgress from 'nprogress';
|
||||||
import 'nprogress/nprogress.css';
|
import 'nprogress/nprogress.css';
|
||||||
@@ -507,7 +508,7 @@ export default {
|
|||||||
this.onLoad(this.page, this.query);
|
this.onLoad(this.page, this.query);
|
||||||
},
|
},
|
||||||
buildQuery(params = {}) {
|
buildQuery(params = {}) {
|
||||||
const values = { ...params, ...this.query };
|
const values = omitInvalidListStatus({ ...params, ...this.query });
|
||||||
const rangeMap = {
|
const rangeMap = {
|
||||||
effectiveDateRange: ['effectiveDateStart', 'effectiveDateEnd'],
|
effectiveDateRange: ['effectiveDateStart', 'effectiveDateEnd'],
|
||||||
expiryDateRange: ['expiryDateStart', 'expiryDateEnd'],
|
expiryDateRange: ['expiryDateStart', 'expiryDateEnd'],
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/customer-type';
|
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/customer-type';
|
||||||
|
import { omitInvalidListStatus } from '@/utils/search-range';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -290,7 +291,7 @@ export default {
|
|||||||
},
|
},
|
||||||
onLoad(page, params = {}) {
|
onLoad(page, params = {}) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
getList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(res => {
|
getList(page.currentPage, page.pageSize, omitInvalidListStatus(Object.assign(params, this.query))).then(res => {
|
||||||
const data = res.data.data;
|
const data = res.data.data;
|
||||||
this.page.total = data.total;
|
this.page.total = data.total;
|
||||||
this.data = data.records;
|
this.data = data.records;
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ import { exportBlob } from '@/api/common';
|
|||||||
import { getDictionaryAll } from '@/api/system/dictbiz';
|
import { getDictionaryAll } from '@/api/system/dictbiz';
|
||||||
import { getDeptTree } from '@/api/system/dept';
|
import { getDeptTree } from '@/api/system/dept';
|
||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
|
import { omitInvalidListStatus } from '@/utils/search-range';
|
||||||
import { getToken } from '@/utils/auth';
|
import { getToken } from '@/utils/auth';
|
||||||
import { Download } from '@element-plus/icons-vue';
|
import { Download } from '@element-plus/icons-vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
@@ -569,7 +570,7 @@ export default {
|
|||||||
},
|
},
|
||||||
onLoad(page, params = {}) {
|
onLoad(page, params = {}) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
getList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(res => {
|
getList(page.currentPage, page.pageSize, omitInvalidListStatus(Object.assign(params, this.query))).then(res => {
|
||||||
const data = res.data.data;
|
const data = res.data.data;
|
||||||
this.page.total = data.total;
|
this.page.total = data.total;
|
||||||
this.data = data.records;
|
this.data = data.records;
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { changeStatus, getDetail, getList, remove, submit } from '@/api/base/measurement-unit';
|
import { changeStatus, getDetail, getList, remove, submit } from '@/api/base/measurement-unit';
|
||||||
import { createOption } from '@/option/base/measurement-unit';
|
import { createOption } from '@/option/base/measurement-unit';
|
||||||
|
import { omitInvalidListStatus } from '@/utils/search-range';
|
||||||
import { isAdministrator } from '@/utils/auth';
|
import { isAdministrator } from '@/utils/auth';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -218,7 +219,7 @@ export default {
|
|||||||
},
|
},
|
||||||
onLoad(page = this.page, params = this.query) {
|
onLoad(page = this.page, params = this.query) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
return getList(page.currentPage, page.pageSize, params)
|
return getList(page.currentPage, page.pageSize, omitInvalidListStatus(params))
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const pageData = res.data.data || {};
|
const pageData = res.data.data || {};
|
||||||
this.data = pageData.records || [];
|
this.data = pageData.records || [];
|
||||||
|
|||||||
@@ -270,6 +270,7 @@ import { mapGetters } from 'vuex';
|
|||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
|
import { omitInvalidListStatus } from '@/utils/search-range';
|
||||||
import { getToken, isAdministrator } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
||||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||||
@@ -1636,7 +1637,7 @@ export default {
|
|||||||
},
|
},
|
||||||
onLoad(page, params = {}) {
|
onLoad(page, params = {}) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
getList(page.currentPage, page.pageSize, { ...params, ...this.query })
|
getList(page.currentPage, page.pageSize, omitInvalidListStatus({ ...params, ...this.query }))
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const data = res.data.data;
|
const data = res.data.data;
|
||||||
this.page.total = data.total;
|
this.page.total = data.total;
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ import { mapGetters } from 'vuex';
|
|||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
|
import { omitInvalidListStatus } from '@/utils/search-range';
|
||||||
import { getToken, isAdministrator } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
||||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||||
@@ -1157,7 +1158,7 @@ export default {
|
|||||||
},
|
},
|
||||||
onLoad(page, params = {}) {
|
onLoad(page, params = {}) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
getList(page.currentPage, page.pageSize, { ...params, ...this.query })
|
getList(page.currentPage, page.pageSize, omitInvalidListStatus({ ...params, ...this.query }))
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const data = res.data.data;
|
const data = res.data.data;
|
||||||
this.page.total = data.total;
|
this.page.total = data.total;
|
||||||
|
|||||||
@@ -396,8 +396,11 @@ export default {
|
|||||||
return this.selectionList.map(item => item.id).join(',');
|
return this.selectionList.map(item => item.id).join(',');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.applyDefaultDeptSearch();
|
// 超管默认查全部组织,不预填组织筛选;普通用户预填当前组织。
|
||||||
|
if (!this.isAdmin) {
|
||||||
|
this.applyDefaultDeptSearch();
|
||||||
|
}
|
||||||
this.initDeptTree();
|
this.initDeptTree();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -455,10 +458,23 @@ export default {
|
|||||||
normalizeListQuery(params = {}) {
|
normalizeListQuery(params = {}) {
|
||||||
// 搜索条件优先于缓存的 query,避免默认组织 ID 覆盖用户新选择的组织。
|
// 搜索条件优先于缓存的 query,避免默认组织 ID 覆盖用户新选择的组织。
|
||||||
const query = { ...this.query, ...params };
|
const query = { ...this.query, ...params };
|
||||||
const rawDeptId = query.deptId ?? query.searchDeptId;
|
// 本次搜索显式带了组织字段时,以本次为准(含清空);否则沿用缓存。
|
||||||
|
const hasDeptInParams =
|
||||||
|
Object.prototype.hasOwnProperty.call(params, 'deptId') ||
|
||||||
|
Object.prototype.hasOwnProperty.call(params, 'searchDeptId');
|
||||||
|
const rawDeptId = hasDeptInParams
|
||||||
|
? params.deptId ?? params.searchDeptId
|
||||||
|
: query.deptId ?? query.searchDeptId;
|
||||||
delete query.searchDeptId;
|
delete query.searchDeptId;
|
||||||
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
|
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
|
||||||
query.deptId = String(rawDeptId).split(',')[0].trim();
|
// 级联可能回传路径数组,统一取末级并保持字符串,避免雪花 ID 精度丢失。
|
||||||
|
const deptValue = Array.isArray(rawDeptId) ? rawDeptId.at(-1) : rawDeptId;
|
||||||
|
query.deptId = String(deptValue ?? '')
|
||||||
|
.split(',')[0]
|
||||||
|
.trim();
|
||||||
|
if (!query.deptId) {
|
||||||
|
delete query.deptId;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
delete query.deptId;
|
delete query.deptId;
|
||||||
}
|
}
|
||||||
@@ -466,6 +482,23 @@ export default {
|
|||||||
return query;
|
return query;
|
||||||
},
|
},
|
||||||
applyDefaultDeptSearch() {
|
applyDefaultDeptSearch() {
|
||||||
|
// 超管默认不限组织,避免预填组织后新增落在其他组织时列表看不见。
|
||||||
|
if (this.isAdmin) {
|
||||||
|
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
|
||||||
|
if (deptColumn) {
|
||||||
|
deptColumn.searchValue = undefined;
|
||||||
|
}
|
||||||
|
const nextQuery = { ...this.query };
|
||||||
|
delete nextQuery.deptId;
|
||||||
|
delete nextQuery.searchDeptId;
|
||||||
|
this.query = nextQuery;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.crud?.searchForm) {
|
||||||
|
this.$refs.crud.searchForm.searchDeptId = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
const deptId = this.defaultDeptId();
|
const deptId = this.defaultDeptId();
|
||||||
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
|
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
|
||||||
if (!deptId || !deptColumn) return;
|
if (!deptId || !deptColumn) return;
|
||||||
@@ -477,6 +510,11 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
resolveDeptName(deptId) {
|
||||||
|
if (!deptId) return '';
|
||||||
|
const matched = this.deptOptions.find(item => String(item.value) === String(deptId));
|
||||||
|
return matched ? String(matched.label || '').trim() : '';
|
||||||
|
},
|
||||||
resetAddressQuery() {
|
resetAddressQuery() {
|
||||||
this.addressQuery = {
|
this.addressQuery = {
|
||||||
addressName: '',
|
addressName: '',
|
||||||
@@ -933,7 +971,8 @@ export default {
|
|||||||
}
|
}
|
||||||
api.submit(submitRow).then(
|
api.submit(submitRow).then(
|
||||||
() => {
|
() => {
|
||||||
this.onLoad(this.page);
|
this.page.currentPage = 1;
|
||||||
|
this.onLoad(this.page, this.query);
|
||||||
this.$message({ type: 'success', message: '操作成功!' });
|
this.$message({ type: 'success', message: '操作成功!' });
|
||||||
done();
|
done();
|
||||||
},
|
},
|
||||||
@@ -951,7 +990,7 @@ export default {
|
|||||||
}
|
}
|
||||||
api.submit(submitRow).then(
|
api.submit(submitRow).then(
|
||||||
() => {
|
() => {
|
||||||
this.onLoad(this.page);
|
this.onLoad(this.page, this.query);
|
||||||
this.$message({ type: 'success', message: '操作成功!' });
|
this.$message({ type: 'success', message: '操作成功!' });
|
||||||
done();
|
done();
|
||||||
},
|
},
|
||||||
@@ -1006,7 +1045,13 @@ export default {
|
|||||||
beforeOpen(done, type) {
|
beforeOpen(done, type) {
|
||||||
this.dialogReadonly = type === 'view';
|
this.dialogReadonly = type === 'view';
|
||||||
if (type === 'add') {
|
if (type === 'add') {
|
||||||
this.form = {};
|
const form = {};
|
||||||
|
// 超管若当前按某组织筛选,新增默认归属该组织,避免保存后被筛选条件滤掉。
|
||||||
|
if (this.isAdmin && this.query.deptId) {
|
||||||
|
form.deptId = String(this.query.deptId);
|
||||||
|
form.deptName = this.resolveDeptName(form.deptId);
|
||||||
|
}
|
||||||
|
this.form = form;
|
||||||
done();
|
done();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2991,12 +2991,51 @@
|
|||||||
<el-table :data="waybillRouteChangeRecords" border>
|
<el-table :data="waybillRouteChangeRecords" border>
|
||||||
<el-table-column prop="changeTime" label="变更时间" min-width="170" />
|
<el-table-column prop="changeTime" label="变更时间" min-width="170" />
|
||||||
<el-table-column prop="changeUser" label="变更人" min-width="120" />
|
<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 }">
|
<template #default="{ row }">
|
||||||
<div
|
<div class="business-crud-page__route-change-record-content">
|
||||||
class="business-crud-page__route-change-record-content"
|
<div
|
||||||
v-text="formatRouteChangeContent(row.content)"
|
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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
|
||||||
@@ -5283,7 +5322,7 @@ import { formatContractOptionLabel } from '@/utils/contract';
|
|||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||||
import { formatRouteChangeContent } from '@/utils/route-change';
|
import { parseRouteChangeContent } from '@/utils/route-change';
|
||||||
import { applyTableMenuWidth } from '@/utils/table-menu';
|
import { applyTableMenuWidth } from '@/utils/table-menu';
|
||||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||||
import { isMobile } from '@/utils/validate';
|
import { isMobile } from '@/utils/validate';
|
||||||
@@ -6630,7 +6669,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
formatContractOptionLabel,
|
formatContractOptionLabel,
|
||||||
formatRouteChangeContent,
|
parseRouteChangeContent,
|
||||||
initStandaloneFormPage() {
|
initStandaloneFormPage() {
|
||||||
if (!this.isStandaloneFormPage) return;
|
if (!this.isStandaloneFormPage) return;
|
||||||
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
||||||
@@ -15900,10 +15939,65 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__route-change-record-content {
|
&__route-change-record-content {
|
||||||
white-space: pre-wrap;
|
display: flex;
|
||||||
overflow-wrap: anywhere;
|
flex-direction: column;
|
||||||
word-break: break-word;
|
gap: 10px;
|
||||||
line-height: 22px;
|
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) {
|
@media (max-width: 1200px) {
|
||||||
|
|||||||
@@ -887,11 +887,14 @@ const syncCarrierOptions = () => {
|
|||||||
carrierOptions.value = [];
|
carrierOptions.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const carrierId =
|
const carrierId = resolveCarrierArchiveId(
|
||||||
customerContract?.partyBId ||
|
customerContract?.partyBId ||
|
||||||
customerContract?.partyBUserId ||
|
customerContract?.partyBUserId ||
|
||||||
customerContract?.customerId ||
|
customerContract?.contractPartyBId ||
|
||||||
'';
|
customerContract?.customerId ||
|
||||||
|
'',
|
||||||
|
carrierName
|
||||||
|
);
|
||||||
carrierOptions.value = [{ id: carrierId, carrierName, label: carrierName, value: carrierName }];
|
carrierOptions.value = [{ id: carrierId, carrierName, label: carrierName, value: carrierName }];
|
||||||
form.carrierId = carrierId;
|
form.carrierId = carrierId;
|
||||||
form.carrierIds = carrierId ? [carrierId] : [];
|
form.carrierIds = carrierId ? [carrierId] : [];
|
||||||
@@ -1317,12 +1320,20 @@ const contractChange = id => {
|
|||||||
const carrierTypeChange = () => {
|
const carrierTypeChange = () => {
|
||||||
syncCarrierOptions();
|
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 carrierChange = value => {
|
||||||
const item = carrierOptions.value.find(option => String(option.value) === String(value || ''));
|
const item = carrierOptions.value.find(option => String(option.value) === String(value || ''));
|
||||||
clearCarrier();
|
clearCarrier();
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
form.carrierId = item.id || '';
|
const carrierId = resolveCarrierArchiveId(item.id, item.carrierName);
|
||||||
form.carrierIds = item.id ? [item.id] : [];
|
form.carrierId = carrierId;
|
||||||
|
form.carrierIds = carrierId ? [carrierId] : [];
|
||||||
form.carrierName = item.carrierName || '';
|
form.carrierName = item.carrierName || '';
|
||||||
if (!isSelfOperated.value) form.carrierContractId = item.carrierContractId || '';
|
if (!isSelfOperated.value) form.carrierContractId = item.carrierContractId || '';
|
||||||
};
|
};
|
||||||
@@ -1738,11 +1749,23 @@ const normalizeOptionalPlanId = value => {
|
|||||||
const buildImportPayload = (status = form.status, rowList = rows.value, extra = {}) => {
|
const buildImportPayload = (status = form.status, rowList = rows.value, extra = {}) => {
|
||||||
const { file, ...batch } = form;
|
const { file, ...batch } = form;
|
||||||
const planId = normalizeOptionalPlanId(batch.planId);
|
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 {
|
return {
|
||||||
...batch,
|
...batch,
|
||||||
planId,
|
planId,
|
||||||
// 未选计划时不带占位名称,避免落库/回显 -1
|
// 未选计划时不带占位名称,避免落库/回显 -1
|
||||||
planName: planId ? batch.planName || undefined : null,
|
planName: planId ? batch.planName || undefined : null,
|
||||||
|
carrierId: carrierId || undefined,
|
||||||
|
carrierIds,
|
||||||
status,
|
status,
|
||||||
rows: rowList.map(normalizeImportRow).map(stripInternalRowFields),
|
rows: rowList.map(normalizeImportRow).map(stripInternalRowFields),
|
||||||
...extra,
|
...extra,
|
||||||
|
|||||||
@@ -3020,12 +3020,57 @@
|
|||||||
<el-table :data="waybillRouteChangeRecords" border>
|
<el-table :data="waybillRouteChangeRecords" border>
|
||||||
<el-table-column prop="changeTime" label="变更时间" min-width="170" />
|
<el-table-column prop="changeTime" label="变更时间" min-width="170" />
|
||||||
<el-table-column prop="changeUser" label="变更人" min-width="120" />
|
<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 }">
|
<template #default="{ row }">
|
||||||
<div
|
<div class="waybill-manage-page__route-change-record-content">
|
||||||
class="waybill-manage-page__route-change-record-content"
|
<div
|
||||||
v-text="formatRouteChangeContent(row.content)"
|
v-for="(group, groupIndex) in parseRouteChangeContent(row.content)"
|
||||||
/>
|
:key="groupIndex"
|
||||||
|
class="waybill-manage-page__route-change-group"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="group.title"
|
||||||
|
class="waybill-manage-page__route-change-group-title"
|
||||||
|
>
|
||||||
|
{{ group.title }}
|
||||||
|
</div>
|
||||||
|
<div v-if="group.text" class="waybill-manage-page__route-change-group-text">
|
||||||
|
{{ group.text }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(field, fieldIndex) in group.fields"
|
||||||
|
:key="fieldIndex"
|
||||||
|
class="waybill-manage-page__route-change-field"
|
||||||
|
>
|
||||||
|
<span class="waybill-manage-page__route-change-field-label">
|
||||||
|
{{ field.label }}
|
||||||
|
</span>
|
||||||
|
<template v-if="field.changed">
|
||||||
|
<span class="waybill-manage-page__route-change-before">
|
||||||
|
{{ field.before }}
|
||||||
|
</span>
|
||||||
|
<span class="waybill-manage-page__route-change-arrow">→</span>
|
||||||
|
<span class="waybill-manage-page__route-change-after">
|
||||||
|
{{ field.after }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span v-else class="waybill-manage-page__route-change-single">
|
||||||
|
{{ field.single }}
|
||||||
|
<span
|
||||||
|
v-if="field.unchanged"
|
||||||
|
class="waybill-manage-page__route-change-flag"
|
||||||
|
>
|
||||||
|
未变更
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
|
||||||
@@ -3667,7 +3712,7 @@ import { wgs84ToGcj02 } from '@/utils/coordinate';
|
|||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||||
import { formatRouteChangeContent } from '@/utils/route-change';
|
import { parseRouteChangeContent } from '@/utils/route-change';
|
||||||
import { applyTableMenuWidth } from '@/utils/table-menu';
|
import { applyTableMenuWidth } from '@/utils/table-menu';
|
||||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||||
import { isMobile } from '@/utils/validate';
|
import { isMobile } from '@/utils/validate';
|
||||||
@@ -4755,7 +4800,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
formatContractOptionLabel,
|
formatContractOptionLabel,
|
||||||
formatRouteChangeContent,
|
parseRouteChangeContent,
|
||||||
// 页面被 keep-alive 缓存/卸载时调用,关闭本页所有 append-to-body 的弹窗,
|
// 页面被 keep-alive 缓存/卸载时调用,关闭本页所有 append-to-body 的弹窗,
|
||||||
// 避免 Teleport 出去的 DOM 残留在 body 上盖住后续页面(详见 data 里的说明)。
|
// 避免 Teleport 出去的 DOM 残留在 body 上盖住后续页面(详见 data 里的说明)。
|
||||||
closeInnerDialogs() {
|
closeInnerDialogs() {
|
||||||
@@ -6508,17 +6553,10 @@ export default {
|
|||||||
if (isAddMode) {
|
if (isAddMode) {
|
||||||
delete submitRow.id;
|
delete submitRow.id;
|
||||||
delete submitRow.waybillNo;
|
delete submitRow.waybillNo;
|
||||||
// 复制提交:状态列未进表单,Avue 可能不带回;显式保留原运单业务状态(含已完成)
|
// 复制新建:业务状态固定为草稿,不沿用原运单状态
|
||||||
const copyId = this.$route.query.copyId || '';
|
const copyId = this.$route.query.copyId || '';
|
||||||
if (copyId && !saveAsDraft) {
|
if (copyId) {
|
||||||
const copiedStatus =
|
submitRow.businessStatus = this.config.draftStatus || 'draft';
|
||||||
this.form?.businessStatus ||
|
|
||||||
this.copySourceBusinessStatus ||
|
|
||||||
submitRow.businessStatus ||
|
|
||||||
'';
|
|
||||||
if (copiedStatus) {
|
|
||||||
submitRow.businessStatus = copiedStatus;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const skipValidation = saveAsDraft && this.config.skipDraftValidation === true;
|
const skipValidation = saveAsDraft && this.config.skipDraftValidation === true;
|
||||||
@@ -6541,14 +6579,16 @@ export default {
|
|||||||
if (this.config.enableAttachmentTable) {
|
if (this.config.enableAttachmentTable) {
|
||||||
submitRow.attachmentsJson = JSON.stringify(this.attachmentRows);
|
submitRow.attachmentsJson = JSON.stringify(this.attachmentRows);
|
||||||
}
|
}
|
||||||
|
const isCopyAdd = Boolean(this.$route.query.copyId) && isAddMode;
|
||||||
if (this.config.saveAsDraft && saveAsDraft) {
|
if (this.config.saveAsDraft && saveAsDraft) {
|
||||||
submitRow[this.config.draftStatusProp || 'approvalStatus'] =
|
submitRow[this.config.draftStatusProp || 'approvalStatus'] =
|
||||||
this.config.draftStatus || 'draft';
|
this.config.draftStatus || 'draft';
|
||||||
} else if (
|
} else if (
|
||||||
|
!isCopyAdd &&
|
||||||
this.config.draftStatusProp === 'businessStatus' &&
|
this.config.draftStatusProp === 'businessStatus' &&
|
||||||
submitRow.businessStatus === (this.config.draftStatus || 'draft')
|
submitRow.businessStatus === (this.config.draftStatus || 'draft')
|
||||||
) {
|
) {
|
||||||
// 正式提交:清空草稿态,由后端按过程配置校正为待执行/进行中
|
// 正式提交:清空草稿态,由后端按过程配置校正为待执行/进行中(复制新建除外,保持草稿)
|
||||||
submitRow.businessStatus = '';
|
submitRow.businessStatus = '';
|
||||||
}
|
}
|
||||||
delete submitRow.basicInfoTitle;
|
delete submitRow.basicInfoTitle;
|
||||||
@@ -6811,12 +6851,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
rowSave(row, done, loading) {
|
rowSave(row, done, loading) {
|
||||||
const submitRow = this.normalizeRow(row);
|
// 复制页提交走草稿保存,避免 submit 接口把 draft 校正为待执行/进行中
|
||||||
|
const isCopySubmit = Boolean(this.$route.query.copyId);
|
||||||
|
const submitRow = this.normalizeRow(row, isCopySubmit);
|
||||||
if (!submitRow) {
|
if (!submitRow) {
|
||||||
this.stopSubmitLoading(loading);
|
this.stopSubmitLoading(loading);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.getSaveRequest()(submitRow).then(
|
const request =
|
||||||
|
isCopySubmit && typeof this.api.saveDraft === 'function'
|
||||||
|
? this.api.saveDraft
|
||||||
|
: this.getSaveRequest();
|
||||||
|
request(submitRow).then(
|
||||||
res => {
|
res => {
|
||||||
this.markWaybillListNeedRefresh();
|
this.markWaybillListNeedRefresh();
|
||||||
this.onLoad(this.page);
|
this.onLoad(this.page);
|
||||||
@@ -6945,13 +6991,14 @@ export default {
|
|||||||
if (type === 'add' && copyId) {
|
if (type === 'add' && copyId) {
|
||||||
this.api.getDetail(copyId).then(res => {
|
this.api.getDetail(copyId).then(res => {
|
||||||
const sourceData = res.data.data || {};
|
const sourceData = res.data.data || {};
|
||||||
// 原运单业务状态(如 completed)提交时需原样带回并生成应收应付
|
this.copySourceBusinessStatus = '';
|
||||||
this.copySourceBusinessStatus = sourceData.businessStatus || '';
|
// 清除不应该复制的字段;复制新建固定为草稿
|
||||||
// 清除不应该复制的字段(业务状态与原运单保持一致)
|
|
||||||
delete sourceData.id;
|
delete sourceData.id;
|
||||||
delete sourceData.code;
|
delete sourceData.code;
|
||||||
delete sourceData.waybillNo;
|
delete sourceData.waybillNo;
|
||||||
delete sourceData.waybillStatus;
|
delete sourceData.waybillStatus;
|
||||||
|
delete sourceData.businessStatus;
|
||||||
|
delete sourceData.businessStatusName;
|
||||||
delete sourceData.driverAcceptStatus;
|
delete sourceData.driverAcceptStatus;
|
||||||
delete sourceData.driverAcceptTime;
|
delete sourceData.driverAcceptTime;
|
||||||
delete sourceData.driverAcceptDriverId;
|
delete sourceData.driverAcceptDriverId;
|
||||||
@@ -6968,11 +7015,8 @@ export default {
|
|||||||
delete sourceData.updateUserName;
|
delete sourceData.updateUserName;
|
||||||
|
|
||||||
this.applyFormDetail(sourceData);
|
this.applyFormDetail(sourceData);
|
||||||
// 标记为手工创建;业务状态沿用原运单(含已完成)
|
|
||||||
this.form.dataSource = '手工创建';
|
this.form.dataSource = '手工创建';
|
||||||
if (this.copySourceBusinessStatus) {
|
this.form.businessStatus = this.config.draftStatus || 'draft';
|
||||||
this.form.businessStatus = this.copySourceBusinessStatus;
|
|
||||||
}
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -11847,10 +11891,65 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__route-change-record-content {
|
&__route-change-record-content {
|
||||||
white-space: pre-wrap;
|
display: flex;
|
||||||
overflow-wrap: anywhere;
|
flex-direction: column;
|
||||||
word-break: break-word;
|
gap: 10px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
|
white-space: normal;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__route-change-group {
|
||||||
|
padding-left: 8px;
|
||||||
|
border-left: 2px solid #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__route-change-group-title {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__route-change-group-text {
|
||||||
|
color: #606266;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__route-change-field {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 54px minmax(0, 1fr);
|
||||||
|
column-gap: 8px;
|
||||||
|
row-gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__route-change-field-label {
|
||||||
|
color: #909399;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__route-change-before,
|
||||||
|
&__route-change-single {
|
||||||
|
color: #606266;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__route-change-arrow {
|
||||||
|
color: #c0c4cc;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__route-change-after {
|
||||||
|
color: #409eff;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__route-change-flag {
|
||||||
|
margin-left: 6px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__common-address-toolbar {
|
&__common-address-toolbar {
|
||||||
|
|||||||
@@ -332,7 +332,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as api from '@/api/business/contract-manage';
|
import * as api from '@/api/business/contract-manage';
|
||||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||||
import BillingPlanEditor from './components/billing-plan-editor.vue';
|
import BillingPlanEditor from './components/billing-plan-editor.vue';
|
||||||
import ContractAttachmentSection, {
|
import ContractAttachmentSection, {
|
||||||
isContractSealMaterialLocation,
|
isContractSealMaterialLocation,
|
||||||
@@ -738,7 +738,7 @@ export default {
|
|||||||
removeCustomPeriodRow(index) { if (index <= 0) return; const periods = [...(this.settlementRule.customPeriods || [])]; periods.splice(index, 1); this.settlementRule.customPeriods = normalizeCustomPeriods(periods); },
|
removeCustomPeriodRow(index) { if (index <= 0) return; const periods = [...(this.settlementRule.customPeriods || [])]; periods.splice(index, 1); this.settlementRule.customPeriods = normalizeCustomPeriods(periods); },
|
||||||
validateCustomPeriods(periods = [], label) { const rows = normalizeCustomPeriods(periods); if (!rows.length) { this.$message.warning(`${label}:请至少配置一段自定义周期`); return false; } for (let index = 0; index < rows.length; index += 1) { const row = rows[index]; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间开始日`); return false; } if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间结束日`); return false; } if (endDay < startDay) { this.$message.warning(`${label}:第${index + 1}行结束日不能早于开始日`); return false; } if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) { this.$message.warning(`${label}:自定义多周期区间必须连续,不允许重叠或存在日期缺口`); return false; } } return true; },
|
validateCustomPeriods(periods = [], label) { const rows = normalizeCustomPeriods(periods); if (!rows.length) { this.$message.warning(`${label}:请至少配置一段自定义周期`); return false; } for (let index = 0; index < rows.length; index += 1) { const row = rows[index]; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间开始日`); return false; } if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间结束日`); return false; } if (endDay < startDay) { this.$message.warning(`${label}:第${index + 1}行结束日不能早于开始日`); return false; } if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) { this.$message.warning(`${label}:自定义多周期区间必须连续,不允许重叠或存在日期缺口`); return false; } } return true; },
|
||||||
validateSettlementRule(rule, label) { if (Number(rule.autoGenerate) !== 1) return true; if (!rule.billStartDate || !rule.settlementType) { this.$message.warning(`${label}:请完整填写账单起始日和结算类型`); return false; } if (rule.settlementType === '月结' && !rule.billCycleType) { this.$message.warning(`${label}:请选择结算周期`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '固定截单日' && !rule.billCutoffDay) { this.$message.warning(`${label}:请选择账单截单日`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') return this.validateCustomPeriods(rule.customPeriods, label); if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) { this.$message.warning(`${label}:请选择周期天数`); return false; } return true; },
|
validateSettlementRule(rule, label) { if (Number(rule.autoGenerate) !== 1) return true; if (!rule.billStartDate || !rule.settlementType) { this.$message.warning(`${label}:请完整填写账单起始日和结算类型`); return false; } if (rule.settlementType === '月结' && !rule.billCycleType) { this.$message.warning(`${label}:请选择结算周期`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '固定截单日' && !rule.billCutoffDay) { this.$message.warning(`${label}:请选择账单截单日`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') return this.validateCustomPeriods(rule.customPeriods, label); if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) { this.$message.warning(`${label}:请选择周期天数`); return false; } return true; },
|
||||||
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); await submitMkApprovalFlow({ bizType: 'contract-manage', bizId: this.form.id, subjectName: this.form.contractName || '', approvalStatus: this.form.approvalStatus || 'change_rejected' }); this.$message.success('变更已提交'); this.$router.back(); },
|
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); await submitMkApprovalFlow({ bizType: 'contract-manage', bizId: this.form.id, subjectName: this.form.contractName || '', approvalStatus: this.form.approvalStatus || 'change_rejected', mkEnabled: await loadMkEnabled().catch(() => false) }); this.$message.success('变更已提交'); this.$router.back(); },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1986,7 +1986,9 @@ export default {
|
|||||||
bizType: 'contract-manage',
|
bizType: 'contract-manage',
|
||||||
bizId: id,
|
bizId: id,
|
||||||
subjectName: saved.contractName || submit.contractName || '',
|
subjectName: saved.contractName || submit.contractName || '',
|
||||||
approvalStatus: previousApprovalStatus || saved.approvalStatus || submit.approvalStatus || '',
|
approvalStatus:
|
||||||
|
previousApprovalStatus || saved.approvalStatus || submit.approvalStatus || '',
|
||||||
|
mkEnabled: this.mkEnabled,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2025,6 +2027,7 @@ export default {
|
|||||||
bizId: payload.id || this.form.id,
|
bizId: payload.id || this.form.id,
|
||||||
subjectName: payload.contractName || this.form.contractName || '',
|
subjectName: payload.contractName || this.form.contractName || '',
|
||||||
approvalStatus: this.form.approvalStatus || 'change_rejected',
|
approvalStatus: this.form.approvalStatus || 'change_rejected',
|
||||||
|
mkEnabled: this.mkEnabled,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.$message.success('已重新提交变更审批');
|
this.$message.success('已重新提交变更审批');
|
||||||
this.closeForm();
|
this.closeForm();
|
||||||
@@ -3001,6 +3004,7 @@ export default {
|
|||||||
bizId: row.id,
|
bizId: row.id,
|
||||||
subjectName: row.contractName || '',
|
subjectName: row.contractName || '',
|
||||||
approvalStatus: row.approvalStatus || '',
|
approvalStatus: row.approvalStatus || '',
|
||||||
|
mkEnabled: this.mkEnabled,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
: request();
|
: request();
|
||||||
@@ -3338,7 +3342,7 @@ export default {
|
|||||||
|
|
||||||
:deep(.el-form-item) {
|
:deep(.el-form-item) {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 24px;
|
//margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-form-item__label) {
|
:deep(.el-form-item__label) {
|
||||||
|
|||||||
@@ -1683,9 +1683,56 @@
|
|||||||
><el-table-column prop="changeTime" label="变更时间" min-width="170" /><el-table-column
|
><el-table-column prop="changeTime" label="变更时间" min-width="170" /><el-table-column
|
||||||
prop="changeUser"
|
prop="changeUser"
|
||||||
label="变更人"
|
label="变更人"
|
||||||
min-width="120" /><el-table-column prop="content" label="变更内容" min-width="360"
|
min-width="120" /><el-table-column
|
||||||
|
prop="content"
|
||||||
|
label="变更内容"
|
||||||
|
min-width="360"
|
||||||
|
:show-overflow-tooltip="false"
|
||||||
><template #default="{ row }"
|
><template #default="{ row }"
|
||||||
><div class="loading-route-change-dialog__record-content">{{ formatRouteChangeContent(row.content) }}</div></template
|
><div class="loading-route-change-dialog__record-content">
|
||||||
|
<div
|
||||||
|
v-for="(group, groupIndex) in parseRouteChangeContent(row.content)"
|
||||||
|
:key="groupIndex"
|
||||||
|
class="loading-route-change-dialog__change-group"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="group.title"
|
||||||
|
class="loading-route-change-dialog__change-group-title"
|
||||||
|
>
|
||||||
|
{{ group.title }}
|
||||||
|
</div>
|
||||||
|
<div v-if="group.text" class="loading-route-change-dialog__change-group-text">
|
||||||
|
{{ group.text }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(field, fieldIndex) in group.fields"
|
||||||
|
:key="fieldIndex"
|
||||||
|
class="loading-route-change-dialog__change-field"
|
||||||
|
>
|
||||||
|
<span class="loading-route-change-dialog__change-field-label">
|
||||||
|
{{ field.label }}
|
||||||
|
</span>
|
||||||
|
<template v-if="field.changed">
|
||||||
|
<span class="loading-route-change-dialog__change-before">
|
||||||
|
{{ field.before }}
|
||||||
|
</span>
|
||||||
|
<span class="loading-route-change-dialog__change-arrow">→</span>
|
||||||
|
<span class="loading-route-change-dialog__change-after">
|
||||||
|
{{ field.after }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span v-else class="loading-route-change-dialog__change-single">
|
||||||
|
{{ field.single }}
|
||||||
|
<span
|
||||||
|
v-if="field.unchanged"
|
||||||
|
class="loading-route-change-dialog__change-flag"
|
||||||
|
>
|
||||||
|
未变更
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div></template
|
||||||
></el-table-column
|
></el-table-column
|
||||||
><el-table-column
|
><el-table-column
|
||||||
prop="remark"
|
prop="remark"
|
||||||
@@ -1798,7 +1845,7 @@ import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { isMobile } from '@/utils/validate';
|
import { isMobile } from '@/utils/validate';
|
||||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||||
import { formatRouteChangeContent } from '@/utils/route-change';
|
import { parseRouteChangeContent } from '@/utils/route-change';
|
||||||
|
|
||||||
const defaultTransportType = '';
|
const defaultTransportType = '';
|
||||||
const defaultCandidateTransportType = '';
|
const defaultCandidateTransportType = '';
|
||||||
@@ -2277,7 +2324,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
formatRouteChangeContent,
|
parseRouteChangeContent,
|
||||||
formatTaskCarrierContractLabel(item = {}) {
|
formatTaskCarrierContractLabel(item = {}) {
|
||||||
const carrierName = item.carrierName || item.partyB || '';
|
const carrierName = item.carrierName || item.partyB || '';
|
||||||
const contractLabel = formatContractOptionLabel(item);
|
const contractLabel = formatContractOptionLabel(item);
|
||||||
@@ -5129,10 +5176,56 @@ export default {
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
.loading-route-change-dialog__record-content {
|
.loading-route-change-dialog__record-content {
|
||||||
white-space: pre-wrap;
|
display: flex;
|
||||||
overflow-wrap: anywhere;
|
flex-direction: column;
|
||||||
word-break: break-word;
|
gap: 10px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
|
white-space: normal;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.loading-route-change-dialog__change-group {
|
||||||
|
padding-left: 8px;
|
||||||
|
border-left: 2px solid #409eff;
|
||||||
|
}
|
||||||
|
.loading-route-change-dialog__change-group-title {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.loading-route-change-dialog__change-group-text {
|
||||||
|
color: #606266;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.loading-route-change-dialog__change-field {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 54px minmax(0, 1fr);
|
||||||
|
column-gap: 8px;
|
||||||
|
row-gap: 2px;
|
||||||
|
}
|
||||||
|
.loading-route-change-dialog__change-field-label {
|
||||||
|
color: #909399;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.loading-route-change-dialog__change-before,
|
||||||
|
.loading-route-change-dialog__change-single {
|
||||||
|
color: #606266;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.loading-route-change-dialog__change-arrow {
|
||||||
|
color: #c0c4cc;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.loading-route-change-dialog__change-after {
|
||||||
|
color: #409eff;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.loading-route-change-dialog__change-flag {
|
||||||
|
margin-left: 6px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.loading-route-change-map-dialog {
|
.loading-route-change-map-dialog {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
<p>{{ timeRange(row) }}</p>
|
<p>{{ timeRange(row) }}</p>
|
||||||
<p>{{ goodsSummary(row) }}</p>
|
<p>{{ goodsSummary(row) }}</p>
|
||||||
<p>创建人:{{ row.createUserName || '-' }}</p>
|
<p>创建人:{{ row.createUserName || '-' }}</p>
|
||||||
<p>创建时间:{{ row.createTime || '-' }}</p>
|
<p>创建时间:{{ formatDate(row.createTime) }}</p>
|
||||||
</section>
|
</section>
|
||||||
<section class="route-progress">
|
<section class="route-progress">
|
||||||
<div class="route-track">
|
<div class="route-track">
|
||||||
@@ -155,6 +155,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as api from '@/api/business/master-order';
|
import * as api from '@/api/business/master-order';
|
||||||
|
import { splitRegionTokens } from '@/utils/region-address';
|
||||||
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||||||
import MasterOrderEditor from './components/master-order-editor.vue';
|
import MasterOrderEditor from './components/master-order-editor.vue';
|
||||||
import MasterOrderDispatch from './components/master-order-dispatch.vue';
|
import MasterOrderDispatch from './components/master-order-dispatch.vue';
|
||||||
@@ -332,6 +333,10 @@ export default {
|
|||||||
? `${String(row.planStartTime).slice(0, 10)} - ${String(row.planEndTime).slice(0, 10)}`
|
? `${String(row.planStartTime).slice(0, 10)} - ${String(row.planEndTime).slice(0, 10)}`
|
||||||
: '-';
|
: '-';
|
||||||
},
|
},
|
||||||
|
// 创建时间只展示日期。
|
||||||
|
formatDate(value) {
|
||||||
|
return value ? String(value).slice(0, 10) : '-';
|
||||||
|
},
|
||||||
goodsSummary(row) {
|
goodsSummary(row) {
|
||||||
const item = (row.goods || [])[0] || {};
|
const item = (row.goods || [])[0] || {};
|
||||||
if (!item.cargoName && !item.cargoType) return '-';
|
if (!item.cargoName && !item.cargoType) return '-';
|
||||||
@@ -391,6 +396,26 @@ export default {
|
|||||||
}`;
|
}`;
|
||||||
return `${city} ${district}`;
|
return `${city} ${district}`;
|
||||||
},
|
},
|
||||||
|
// 悬停气泡里的地址:起讫点若只保存了名称或详细地址的一半,气泡就会显示成
|
||||||
|
// “柳南区”“安徽”这类残缺地址,故把两个字段互为补充后展示。
|
||||||
|
completeAddress(name, address) {
|
||||||
|
const rawName = String(name || '').trim();
|
||||||
|
const rawAddress = String(address || '').trim();
|
||||||
|
if (!rawName) return rawAddress;
|
||||||
|
if (!rawAddress) return rawName;
|
||||||
|
// 详细地址已含省市,本身就是完整地址,不再拼接名称,避免重复卡片上已有的站点名。
|
||||||
|
if (/(?:省|自治区|特别行政区|市)/.test(rawAddress)) return rawAddress;
|
||||||
|
const normalizedName = rawName.replace(/\s+/g, '');
|
||||||
|
const normalizedAddress = rawAddress.replace(/\s+/g, '');
|
||||||
|
if (normalizedAddress.includes(normalizedName)) return rawAddress;
|
||||||
|
if (normalizedName.includes(normalizedAddress)) return rawName;
|
||||||
|
// “珠海1号港口”与“珠海港口1号”这类同字异序写法只保留一个。
|
||||||
|
if ([...normalizedName].sort().join('') === [...normalizedAddress].sort().join('')) {
|
||||||
|
return normalizedName.length >= normalizedAddress.length ? rawName : rawAddress;
|
||||||
|
}
|
||||||
|
// 区域在前、详细地址在后。
|
||||||
|
return splitRegionTokens(rawName) ? `${rawName}${rawAddress}` : `${rawAddress}${rawName}`;
|
||||||
|
},
|
||||||
routeNodeName(value, address, transportType, region = {}) {
|
routeNodeName(value, address, transportType, region = {}) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (!text) return '-';
|
if (!text) return '-';
|
||||||
@@ -457,15 +482,12 @@ export default {
|
|||||||
if (this.isStationLikeName(rawAddress) && !this.isStationLikeName(rawName)) {
|
if (this.isStationLikeName(rawAddress) && !this.isStationLikeName(rawName)) {
|
||||||
return {
|
return {
|
||||||
name: rawAddress,
|
name: rawAddress,
|
||||||
address: rawName || regionText || rawAddress,
|
address: this.completeAddress(rawName, rawAddress) || regionText || rawAddress,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const displayName = rawName || rawAddress || '-';
|
const displayName = rawName || rawAddress || '-';
|
||||||
const tipAddress =
|
const tipAddress =
|
||||||
(rawAddress && rawAddress !== displayName ? rawAddress : '') ||
|
this.completeAddress(rawName, rawAddress) || regionText || rawAddress || displayName;
|
||||||
regionText ||
|
|
||||||
rawAddress ||
|
|
||||||
displayName;
|
|
||||||
return { name: displayName, address: tipAddress };
|
return { name: displayName, address: tipAddress };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -475,7 +497,7 @@ export default {
|
|||||||
siteCode,
|
siteCode,
|
||||||
nextTransportType,
|
nextTransportType,
|
||||||
}),
|
}),
|
||||||
address: rawAddress || rawName || regionText || '-',
|
address: this.completeAddress(rawName, rawAddress) || regionText || '-',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
routeNodes(row = {}) {
|
routeNodes(row = {}) {
|
||||||
|
|||||||
@@ -416,6 +416,7 @@ import { getList as getProjectList } from '@/api/business/project-apply';
|
|||||||
import { getDeptTree } from '@/api/system/dept';
|
import { getDeptTree } from '@/api/system/dept';
|
||||||
import { exportBlob } from '@/api/common';
|
import { exportBlob } from '@/api/common';
|
||||||
import { config, option } from '@/option/business/process-config';
|
import { config, option } from '@/option/business/process-config';
|
||||||
|
import { applyIncomingSearch } from '@/utils/search-range';
|
||||||
import { getToken, isAdministrator } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
@@ -603,7 +604,8 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
normalizeListQuery(params = {}) {
|
normalizeListQuery(params = {}) {
|
||||||
const query = { ...params, ...this.query };
|
// 新条件覆盖旧条件,清空的筛选项不再沿用上一次的值。
|
||||||
|
const query = applyIncomingSearch(this.query, params);
|
||||||
const rawDeptId = query.deptId ?? query.searchDeptId;
|
const rawDeptId = query.deptId ?? query.searchDeptId;
|
||||||
delete query.searchDeptId;
|
delete query.searchDeptId;
|
||||||
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
|
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
|
||||||
|
|||||||
@@ -231,6 +231,9 @@
|
|||||||
<el-select
|
<el-select
|
||||||
v-model="form.undertakeDeptId"
|
v-model="form.undertakeDeptId"
|
||||||
placeholder="请选择"
|
placeholder="请选择"
|
||||||
|
multiple
|
||||||
|
collapse-tags
|
||||||
|
collapse-tags-tooltip
|
||||||
clearable
|
clearable
|
||||||
filterable
|
filterable
|
||||||
:disabled="isBasicInfoReadonly"
|
:disabled="isBasicInfoReadonly"
|
||||||
@@ -956,7 +959,7 @@ const emptyForm = () => ({
|
|||||||
projectShortName: '',
|
projectShortName: '',
|
||||||
businessDeptId: '',
|
businessDeptId: '',
|
||||||
businessDeptName: '',
|
businessDeptName: '',
|
||||||
undertakeDeptId: '',
|
undertakeDeptId: [],
|
||||||
undertakeDeptName: '',
|
undertakeDeptName: '',
|
||||||
projectSource: '',
|
projectSource: '',
|
||||||
sourceRemark: '',
|
sourceRemark: '',
|
||||||
@@ -1146,7 +1149,15 @@ export default {
|
|||||||
{ max: 20, message: '项目简称不能超过20个字符', trigger: 'blur' },
|
{ max: 20, message: '项目简称不能超过20个字符', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
businessDeptId: [{ required: true, message: '请选择业务部门', trigger: 'change' }],
|
businessDeptId: [{ required: true, message: '请选择业务部门', trigger: 'change' }],
|
||||||
undertakeDeptId: [{ required: true, message: '请选择平台公司', trigger: 'change' }],
|
undertakeDeptId: [
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
required: true,
|
||||||
|
min: 1,
|
||||||
|
message: '请选择平台公司',
|
||||||
|
trigger: 'change',
|
||||||
|
},
|
||||||
|
],
|
||||||
businessMode: [{ required: true, message: '请选择业务模式', trigger: 'change' }],
|
businessMode: [{ required: true, message: '请选择业务模式', trigger: 'change' }],
|
||||||
customerNames: [{ required: true, message: '请选择客户名称', trigger: 'change' }],
|
customerNames: [{ required: true, message: '请选择客户名称', trigger: 'change' }],
|
||||||
carrierNames: [{ required: true, message: '请选择下游承运商', trigger: 'change' }],
|
carrierNames: [{ required: true, message: '请选择下游承运商', trigger: 'change' }],
|
||||||
@@ -1508,13 +1519,13 @@ export default {
|
|||||||
this.businessDeptTreeOptions = [{ label, value: detail.businessDeptId }];
|
this.businessDeptTreeOptions = [{ label, value: detail.businessDeptId }];
|
||||||
this.deptOptions = [{ label, rawLabel: label, value: detail.businessDeptId }];
|
this.deptOptions = [{ label, rawLabel: label, value: detail.businessDeptId }];
|
||||||
}
|
}
|
||||||
if (detail.undertakeDeptId) {
|
const undertakeDeptIds = this.parseUndertakeDeptIds(detail.undertakeDeptId);
|
||||||
this.platformCompanyOptions = [
|
const undertakeDeptNames = this.parseUndertakeDeptNames(detail.undertakeDeptName);
|
||||||
{
|
if (undertakeDeptIds.length) {
|
||||||
label: detail.undertakeDeptName || String(detail.undertakeDeptId),
|
this.platformCompanyOptions = undertakeDeptIds.map((id, index) => ({
|
||||||
value: detail.undertakeDeptId,
|
label: undertakeDeptNames[index] || String(id),
|
||||||
},
|
value: id,
|
||||||
];
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resolvePublicDictOptions(options, currentValue) {
|
resolvePublicDictOptions(options, currentValue) {
|
||||||
@@ -1739,6 +1750,7 @@ export default {
|
|||||||
bizId: row.id,
|
bizId: row.id,
|
||||||
subjectName: row.projectName || '',
|
subjectName: row.projectName || '',
|
||||||
approvalStatus: row.approvalStatus || '',
|
approvalStatus: row.approvalStatus || '',
|
||||||
|
mkEnabled: this.mkEnabled,
|
||||||
}).then(() => request())
|
}).then(() => request())
|
||||||
: request();
|
: request();
|
||||||
afterMk.then(res => {
|
afterMk.then(res => {
|
||||||
@@ -1860,6 +1872,8 @@ export default {
|
|||||||
this.form = {
|
this.form = {
|
||||||
...form,
|
...form,
|
||||||
...situation,
|
...situation,
|
||||||
|
undertakeDeptId: this.parseUndertakeDeptIds(form.undertakeDeptId),
|
||||||
|
undertakeDeptName: form.undertakeDeptName || '',
|
||||||
customerNames: this.getCustomerRowNames(customerRows, 'customer') || form.customerNames,
|
customerNames: this.getCustomerRowNames(customerRows, 'customer') || form.customerNames,
|
||||||
carrierNames: this.getCustomerRowNames(carrierRows, 'carrier') || form.carrierNames,
|
carrierNames: this.getCustomerRowNames(carrierRows, 'carrier') || form.carrierNames,
|
||||||
};
|
};
|
||||||
@@ -1943,6 +1957,7 @@ export default {
|
|||||||
bizId: id,
|
bizId: id,
|
||||||
subjectName: name,
|
subjectName: name,
|
||||||
approvalStatus: status,
|
approvalStatus: status,
|
||||||
|
mkEnabled: this.mkEnabled,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.$message.success('提交成功!');
|
this.$message.success('提交成功!');
|
||||||
this.closeProjectForm();
|
this.closeProjectForm();
|
||||||
@@ -2014,6 +2029,7 @@ export default {
|
|||||||
bizId: id,
|
bizId: id,
|
||||||
subjectName: this.form.projectName || '',
|
subjectName: this.form.projectName || '',
|
||||||
approvalStatus: this.form.approvalStatus || 'change_rejected',
|
approvalStatus: this.form.approvalStatus || 'change_rejected',
|
||||||
|
mkEnabled: this.mkEnabled,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.$message.success('提交成功!');
|
this.$message.success('提交成功!');
|
||||||
this.closeProjectForm();
|
this.closeProjectForm();
|
||||||
@@ -2026,8 +2042,13 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
normalizeSubmitForm(options = {}) {
|
normalizeSubmitForm(options = {}) {
|
||||||
|
const undertakeDeptIds = this.parseUndertakeDeptIds(this.form.undertakeDeptId);
|
||||||
|
const undertakeDeptName =
|
||||||
|
this.resolveUndertakeDeptNames(undertakeDeptIds) || this.form.undertakeDeptName || '';
|
||||||
const submitRow = {
|
const submitRow = {
|
||||||
...this.form,
|
...this.form,
|
||||||
|
undertakeDeptId: undertakeDeptIds.join(','),
|
||||||
|
undertakeDeptName,
|
||||||
fundLimit: this.formatAmount(this.form.fundLimit),
|
fundLimit: this.formatAmount(this.form.fundLimit),
|
||||||
receivableLimit: this.formatAmount(this.form.receivableLimit),
|
receivableLimit: this.formatAmount(this.form.receivableLimit),
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
@@ -2419,11 +2440,40 @@ export default {
|
|||||||
this.$refs.projectForm?.validateField('businessDeptId');
|
this.$refs.projectForm?.validateField('businessDeptId');
|
||||||
},
|
},
|
||||||
handleUndertakeDeptChange(value) {
|
handleUndertakeDeptChange(value) {
|
||||||
const id = Array.isArray(value) ? value.at(-1) : value;
|
const ids = this.parseUndertakeDeptIds(value);
|
||||||
const dept = this.platformCompanyOptions.find(item => String(item.value) === String(id));
|
this.form.undertakeDeptId = ids;
|
||||||
this.form.undertakeDeptName = dept ? dept.label : '';
|
this.form.undertakeDeptName = this.resolveUndertakeDeptNames(ids);
|
||||||
this.$refs.projectForm?.validateField('undertakeDeptId');
|
this.$refs.projectForm?.validateField('undertakeDeptId');
|
||||||
},
|
},
|
||||||
|
parseUndertakeDeptIds(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
.map(item => String(item ?? '').trim())
|
||||||
|
.filter(item => item && item !== '-1');
|
||||||
|
}
|
||||||
|
return String(value ?? '')
|
||||||
|
.split(/[,,]/)
|
||||||
|
.map(item => item.trim())
|
||||||
|
.filter(item => item && item !== '-1');
|
||||||
|
},
|
||||||
|
parseUndertakeDeptNames(value) {
|
||||||
|
return String(value ?? '')
|
||||||
|
.split(/[,,、]/)
|
||||||
|
.map(item => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
},
|
||||||
|
resolveUndertakeDeptNames(ids = []) {
|
||||||
|
return ids
|
||||||
|
.map(id => {
|
||||||
|
const dept = this.platformCompanyOptions.find(item => String(item.value) === String(id));
|
||||||
|
return dept?.label || '';
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('、');
|
||||||
|
},
|
||||||
|
firstUndertakeDeptId(value) {
|
||||||
|
return this.parseUndertakeDeptIds(value)[0] || '';
|
||||||
|
},
|
||||||
// 由部门 id 反查「根 → 该节点」的 id 路径,供级联回显(找不到返回 null)
|
// 由部门 id 反查「根 → 该节点」的 id 路径,供级联回显(找不到返回 null)
|
||||||
findDeptPath(id, tree = [], parents = []) {
|
findDeptPath(id, tree = [], parents = []) {
|
||||||
const target = Array.isArray(id) ? id.at(-1) : id;
|
const target = Array.isArray(id) ? id.at(-1) : id;
|
||||||
@@ -2456,21 +2506,30 @@ export default {
|
|||||||
const list = res.data.data || [];
|
const list = res.data.data || [];
|
||||||
this.platformCompanyOptions = list.map(item => ({
|
this.platformCompanyOptions = list.map(item => ({
|
||||||
label: item.deptName || item.fullName || item.title || item.name,
|
label: item.deptName || item.fullName || item.title || item.name,
|
||||||
value: item.id,
|
// 雪花 ID 必须以字符串绑定,避免 JS Number 精度丢失导致多选提交异常
|
||||||
}));
|
value: item.id == null || item.id === '' ? '' : String(item.id),
|
||||||
|
})).filter(item => item.value);
|
||||||
this.ensureCurrentPlatformOption();
|
this.ensureCurrentPlatformOption();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
ensureCurrentPlatformOption() {
|
ensureCurrentPlatformOption() {
|
||||||
const id = this.form.undertakeDeptId;
|
const ids = this.parseUndertakeDeptIds(this.form.undertakeDeptId);
|
||||||
if (!id) return;
|
if (!ids.length) return;
|
||||||
const exists = this.platformCompanyOptions.some(item => String(item.value) === String(id));
|
const names = this.parseUndertakeDeptNames(this.form.undertakeDeptName);
|
||||||
if (!exists) {
|
ids.forEach((id, index) => {
|
||||||
this.platformCompanyOptions.unshift({
|
const exists = this.platformCompanyOptions.some(item => String(item.value) === String(id));
|
||||||
label: this.form.undertakeDeptName || String(id),
|
if (!exists) {
|
||||||
value: id,
|
this.platformCompanyOptions.unshift({
|
||||||
});
|
label: names[index] || String(id),
|
||||||
}
|
value: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 选项 value 与已选 ID 类型对齐,避免回显失败
|
||||||
|
this.form.undertakeDeptId = ids.map(id => {
|
||||||
|
const matched = this.platformCompanyOptions.find(item => String(item.value) === String(id));
|
||||||
|
return matched ? matched.value : id;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
excludeExternalOrganization(list = []) {
|
excludeExternalOrganization(list = []) {
|
||||||
return list.reduce((result, item) => {
|
return list.reduce((result, item) => {
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ import * as api from '@/api/payment/billPayment';
|
|||||||
import * as billLedgerApi from '@/api/payment/billLedger';
|
import * as billLedgerApi from '@/api/payment/billLedger';
|
||||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||||
import { getMkPublicDetail } from '@/api/mk-process';
|
import { getMkPublicDetail } from '@/api/mk-process';
|
||||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||||
|
|
||||||
const emptyForm = () => ({
|
const emptyForm = () => ({
|
||||||
id: null,
|
id: null,
|
||||||
@@ -348,6 +348,7 @@ export default {
|
|||||||
bizId: this.form.id,
|
bizId: this.form.id,
|
||||||
subjectName: this.form.paymentNo || this.form.billNo || '',
|
subjectName: this.form.paymentNo || this.form.billNo || '',
|
||||||
approvalStatus: previousApprovalStatus,
|
approvalStatus: previousApprovalStatus,
|
||||||
|
mkEnabled: await loadMkEnabled().catch(() => false),
|
||||||
});
|
});
|
||||||
this.$message.success('提交成功');
|
this.$message.success('提交成功');
|
||||||
this.goBack();
|
this.goBack();
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ export default {
|
|||||||
bizId: row.id,
|
bizId: row.id,
|
||||||
subjectName: row.paymentNo || row.billNo || '',
|
subjectName: row.paymentNo || row.billNo || '',
|
||||||
approvalStatus: previousApprovalStatus,
|
approvalStatus: previousApprovalStatus,
|
||||||
|
mkEnabled: this.mkEnabled,
|
||||||
});
|
});
|
||||||
this.$message.success('提交成功');
|
this.$message.success('提交成功');
|
||||||
this.loadTable();
|
this.loadTable();
|
||||||
|
|||||||
@@ -52,12 +52,13 @@
|
|||||||
<el-icon><Delete /></el-icon>
|
<el-icon><Delete /></el-icon>
|
||||||
<span>重置</span>
|
<span>重置</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-tooltip :content="expanded ? '折叠' : '展开'" placement="top">
|
<el-button
|
||||||
<el-button text @click="expanded = !expanded">
|
v-if="showSearchToggle"
|
||||||
<el-icon><component :is="expanded ? ArrowUp : ArrowDown" /></el-icon>
|
text
|
||||||
<span>{{ expanded ? '折叠' : '展开' }}</span>
|
:icon="expanded ? ArrowUp : ArrowDown"
|
||||||
</el-button>
|
@click="expanded = !expanded"
|
||||||
</el-tooltip>
|
>{{ expanded ? '收起' : '展开' }}</el-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
</section>
|
</section>
|
||||||
@@ -81,10 +82,18 @@ export default {
|
|||||||
Delete,
|
Delete,
|
||||||
Search,
|
Search,
|
||||||
expanded: false,
|
expanded: false,
|
||||||
|
// 与 avue-crud 一致:搜索项数量超过 searchIndex(默认一行 4 项)时才显示“展开/收起”
|
||||||
|
searchIndex: 4,
|
||||||
|
searchItemCount: 6,
|
||||||
billTypeOptions,
|
billTypeOptions,
|
||||||
maturityStatusOptions,
|
maturityStatusOptions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
showSearchToggle() {
|
||||||
|
return this.searchItemCount > this.searchIndex;
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -43,11 +43,13 @@
|
|||||||
<div class="bill-payment-search__actions">
|
<div class="bill-payment-search__actions">
|
||||||
<el-button type="primary" :loading="loading" @click="$emit('search')">查询</el-button>
|
<el-button type="primary" :loading="loading" @click="$emit('search')">查询</el-button>
|
||||||
<el-button @click="$emit('reset')">重置</el-button>
|
<el-button @click="$emit('reset')">重置</el-button>
|
||||||
<el-tooltip :content="expanded ? '折叠' : '展开'" placement="top">
|
<el-button
|
||||||
<el-button text @click="expanded = !expanded">
|
v-if="showSearchToggle"
|
||||||
<el-icon><component :is="expanded ? ArrowUp : ArrowDown" /></el-icon>
|
text
|
||||||
</el-button>
|
:icon="expanded ? ArrowUp : ArrowDown"
|
||||||
</el-tooltip>
|
@click="expanded = !expanded"
|
||||||
|
>{{ expanded ? '收起' : '展开' }}</el-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
</section>
|
</section>
|
||||||
@@ -67,7 +69,20 @@ export default {
|
|||||||
},
|
},
|
||||||
emits: ['search', 'reset'],
|
emits: ['search', 'reset'],
|
||||||
data() {
|
data() {
|
||||||
return { ArrowDown, ArrowUp, expanded: false, approvalStatusOptions };
|
return {
|
||||||
|
ArrowDown,
|
||||||
|
ArrowUp,
|
||||||
|
expanded: false,
|
||||||
|
// 与 avue-crud 一致:搜索项数量超过 searchIndex(默认一行 4 项)时才显示“展开/收起”
|
||||||
|
searchIndex: 4,
|
||||||
|
searchItemCount: 4,
|
||||||
|
approvalStatusOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
showSearchToggle() {
|
||||||
|
return this.searchItemCount > this.searchIndex;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.loadOrganizationOptions();
|
this.loadOrganizationOptions();
|
||||||
|
|||||||
@@ -492,13 +492,14 @@ import { getDictionary } from '@/api/system/dictbiz';
|
|||||||
import { invoiceApplicationDetailColumns } from '@/option/payment/invoiceApplication';
|
import { invoiceApplicationDetailColumns } from '@/option/payment/invoiceApplication';
|
||||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||||
import { getMkPublicDetail } from '@/api/mk-process';
|
import { getMkPublicDetail } from '@/api/mk-process';
|
||||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||||
|
|
||||||
let localId = 0;
|
let localId = 0;
|
||||||
const emptyLine = () => ({
|
const emptyLine = () => ({
|
||||||
localId: ++localId,
|
localId: ++localId,
|
||||||
goodsCategory: '',
|
goodsCategory: '',
|
||||||
goodsName: '',
|
goodsName: '',
|
||||||
|
taxClassificationCode: '',
|
||||||
unit: '吨',
|
unit: '吨',
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
unitPriceNoTax: 0,
|
unitPriceNoTax: 0,
|
||||||
@@ -742,9 +743,11 @@ export default {
|
|||||||
const item = this.findInvoiceItem(line);
|
const item = this.findInvoiceItem(line);
|
||||||
if (item) {
|
if (item) {
|
||||||
line.taxRate = Number(item.defaultTaxRate || 0);
|
line.taxRate = Number(item.defaultTaxRate || 0);
|
||||||
|
line.taxClassificationCode = item.taxClassificationCode || '';
|
||||||
} else if (clearInvalid) {
|
} else if (clearInvalid) {
|
||||||
line.goodsName = '';
|
line.goodsName = '';
|
||||||
line.taxRate = 0;
|
line.taxRate = 0;
|
||||||
|
line.taxClassificationCode = '';
|
||||||
}
|
}
|
||||||
this.recalculateLine(line);
|
this.recalculateLine(line);
|
||||||
},
|
},
|
||||||
@@ -752,6 +755,7 @@ export default {
|
|||||||
const names = this.invoiceItemNames(line.goodsCategory);
|
const names = this.invoiceItemNames(line.goodsCategory);
|
||||||
if (!names.some(item => item.shortName === line.goodsName)) line.goodsName = '';
|
if (!names.some(item => item.shortName === line.goodsName)) line.goodsName = '';
|
||||||
line.taxRate = 0;
|
line.taxRate = 0;
|
||||||
|
line.taxClassificationCode = this.findInvoiceItem(line)?.taxClassificationCode || '';
|
||||||
if (names.length === 1) {
|
if (names.length === 1) {
|
||||||
line.goodsName = names[0].shortName;
|
line.goodsName = names[0].shortName;
|
||||||
this.handleGoodsNameChange(line);
|
this.handleGoodsNameChange(line);
|
||||||
@@ -761,9 +765,11 @@ export default {
|
|||||||
const item = this.findInvoiceItem(line);
|
const item = this.findInvoiceItem(line);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
line.taxRate = 0;
|
line.taxRate = 0;
|
||||||
|
line.taxClassificationCode = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
line.taxRate = Number(item.defaultTaxRate || 0);
|
line.taxRate = Number(item.defaultTaxRate || 0);
|
||||||
|
line.taxClassificationCode = item.taxClassificationCode || '';
|
||||||
this.recalculateLine(line);
|
this.recalculateLine(line);
|
||||||
},
|
},
|
||||||
validateSettlements(rule, value, callback) {
|
validateSettlements(rule, value, callback) {
|
||||||
@@ -1159,6 +1165,7 @@ export default {
|
|||||||
lines: sheet.lines.map(line => ({
|
lines: sheet.lines.map(line => ({
|
||||||
goodsCategory: line.goodsCategory,
|
goodsCategory: line.goodsCategory,
|
||||||
goodsName: line.goodsName,
|
goodsName: line.goodsName,
|
||||||
|
taxClassificationCode: line.taxClassificationCode,
|
||||||
unit: line.unit,
|
unit: line.unit,
|
||||||
quantity: line.quantity,
|
quantity: line.quantity,
|
||||||
unitPriceNoTax: line.unitPriceNoTax,
|
unitPriceNoTax: line.unitPriceNoTax,
|
||||||
@@ -1201,6 +1208,7 @@ export default {
|
|||||||
bizId: this.form.id,
|
bizId: this.form.id,
|
||||||
subjectName: this.form.applicationNo || '',
|
subjectName: this.form.applicationNo || '',
|
||||||
approvalStatus: previousApprovalStatus,
|
approvalStatus: previousApprovalStatus,
|
||||||
|
mkEnabled: await loadMkEnabled().catch(() => false),
|
||||||
});
|
});
|
||||||
this.$message.success('提交成功');
|
this.$message.success('提交成功');
|
||||||
this.goBack();
|
this.goBack();
|
||||||
|
|||||||
@@ -45,11 +45,13 @@
|
|||||||
<div class="invoice-page__search-actions">
|
<div class="invoice-page__search-actions">
|
||||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
<el-button @click="resetSearch">重置</el-button>
|
<el-button @click="resetSearch">重置</el-button>
|
||||||
<el-tooltip :content="searchExpanded ? '折叠' : '展开'" placement="top">
|
<el-button
|
||||||
<el-button text @click="searchExpanded = !searchExpanded">
|
v-if="showSearchToggle"
|
||||||
<el-icon><component :is="searchExpanded ? ArrowUp : ArrowDown" /></el-icon>
|
text
|
||||||
</el-button>
|
:icon="searchExpanded ? ArrowUp : ArrowDown"
|
||||||
</el-tooltip>
|
@click="searchExpanded = !searchExpanded"
|
||||||
|
>{{ searchExpanded ? '收起' : '展开' }}</el-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
</section>
|
</section>
|
||||||
@@ -61,6 +63,7 @@
|
|||||||
v-if="hasPermission('invoice_application_sync')"
|
v-if="hasPermission('invoice_application_sync')"
|
||||||
type="primary"
|
type="primary"
|
||||||
plain
|
plain
|
||||||
|
:loading="batchSyncing"
|
||||||
@click="handleBatchSync"
|
@click="handleBatchSync"
|
||||||
>批量同步</el-button
|
>批量同步</el-button
|
||||||
>
|
>
|
||||||
@@ -122,6 +125,12 @@
|
|||||||
{{ displayValue(row[column.prop]) }}
|
{{ displayValue(row[column.prop]) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||||
|
<template v-else-if="column.prop === 'kingdeeLockStatus'">
|
||||||
|
<el-tooltip v-if="lockTag(row).show" :content="lockTag(row).tip" placement="top">
|
||||||
|
<el-tag :type="lockTag(row).type" class="status-text">{{ lockTag(row).label }}</el-tag>
|
||||||
|
</el-tooltip>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -190,7 +199,9 @@
|
|||||||
>
|
>
|
||||||
<el-link
|
<el-link
|
||||||
v-if="
|
v-if="
|
||||||
hasPermission('invoice_application_void') && row.approvalStatus === 'approved'
|
hasPermission('invoice_application_void') &&
|
||||||
|
row.approvalStatus === 'approved' &&
|
||||||
|
row.kingdeeLockStatus !== 'locked'
|
||||||
"
|
"
|
||||||
type="danger"
|
type="danger"
|
||||||
@click="handleVoid(row)"
|
@click="handleVoid(row)"
|
||||||
@@ -247,6 +258,9 @@ export default {
|
|||||||
ArrowUp,
|
ArrowUp,
|
||||||
query: emptyQuery(),
|
query: emptyQuery(),
|
||||||
searchExpanded: false,
|
searchExpanded: false,
|
||||||
|
// 与 avue-crud 一致:搜索项数量超过 searchIndex(默认一行 4 项)时才显示“展开/收起”
|
||||||
|
searchIndex: 4,
|
||||||
|
searchItemCount: 5,
|
||||||
searchVisible: true,
|
searchVisible: true,
|
||||||
tableSize: 'default',
|
tableSize: 'default',
|
||||||
approvalStatusOptions: api.approvalStatusOptions,
|
approvalStatusOptions: api.approvalStatusOptions,
|
||||||
@@ -256,6 +270,7 @@ export default {
|
|||||||
rows: [],
|
rows: [],
|
||||||
selection: [],
|
selection: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
|
batchSyncing: false,
|
||||||
page: { current: 1, size: 10, total: 0 },
|
page: { current: 1, size: 10, total: 0 },
|
||||||
mkEnabled: false,
|
mkEnabled: false,
|
||||||
};
|
};
|
||||||
@@ -265,6 +280,9 @@ export default {
|
|||||||
visibleColumns() {
|
visibleColumns() {
|
||||||
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
||||||
},
|
},
|
||||||
|
showSearchToggle() {
|
||||||
|
return this.searchItemCount > this.searchIndex;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.loadOrganizationOptions();
|
this.loadOrganizationOptions();
|
||||||
@@ -366,6 +384,7 @@ export default {
|
|||||||
bizId: row.id,
|
bizId: row.id,
|
||||||
subjectName: row.applicationNo || '',
|
subjectName: row.applicationNo || '',
|
||||||
approvalStatus: previousApprovalStatus,
|
approvalStatus: previousApprovalStatus,
|
||||||
|
mkEnabled: this.mkEnabled,
|
||||||
});
|
});
|
||||||
this.$message.success('提交成功');
|
this.$message.success('提交成功');
|
||||||
this.loadTable();
|
this.loadTable();
|
||||||
@@ -390,8 +409,13 @@ export default {
|
|||||||
},
|
},
|
||||||
async handleApprove(row) {
|
async handleApprove(row) {
|
||||||
await this.$confirm('确认审批通过该开票申请?', '提示', { type: 'warning' });
|
await this.$confirm('确认审批通过该开票申请?', '提示', { type: 'warning' });
|
||||||
await api.approve({ id: row.id });
|
const res = await api.approve({ id: row.id });
|
||||||
this.$message.success('审批通过');
|
// data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
|
||||||
|
if (res.data.data === false) {
|
||||||
|
this.$message.warning(res.data.msg);
|
||||||
|
} else {
|
||||||
|
this.$message.success(res.data.msg || '审批通过');
|
||||||
|
}
|
||||||
this.loadTable();
|
this.loadTable();
|
||||||
},
|
},
|
||||||
async handleReturn(row) {
|
async handleReturn(row) {
|
||||||
@@ -407,14 +431,31 @@ export default {
|
|||||||
this.loadTable();
|
this.loadTable();
|
||||||
},
|
},
|
||||||
async handleBatchSync() {
|
async handleBatchSync() {
|
||||||
|
if (this.batchSyncing) return;
|
||||||
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
|
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
this.$message.warning('请选择至少一条审批通过的开票申请');
|
this.$message.warning('请选择至少一条审批通过的开票申请');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await Promise.all(rows.map(row => api.syncKingdee(row.id)));
|
this.batchSyncing = true;
|
||||||
this.$message.success(`已同步${rows.length}条开票申请`);
|
try {
|
||||||
this.loadTable();
|
await Promise.all(rows.map(row => api.syncKingdee(row.id)));
|
||||||
|
this.$message.success(`已同步${rows.length}条开票申请`);
|
||||||
|
await this.loadTable();
|
||||||
|
// 附件联带同步提示:同步成功但附件未全部上传时追加 warning,重推即自动补传
|
||||||
|
const idSet = new Set(rows.map(row => String(row.id)));
|
||||||
|
const pendingCount = (this.rows || []).filter(row => idSet.has(String(row.id))
|
||||||
|
&& ['pending', 'partial'].includes(row.attachmentSyncStatus)).length;
|
||||||
|
if (pendingCount) {
|
||||||
|
this.$message({
|
||||||
|
type: 'warning',
|
||||||
|
message: `其中${pendingCount}单附件未全部上传,可重推补传`,
|
||||||
|
duration: 8000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.batchSyncing = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async handleExport() {
|
async handleExport() {
|
||||||
const data = this.unwrapData(await api.getList(1, 100000, this.query));
|
const data = this.unwrapData(await api.getList(1, 100000, this.query));
|
||||||
@@ -444,6 +485,38 @@ export default {
|
|||||||
''
|
''
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
lockTag(row) {
|
||||||
|
if (row.kingdeeLockStatus === 'locked') {
|
||||||
|
return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' };
|
||||||
|
}
|
||||||
|
if (row.kingdeeLockStatus === 'failed') {
|
||||||
|
if ((row.kingdeeTryCount || 0) >= 5) {
|
||||||
|
return {
|
||||||
|
show: true,
|
||||||
|
type: 'danger',
|
||||||
|
label: '锁定失败·已达上限',
|
||||||
|
tip: row.kingdeeLockFailReason || '自动补偿次数已用完,请重推「同步金蝶」立即补锁',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
show: true,
|
||||||
|
type: 'warning',
|
||||||
|
label: '锁定失败·补偿中',
|
||||||
|
tip: row.kingdeeLockFailReason || '系统将自动补偿重锁',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 金蝶解锁终态标识:删单解锁的源单已作废,此标签用于区分人工作废与金蝶删单
|
||||||
|
if (row.kingdeeUnlockTime) {
|
||||||
|
const isDelete = row.kingdeeUnlockOperationName === '金蝶删单';
|
||||||
|
return {
|
||||||
|
show: true,
|
||||||
|
type: isDelete ? 'danger' : 'warning',
|
||||||
|
label: row.kingdeeUnlockOperationName || '金蝶解锁',
|
||||||
|
tip: `${row.kingdeeUnlockOperator || '-'} 于 ${this.$dayjs(row.kingdeeUnlockTime).format('YYYY-MM-DD HH:mm:ss')} 解锁`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { show: false };
|
||||||
|
},
|
||||||
displayValue(value) {
|
displayValue(value) {
|
||||||
return value === null || value === undefined || value === '' ? '-' : value;
|
return value === null || value === undefined || value === '' ? '-' : value;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="开票金额">
|
<el-form-item label="开票金额">
|
||||||
<el-input
|
<el-input
|
||||||
:model-value="form.kingdeeInvoicePoolId ? formatMoney(form.invoiceAmount) : ''"
|
:model-value="form.invoiceNo ? formatMoney(form.invoiceAmount) : ''"
|
||||||
disabled
|
disabled
|
||||||
placeholder="从金蝶票据池带出"
|
placeholder="从金蝶票据池带出"
|
||||||
/>
|
/>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="税额">
|
<el-form-item label="税额">
|
||||||
<el-input
|
<el-input
|
||||||
:model-value="form.kingdeeInvoicePoolId ? formatMoney(form.taxAmount) : ''"
|
:model-value="form.invoiceNo ? formatMoney(form.taxAmount) : ''"
|
||||||
disabled
|
disabled
|
||||||
placeholder="从金蝶票据池带出"
|
placeholder="从金蝶票据池带出"
|
||||||
/>
|
/>
|
||||||
@@ -118,7 +118,13 @@
|
|||||||
</section-card>
|
</section-card>
|
||||||
|
|
||||||
<section-card title="附件信息">
|
<section-card title="附件信息">
|
||||||
<vehicle-attachment-table v-model="form.attachments" :readonly="readonly" />
|
<!-- 金蝶票池认领的发票附件为 piaozone path 型,需带发票号走 A801-2 代理查看/下载 -->
|
||||||
|
<vehicle-attachment-table
|
||||||
|
v-model="form.attachments"
|
||||||
|
:readonly="readonly"
|
||||||
|
show-view-actions
|
||||||
|
:kingdee-invoice-no="form.invoiceNo"
|
||||||
|
/>
|
||||||
</section-card>
|
</section-card>
|
||||||
|
|
||||||
<section-card title="备注">
|
<section-card title="备注">
|
||||||
@@ -149,62 +155,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<el-dialog v-model="invoiceDialog.visible" title="查询金蝶票据池" width="86%" append-to-body>
|
<kingdee-invoice-pool-dialog
|
||||||
<div class="receipt-form-page__dialog-search">
|
v-model="invoiceDialog.visible"
|
||||||
<el-input
|
title="查询金蝶票据池"
|
||||||
v-model="invoiceDialog.keyword"
|
:initial-invoice-no="form.invoiceNo"
|
||||||
clearable
|
:query-api="apiQueryInvoicePool"
|
||||||
placeholder="发票号码、开票单位或受票单位"
|
@confirm="handleInvoicePicked"
|
||||||
@keyup.enter="loadInvoicePool"
|
/>
|
||||||
/>
|
|
||||||
<el-button type="primary" @click="loadInvoicePool">查询</el-button>
|
|
||||||
</div>
|
|
||||||
<el-table
|
|
||||||
v-loading="invoiceDialog.loading"
|
|
||||||
:data="invoiceDialog.rows"
|
|
||||||
border
|
|
||||||
empty-text="暂无票据数据"
|
|
||||||
highlight-current-row
|
|
||||||
@current-change="invoiceDialog.current = $event"
|
|
||||||
@row-dblclick="confirmInvoice"
|
|
||||||
>
|
|
||||||
<el-table-column prop="invoiceNo" label="发票号码" min-width="180" />
|
|
||||||
<el-table-column prop="invoiceDate" label="开票日期" min-width="120" />
|
|
||||||
<el-table-column prop="invoiceType" label="发票类型" min-width="130" />
|
|
||||||
<el-table-column prop="issuerName" label="开票单位" min-width="170" />
|
|
||||||
<el-table-column prop="receiverName" label="受票单位" min-width="170" />
|
|
||||||
<el-table-column label="开票金额" min-width="130" align="right">
|
|
||||||
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="税率" min-width="100" align="center">
|
|
||||||
<template #default="{ row }">{{ formatRate(row.taxRate) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="税额" min-width="120" align="right">
|
|
||||||
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="150" />
|
|
||||||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-link type="primary" @click.stop="confirmInvoice(row)">选择</el-link>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div class="receipt-form-page__dialog-pagination">
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="invoiceDialog.page.current"
|
|
||||||
v-model:page-size="invoiceDialog.page.size"
|
|
||||||
:total="invoiceDialog.page.total"
|
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
@current-change="refreshInvoicePoolPage"
|
|
||||||
@size-change="handleInvoicePoolSizeChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="invoiceDialog.visible = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="confirmInvoice()">确定</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="settlementDialog.visible"
|
v-model="settlementDialog.visible"
|
||||||
@@ -259,82 +216,13 @@
|
|||||||
import { Search } from '@element-plus/icons-vue';
|
import { Search } from '@element-plus/icons-vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import * as api from '@/api/payment/invoiceReceipt';
|
import * as api from '@/api/payment/invoiceReceipt';
|
||||||
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
|
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
|
||||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||||
import { getMkPublicDetail } from '@/api/mk-process';
|
import { getMkPublicDetail } from '@/api/mk-process';
|
||||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||||
|
|
||||||
const mockInvoiceTypes = ['增值税专用发票', '增值税普通发票'];
|
|
||||||
const mockTaxRates = [3, 6, 9, 13];
|
|
||||||
const mockBankNames = ['中国工商银行', '中国农业银行', '中国银行', '中国建设银行', '招商银行'];
|
|
||||||
|
|
||||||
const randomDigits = length =>
|
|
||||||
Array.from({ length }, () => Math.floor(Math.random() * 10)).join('');
|
|
||||||
|
|
||||||
const formatMockDate = date => {
|
|
||||||
const year = date.getFullYear();
|
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
|
||||||
return `${year}-${month}-${day}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const customerName = customer =>
|
|
||||||
customer.fullName || customer.shortName || customer.customerCode || `客商${customer.id}`;
|
|
||||||
|
|
||||||
const customerEmail = (customer, index) => {
|
|
||||||
const prefix = String(customer.customerCode || customer.id || `customer${index + 1}`)
|
|
||||||
.replace(/[^a-zA-Z0-9_-]/g, '')
|
|
||||||
.toLowerCase();
|
|
||||||
return `finance@${prefix || `customer${index + 1}`}.example.com`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createInvoicePoolMockRows = (customers, counterpartyName, departmentEmail) => {
|
|
||||||
const rows = [];
|
|
||||||
const baseId = Date.now();
|
|
||||||
customers.forEach((customer, customerIndex) => {
|
|
||||||
const name = customerName(customer);
|
|
||||||
['issuer', 'receiver'].forEach((role, roleIndex) => {
|
|
||||||
const count = 3 + Math.floor(Math.random() * 5);
|
|
||||||
for (let index = 0; index < count; index += 1) {
|
|
||||||
const rowIndex = rows.length;
|
|
||||||
const invoiceDateValue = new Date();
|
|
||||||
invoiceDateValue.setDate(invoiceDateValue.getDate() - Math.floor(Math.random() * 90));
|
|
||||||
const invoiceDate = formatMockDate(invoiceDateValue);
|
|
||||||
const taxRate = mockTaxRates[(customerIndex + roleIndex + index) % mockTaxRates.length];
|
|
||||||
const invoiceAmount = Math.floor(10000 + Math.random() * 190000);
|
|
||||||
const taxAmount = Number(
|
|
||||||
((invoiceAmount * Number(taxRate)) / (100 + Number(taxRate))).toFixed(2)
|
|
||||||
);
|
|
||||||
const bankName = mockBankNames[(customerIndex + index) % mockBankNames.length];
|
|
||||||
rows.push({
|
|
||||||
id: baseId + rowIndex,
|
|
||||||
invoiceNo: `25${randomDigits(18)}`,
|
|
||||||
invoiceDate,
|
|
||||||
invoiceType: mockInvoiceTypes[(customerIndex + index) % mockInvoiceTypes.length],
|
|
||||||
taxRate,
|
|
||||||
invoiceAmount,
|
|
||||||
taxAmount,
|
|
||||||
receiverName: role === 'receiver' ? name : counterpartyName,
|
|
||||||
issuerName: role === 'issuer' ? name : counterpartyName,
|
|
||||||
bankName,
|
|
||||||
bankAccount: randomDigits(19),
|
|
||||||
issuingBank: `${bankName}营业部`,
|
|
||||||
phone: customer.contactPhone || '',
|
|
||||||
customerEmails: customerEmail(customer, customerIndex),
|
|
||||||
departmentEmails: departmentEmail || '',
|
|
||||||
attachmentsJson: '[]',
|
|
||||||
kingdeeBillNo: `KD-FP-${invoiceDate.replaceAll('-', '')}${randomDigits(5)}`,
|
|
||||||
kingdeeStatus: 'unsynced',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return rows;
|
|
||||||
};
|
|
||||||
|
|
||||||
const emptyForm = () => ({
|
const emptyForm = () => ({
|
||||||
id: null,
|
id: null,
|
||||||
kingdeeInvoicePoolId: null,
|
|
||||||
invoiceNo: '',
|
invoiceNo: '',
|
||||||
invoiceDate: '',
|
invoiceDate: '',
|
||||||
invoiceType: '',
|
invoiceType: '',
|
||||||
@@ -346,8 +234,6 @@ const emptyForm = () => ({
|
|||||||
bankName: '',
|
bankName: '',
|
||||||
bankAccount: '',
|
bankAccount: '',
|
||||||
issuingBank: '',
|
issuingBank: '',
|
||||||
kingdeeBillNo: '',
|
|
||||||
kingdeeStatus: 'unsynced',
|
|
||||||
phone: '',
|
phone: '',
|
||||||
customerEmails: [],
|
customerEmails: [],
|
||||||
departmentEmails: [],
|
departmentEmails: [],
|
||||||
@@ -363,24 +249,17 @@ const emptyForm = () => ({
|
|||||||
export default {
|
export default {
|
||||||
name: 'InvoiceReceiptForm',
|
name: 'InvoiceReceiptForm',
|
||||||
components: {
|
components: {
|
||||||
|
KingdeeInvoicePoolDialog,
|
||||||
VehicleAttachmentTable,
|
VehicleAttachmentTable,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
Search,
|
Search,
|
||||||
form: emptyForm(),
|
form: emptyForm(),
|
||||||
invoicePoolMockRows: [],
|
|
||||||
invoicePoolMockLoaded: false,
|
|
||||||
customerEmailOptions: [],
|
customerEmailOptions: [],
|
||||||
departmentEmailOptions: [],
|
departmentEmailOptions: [],
|
||||||
invoiceDialog: {
|
invoiceDialog: {
|
||||||
visible: false,
|
visible: false,
|
||||||
loading: false,
|
|
||||||
keyword: '',
|
|
||||||
sourceRows: [],
|
|
||||||
rows: [],
|
|
||||||
current: null,
|
|
||||||
page: { current: 1, size: 10, total: 0 },
|
|
||||||
},
|
},
|
||||||
settlementDialog: {
|
settlementDialog: {
|
||||||
visible: false,
|
visible: false,
|
||||||
@@ -426,6 +305,12 @@ export default {
|
|||||||
this.initialize();
|
this.initialize();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
apiQueryInvoicePool(params) {
|
||||||
|
return api.queryInvoicePool(params);
|
||||||
|
},
|
||||||
|
handleInvoicePicked(rows) {
|
||||||
|
this.confirmInvoice(rows[0]);
|
||||||
|
},
|
||||||
hasPermission(code) {
|
hasPermission(code) {
|
||||||
return this.permission?.[code] !== false;
|
return this.permission?.[code] !== false;
|
||||||
},
|
},
|
||||||
@@ -483,7 +368,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
validateInvoice(rule, value, callback) {
|
validateInvoice(rule, value, callback) {
|
||||||
if (!value || !this.form.kingdeeInvoicePoolId) {
|
if (!value) {
|
||||||
callback(new Error('请选择金蝶票据池发票'));
|
callback(new Error('请选择金蝶票据池发票'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -509,99 +394,17 @@ export default {
|
|||||||
}
|
}
|
||||||
callback();
|
callback();
|
||||||
},
|
},
|
||||||
async openInvoiceDialog() {
|
openInvoiceDialog() {
|
||||||
this.invoiceDialog.visible = true;
|
this.invoiceDialog.visible = true;
|
||||||
this.invoiceDialog.keyword = this.form.invoiceNo || '';
|
|
||||||
await this.loadInvoicePool();
|
|
||||||
},
|
|
||||||
async loadInvoicePool() {
|
|
||||||
this.invoiceDialog.loading = true;
|
|
||||||
this.invoiceDialog.page.current = 1;
|
|
||||||
this.invoiceDialog.current = null;
|
|
||||||
try {
|
|
||||||
if (!this.recordId) {
|
|
||||||
if (!this.invoicePoolMockLoaded) await this.loadInvoicePoolMockRows();
|
|
||||||
this.invoiceDialog.sourceRows = this.filterInvoicePoolMockRows(
|
|
||||||
this.invoiceDialog.keyword
|
|
||||||
);
|
|
||||||
this.refreshInvoicePoolPage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = this.unwrapData(await api.getInvoicePool(this.invoiceDialog.keyword));
|
|
||||||
this.invoiceDialog.sourceRows = Array.isArray(data) ? data : data?.records || [];
|
|
||||||
this.refreshInvoicePoolPage();
|
|
||||||
} finally {
|
|
||||||
this.invoiceDialog.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async loadInvoicePoolMockRows() {
|
|
||||||
const pageSize = 500;
|
|
||||||
const firstData = this.unwrapData(await getCustomerList(1, pageSize, {}));
|
|
||||||
const firstRows = Array.isArray(firstData) ? firstData : firstData?.records || [];
|
|
||||||
const total = Number(firstData?.total || firstRows.length);
|
|
||||||
const pageCount = Math.ceil(total / pageSize);
|
|
||||||
const remainingPages = await Promise.all(
|
|
||||||
Array.from({ length: Math.max(pageCount - 1, 0) }, (_, index) =>
|
|
||||||
getCustomerList(index + 2, pageSize, {})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const customers = [
|
|
||||||
...firstRows,
|
|
||||||
...remainingPages.flatMap(response => {
|
|
||||||
const data = this.unwrapData(response);
|
|
||||||
return Array.isArray(data) ? data : data?.records || [];
|
|
||||||
}),
|
|
||||||
].filter(
|
|
||||||
(customer, index, rows) =>
|
|
||||||
rows.findIndex(item => String(item.id) === String(customer.id)) === index
|
|
||||||
);
|
|
||||||
const userInfo = this.$store.getters.userInfo || {};
|
|
||||||
const counterpartyName = userInfo.deptName || userInfo.dept_name || '本企业';
|
|
||||||
this.invoicePoolMockRows = createInvoicePoolMockRows(
|
|
||||||
customers,
|
|
||||||
counterpartyName,
|
|
||||||
userInfo.email || ''
|
|
||||||
);
|
|
||||||
this.invoicePoolMockLoaded = true;
|
|
||||||
},
|
|
||||||
refreshInvoicePoolPage() {
|
|
||||||
const page = this.invoiceDialog.page;
|
|
||||||
const total = this.invoiceDialog.sourceRows.length;
|
|
||||||
const maxPage = Math.max(Math.ceil(total / page.size), 1);
|
|
||||||
if (page.current > maxPage) page.current = maxPage;
|
|
||||||
const start = (page.current - 1) * page.size;
|
|
||||||
page.total = total;
|
|
||||||
this.invoiceDialog.current = null;
|
|
||||||
this.invoiceDialog.rows = this.invoiceDialog.sourceRows.slice(start, start + page.size);
|
|
||||||
},
|
|
||||||
handleInvoicePoolSizeChange() {
|
|
||||||
this.invoiceDialog.page.current = 1;
|
|
||||||
this.refreshInvoicePoolPage();
|
|
||||||
},
|
|
||||||
filterInvoicePoolMockRows(keyword) {
|
|
||||||
const normalizedKeyword = String(keyword || '')
|
|
||||||
.trim()
|
|
||||||
.toLowerCase();
|
|
||||||
if (!normalizedKeyword) return this.invoicePoolMockRows.map(item => ({ ...item }));
|
|
||||||
return this.invoicePoolMockRows
|
|
||||||
.filter(item =>
|
|
||||||
[item.invoiceNo, item.issuerName, item.receiverName].some(value =>
|
|
||||||
String(value || '')
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(normalizedKeyword)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.map(item => ({ ...item }));
|
|
||||||
},
|
},
|
||||||
confirmInvoice(row, options = {}) {
|
confirmInvoice(row, options = {}) {
|
||||||
const selected = row?.id ? row : this.invoiceDialog.current;
|
const selected = row;
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
this.$message.warning('请选择一张金蝶票据池发票');
|
this.$message.warning('请选择一张金蝶票据池发票');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const preserveUserInput = options.preserveUserInput === true;
|
const preserveUserInput = options.preserveUserInput === true;
|
||||||
Object.assign(this.form, {
|
Object.assign(this.form, {
|
||||||
kingdeeInvoicePoolId: selected.id,
|
|
||||||
invoiceNo: selected.invoiceNo,
|
invoiceNo: selected.invoiceNo,
|
||||||
invoiceDate: selected.invoiceDate,
|
invoiceDate: selected.invoiceDate,
|
||||||
invoiceType: selected.invoiceType,
|
invoiceType: selected.invoiceType,
|
||||||
@@ -613,8 +416,6 @@ export default {
|
|||||||
bankName: selected.bankName,
|
bankName: selected.bankName,
|
||||||
bankAccount: selected.bankAccount,
|
bankAccount: selected.bankAccount,
|
||||||
issuingBank: selected.issuingBank,
|
issuingBank: selected.issuingBank,
|
||||||
kingdeeBillNo: selected.kingdeeBillNo || '',
|
|
||||||
kingdeeStatus: selected.kingdeeStatus || 'unsynced',
|
|
||||||
phone: preserveUserInput ? this.form.phone : selected.phone || '',
|
phone: preserveUserInput ? this.form.phone : selected.phone || '',
|
||||||
customerEmails: preserveUserInput
|
customerEmails: preserveUserInput
|
||||||
? this.form.customerEmails
|
? this.form.customerEmails
|
||||||
@@ -725,7 +526,6 @@ export default {
|
|||||||
payload() {
|
payload() {
|
||||||
return {
|
return {
|
||||||
id: this.form.id,
|
id: this.form.id,
|
||||||
kingdeeInvoicePoolId: this.form.kingdeeInvoicePoolId,
|
|
||||||
invoiceNo: this.form.invoiceNo,
|
invoiceNo: this.form.invoiceNo,
|
||||||
invoiceDate: this.form.invoiceDate,
|
invoiceDate: this.form.invoiceDate,
|
||||||
invoiceType: this.form.invoiceType,
|
invoiceType: this.form.invoiceType,
|
||||||
@@ -737,8 +537,6 @@ export default {
|
|||||||
bankName: this.form.bankName,
|
bankName: this.form.bankName,
|
||||||
bankAccount: this.form.bankAccount,
|
bankAccount: this.form.bankAccount,
|
||||||
issuingBank: this.form.issuingBank,
|
issuingBank: this.form.issuingBank,
|
||||||
kingdeeBillNo: this.form.kingdeeBillNo,
|
|
||||||
kingdeeStatus: this.form.kingdeeStatus,
|
|
||||||
phone: this.form.phone,
|
phone: this.form.phone,
|
||||||
customerEmails: this.form.customerEmails.join(';'),
|
customerEmails: this.form.customerEmails.join(';'),
|
||||||
departmentEmails: this.form.departmentEmails.join(';'),
|
departmentEmails: this.form.departmentEmails.join(';'),
|
||||||
@@ -773,6 +571,7 @@ export default {
|
|||||||
bizId: this.form.id,
|
bizId: this.form.id,
|
||||||
subjectName: this.form.invoiceNo || '',
|
subjectName: this.form.invoiceNo || '',
|
||||||
approvalStatus: previousApprovalStatus,
|
approvalStatus: previousApprovalStatus,
|
||||||
|
mkEnabled: await loadMkEnabled().catch(() => false),
|
||||||
});
|
});
|
||||||
this.$message.success('提交成功');
|
this.$message.success('提交成功');
|
||||||
this.goBack();
|
this.goBack();
|
||||||
@@ -782,13 +581,12 @@ export default {
|
|||||||
this.$message.warning('请先选择金蝶票据池发票');
|
this.$message.warning('请先选择金蝶票据池发票');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = this.recordId
|
const data = this.unwrapData(
|
||||||
? this.unwrapData(await api.getInvoicePool(this.form.invoiceNo))
|
await api.queryInvoicePool({ current: 1, size: 1, invoiceNo: this.form.invoiceNo })
|
||||||
: this.filterInvoicePoolMockRows(this.form.invoiceNo);
|
);
|
||||||
const rows = Array.isArray(data) ? data : data?.records || [];
|
const invoice = (data.records || []).find(item => item.invoiceNo === this.form.invoiceNo);
|
||||||
const invoice = rows.find(item => item.invoiceNo === this.form.invoiceNo);
|
|
||||||
if (!invoice) {
|
if (!invoice) {
|
||||||
this.$message.warning('金蝶票据池中未查询到该发票');
|
this.$message.warning('金蝶票池未查询到该发票');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.confirmInvoice(invoice, { preserveUserInput: true })) return;
|
if (!this.confirmInvoice(invoice, { preserveUserInput: true })) return;
|
||||||
@@ -851,11 +649,6 @@ export default {
|
|||||||
.receipt-form-page__dialog-search .el-input {
|
.receipt-form-page__dialog-search .el-input {
|
||||||
width: 360px;
|
width: 360px;
|
||||||
}
|
}
|
||||||
.receipt-form-page__dialog-pagination {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
.receipt-form-page :deep(.el-table) {
|
.receipt-form-page :deep(.el-table) {
|
||||||
--el-table-border-color: #eff1f7;
|
--el-table-border-color: #eff1f7;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,10 +40,10 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-if="searchExpanded" label="金蝶单据状态">
|
<el-form-item v-if="searchExpanded" label="金蝶状态">
|
||||||
<el-select v-model="query.kingdeeStatus" clearable placeholder="请选择">
|
<el-select v-model="query.kingdeePushStatus" clearable placeholder="请选择">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in kingdeeStatusOptions"
|
v-for="item in kingdeePushStatusOptions"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:label="item.label"
|
:label="item.label"
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
@@ -54,11 +54,13 @@
|
|||||||
<div class="receipt-page__search-actions">
|
<div class="receipt-page__search-actions">
|
||||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
<el-button @click="resetSearch">重置</el-button>
|
<el-button @click="resetSearch">重置</el-button>
|
||||||
<el-tooltip :content="searchExpanded ? '折叠' : '展开'" placement="top">
|
<el-button
|
||||||
<el-button text @click="searchExpanded = !searchExpanded">
|
v-if="showSearchToggle"
|
||||||
<el-icon><component :is="searchExpanded ? ArrowUp : ArrowDown" /></el-icon>
|
text
|
||||||
</el-button>
|
:icon="searchExpanded ? ArrowUp : ArrowDown"
|
||||||
</el-tooltip>
|
@click="searchExpanded = !searchExpanded"
|
||||||
|
>{{ searchExpanded ? '收起' : '展开' }}</el-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
</section>
|
</section>
|
||||||
@@ -70,7 +72,8 @@
|
|||||||
v-if="hasPermission('invoice_receipt_sync')"
|
v-if="hasPermission('invoice_receipt_sync')"
|
||||||
type="primary"
|
type="primary"
|
||||||
plain
|
plain
|
||||||
@click="handleBatchSync"
|
:loading="pushLoading"
|
||||||
|
@click="handlePushKingdee"
|
||||||
>批量同步</el-button
|
>批量同步</el-button
|
||||||
>
|
>
|
||||||
<el-button v-if="hasPermission('invoice_receipt_add')" type="primary" @click="openCreate"
|
<el-button v-if="hasPermission('invoice_receipt_add')" type="primary" @click="openCreate"
|
||||||
@@ -227,7 +230,7 @@ const emptyQuery = () => ({
|
|||||||
deptName: '',
|
deptName: '',
|
||||||
deptPath: [],
|
deptPath: [],
|
||||||
approvalStatus: '',
|
approvalStatus: '',
|
||||||
kingdeeStatus: '',
|
kingdeePushStatus: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -240,15 +243,19 @@ export default {
|
|||||||
ArrowUp,
|
ArrowUp,
|
||||||
query: emptyQuery(),
|
query: emptyQuery(),
|
||||||
searchExpanded: false,
|
searchExpanded: false,
|
||||||
|
// 与 avue-crud 一致:搜索项数量超过 searchIndex(默认一行 4 项)时才显示“展开/收起”
|
||||||
|
searchIndex: 4,
|
||||||
|
searchItemCount: 6,
|
||||||
searchVisible: true,
|
searchVisible: true,
|
||||||
tableSize: 'default',
|
tableSize: 'default',
|
||||||
approvalStatusOptions: api.approvalStatusOptions,
|
approvalStatusOptions: api.approvalStatusOptions,
|
||||||
kingdeeStatusOptions: api.kingdeeStatusOptions,
|
kingdeePushStatusOptions: api.kingdeePushStatusOptions,
|
||||||
columns: invoiceReceiptTableColumns,
|
columns: invoiceReceiptTableColumns,
|
||||||
visibleColumnKeys: invoiceReceiptTableColumns.map(column => column.prop),
|
visibleColumnKeys: invoiceReceiptTableColumns.map(column => column.prop),
|
||||||
rows: [],
|
rows: [],
|
||||||
selection: [],
|
selection: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
|
pushLoading: false,
|
||||||
page: { current: 1, size: 10, total: 0 },
|
page: { current: 1, size: 10, total: 0 },
|
||||||
mkEnabled: false,
|
mkEnabled: false,
|
||||||
};
|
};
|
||||||
@@ -258,6 +265,9 @@ export default {
|
|||||||
visibleColumns() {
|
visibleColumns() {
|
||||||
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
||||||
},
|
},
|
||||||
|
showSearchToggle() {
|
||||||
|
return this.searchItemCount > this.searchIndex;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.loadOrganizationOptions();
|
this.loadOrganizationOptions();
|
||||||
@@ -331,6 +341,7 @@ export default {
|
|||||||
bizId: row.id,
|
bizId: row.id,
|
||||||
subjectName: row.invoiceNo || '',
|
subjectName: row.invoiceNo || '',
|
||||||
approvalStatus: previousApprovalStatus,
|
approvalStatus: previousApprovalStatus,
|
||||||
|
mkEnabled: this.mkEnabled,
|
||||||
});
|
});
|
||||||
this.$message.success('提交成功');
|
this.$message.success('提交成功');
|
||||||
this.loadTable();
|
this.loadTable();
|
||||||
@@ -355,8 +366,13 @@ export default {
|
|||||||
},
|
},
|
||||||
async handleApprove(row) {
|
async handleApprove(row) {
|
||||||
await this.$confirm('确认审批通过该收票登记?', '提示', { type: 'warning' });
|
await this.$confirm('确认审批通过该收票登记?', '提示', { type: 'warning' });
|
||||||
await api.approve({ id: row.id });
|
const res = await api.approve({ id: row.id });
|
||||||
this.$message.success('审批通过');
|
// data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
|
||||||
|
if (res.data.data === false) {
|
||||||
|
this.$message.warning(res.data.msg);
|
||||||
|
} else {
|
||||||
|
this.$message.success(res.data.msg || '审批通过');
|
||||||
|
}
|
||||||
this.loadTable();
|
this.loadTable();
|
||||||
},
|
},
|
||||||
async handleReturn(row) {
|
async handleReturn(row) {
|
||||||
@@ -371,15 +387,29 @@ export default {
|
|||||||
this.$message.success('作废成功');
|
this.$message.success('作废成功');
|
||||||
this.loadTable();
|
this.loadTable();
|
||||||
},
|
},
|
||||||
async handleBatchSync() {
|
async handlePushKingdee() {
|
||||||
|
if (this.pushLoading) return;
|
||||||
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
|
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
this.$message.warning('请选择至少一条审批通过的收票登记');
|
this.$message.warning('请选择至少一条审批通过的收票登记');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await Promise.all(rows.map(row => api.syncKingdee(row.id)));
|
await this.$confirm(`确认将选中的 ${rows.length} 条收票登记同步至金蝶?`, '提示', {
|
||||||
this.$message.success(`已同步${rows.length}条收票登记`);
|
type: 'warning',
|
||||||
this.loadTable();
|
});
|
||||||
|
this.pushLoading = true;
|
||||||
|
try {
|
||||||
|
const summary = this.unwrapData(await api.pushKingdee(rows.map(row => row.id)));
|
||||||
|
await this.loadTable();
|
||||||
|
// 附件联带同步提示:推送成功但附件未全部上传时在结果弹窗追加说明,重推即自动补传
|
||||||
|
const idSet = new Set(rows.map(row => String(row.id)));
|
||||||
|
const pendingCount = (this.rows || []).filter(row => idSet.has(String(row.id))
|
||||||
|
&& ['pending', 'partial'].includes(row.attachmentSyncStatus)).length;
|
||||||
|
const attachmentTip = pendingCount ? `\n其中${pendingCount}单附件未全部上传,可重推补传` : '';
|
||||||
|
this.$alert((summary || '推送完成') + attachmentTip, '推送结果', { confirmButtonText: '知道了' });
|
||||||
|
} finally {
|
||||||
|
this.pushLoading = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async handleExport() {
|
async handleExport() {
|
||||||
const data = this.unwrapData(await api.getList(1, 100000, this.query));
|
const data = this.unwrapData(await api.getList(1, 100000, this.query));
|
||||||
@@ -396,8 +426,9 @@ export default {
|
|||||||
当前处理人: item.currentProcessor,
|
当前处理人: item.currentProcessor,
|
||||||
创建人: item.createUserName,
|
创建人: item.createUserName,
|
||||||
创建时间: item.createTime,
|
创建时间: item.createTime,
|
||||||
金蝶单据号: item.kingdeeBillNo,
|
金蝶单据号: item.kingdeeReceivedInvoiceNo,
|
||||||
金蝶单据状态: item.kingdeeStatusName,
|
金蝶状态: item.kingdeePushStatusName,
|
||||||
|
失败原因: item.kingdeePushMsg,
|
||||||
}));
|
}));
|
||||||
const workbook = XLSX.utils.book_new();
|
const workbook = XLSX.utils.book_new();
|
||||||
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(exportRows), '收票登记');
|
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(exportRows), '收票登记');
|
||||||
|
|||||||
@@ -36,23 +36,20 @@
|
|||||||
<el-col v-if="isSettlementRelatedType" :span="6"
|
<el-col v-if="isSettlementRelatedType" :span="6"
|
||||||
><el-form-item label="结算单号" prop="referenceId">
|
><el-form-item label="结算单号" prop="referenceId">
|
||||||
<div class="payment-form-page__settlement-nos">
|
<div class="payment-form-page__settlement-nos">
|
||||||
<template v-if="referenceSettlementLinks.length">
|
<el-tooltip
|
||||||
<el-link
|
:content="referenceLabel"
|
||||||
v-for="item in referenceSettlementLinks"
|
placement="top"
|
||||||
:key="`${item.id || item.settlementNo}`"
|
:disabled="!referenceLabel || referenceSettlementCount < 2"
|
||||||
type="primary"
|
>
|
||||||
@click="openSettlementDetail(item)"
|
<el-input
|
||||||
>
|
:model-value="referenceDisplayText"
|
||||||
{{ item.settlementNo }}
|
readonly
|
||||||
</el-link>
|
:disabled="readonly"
|
||||||
</template>
|
placeholder="请输入"
|
||||||
<el-input
|
class="payment-form-page__settlement-input"
|
||||||
v-else
|
@click="!readonly && openReference()"
|
||||||
v-model="referenceLabel"
|
/>
|
||||||
readonly
|
</el-tooltip>
|
||||||
:disabled="readonly"
|
|
||||||
placeholder="请选择预结算/正式结算单"
|
|
||||||
/>
|
|
||||||
<el-button
|
<el-button
|
||||||
v-if="!readonly"
|
v-if="!readonly"
|
||||||
class="payment-form-page__settlement-select-btn"
|
class="payment-form-page__settlement-select-btn"
|
||||||
@@ -251,7 +248,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</section-card>
|
</section-card>
|
||||||
<section-card v-if="isSettlementRelatedType" title="发票信息">
|
<section-card v-if="showInvoiceSection" title="发票信息">
|
||||||
<el-table :data="form.invoices" border>
|
<el-table :data="form.invoices" border>
|
||||||
<el-table-column type="index" label="序号" width="70" />
|
<el-table-column type="index" label="序号" width="70" />
|
||||||
<el-table-column prop="settlementNo" label="结算单号" min-width="120">
|
<el-table-column prop="settlementNo" label="结算单号" min-width="120">
|
||||||
@@ -281,8 +278,18 @@
|
|||||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||||
<template #default="{ row, $index }">
|
<template #default="{ row, $index }">
|
||||||
<div class="payment-form-page__invoice-actions">
|
<div class="payment-form-page__invoice-actions">
|
||||||
<el-link type="primary" @click="viewInvoiceAttachment(row)">查看</el-link>
|
<el-link
|
||||||
<el-link type="primary" @click="downloadInvoiceAttachment(row)">下载</el-link>
|
type="primary"
|
||||||
|
:disabled="kingdeeAttachmentBusy"
|
||||||
|
@click="viewInvoiceAttachment(row)"
|
||||||
|
>查看</el-link
|
||||||
|
>
|
||||||
|
<el-link
|
||||||
|
type="primary"
|
||||||
|
:disabled="kingdeeAttachmentBusy"
|
||||||
|
@click="downloadInvoiceAttachment(row)"
|
||||||
|
>下载</el-link
|
||||||
|
>
|
||||||
<el-link
|
<el-link
|
||||||
v-if="!readonly"
|
v-if="!readonly"
|
||||||
type="danger"
|
type="danger"
|
||||||
@@ -436,7 +443,7 @@
|
|||||||
></el-table
|
></el-table
|
||||||
>
|
>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="payment-form-page__reference-search">
|
<el-card class="payment-form-page__reference-search">
|
||||||
<el-form
|
<el-form
|
||||||
:model="referenceQuery"
|
:model="referenceQuery"
|
||||||
label-position="right"
|
label-position="right"
|
||||||
@@ -474,7 +481,7 @@
|
|||||||
<el-button @click="resetReferenceSearch">清空</el-button>
|
<el-button @click="resetReferenceSearch">清空</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</el-card>
|
||||||
<el-tabs v-model="referenceTab" @tab-change="handleReferenceTabChange">
|
<el-tabs v-model="referenceTab" @tab-change="handleReferenceTabChange">
|
||||||
<el-tab-pane v-if="canSelectFormalSettlement" label="正式结算单" name="formal">
|
<el-tab-pane v-if="canSelectFormalSettlement" label="正式结算单" name="formal">
|
||||||
<el-table
|
<el-table
|
||||||
@@ -666,7 +673,12 @@ import {
|
|||||||
import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer';
|
import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer';
|
||||||
import { formatContractOptionLabel } from '@/utils/contract';
|
import { formatContractOptionLabel } from '@/utils/contract';
|
||||||
import { downloadFileByUrl } from '@/utils/util';
|
import { downloadFileByUrl } from '@/utils/util';
|
||||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||||
|
import {
|
||||||
|
downloadKingdeeInvoiceFile,
|
||||||
|
isKingdeePathAttachment,
|
||||||
|
viewKingdeeInvoiceFile,
|
||||||
|
} from '@/utils/kingdee-attachment';
|
||||||
import PdfPreview from '@/components/pdf-preview/main.vue';
|
import PdfPreview from '@/components/pdf-preview/main.vue';
|
||||||
|
|
||||||
const attachmentViewerPlugins = [
|
const attachmentViewerPlugins = [
|
||||||
@@ -676,6 +688,9 @@ const attachmentViewerPlugins = [
|
|||||||
fallbackPlugin(),
|
fallbackPlugin(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const paymentApplicationListRoute = '/payment/payment-application';
|
||||||
|
const paymentApplicationListRefreshKey = 'payment-application-list-refresh';
|
||||||
|
|
||||||
const ATTACHMENT_MATERIALS = {
|
const ATTACHMENT_MATERIALS = {
|
||||||
weighingSlip: { key: 'weighingSlip', label: '磅单', keywords: ['磅单'] },
|
weighingSlip: { key: 'weighingSlip', label: '磅单', keywords: ['磅单'] },
|
||||||
entrustOrder: { key: 'entrustOrder', label: '委托单', keywords: ['委托单'] },
|
entrustOrder: { key: 'entrustOrder', label: '委托单', keywords: ['委托单'] },
|
||||||
@@ -747,6 +762,8 @@ export default {
|
|||||||
form: emptyForm(),
|
form: emptyForm(),
|
||||||
amountSyncing: false,
|
amountSyncing: false,
|
||||||
transferPayload: null,
|
transferPayload: null,
|
||||||
|
// 金蝶侧发票附件查看/下载进行中(行组级 loading 守卫)
|
||||||
|
kingdeeAttachmentBusy: false,
|
||||||
referenceRows: [],
|
referenceRows: [],
|
||||||
paymentRecords: [],
|
paymentRecords: [],
|
||||||
referenceVisible: false,
|
referenceVisible: false,
|
||||||
@@ -874,6 +891,10 @@ export default {
|
|||||||
isSettlementRelatedType() {
|
isSettlementRelatedType() {
|
||||||
return ['progress_advance', 'settlement_payment'].includes(this.form.paymentType);
|
return ['progress_advance', 'settlement_payment'].includes(this.form.paymentType);
|
||||||
},
|
},
|
||||||
|
// 项目预付、进度预付不展示发票信息,仅尾款付款展示
|
||||||
|
showInvoiceSection() {
|
||||||
|
return this.form.paymentType === 'settlement_payment';
|
||||||
|
},
|
||||||
canSelectPreSettlement() {
|
canSelectPreSettlement() {
|
||||||
// 进度预付仅预结算;尾款付款可正式/预结算
|
// 进度预付仅预结算;尾款付款可正式/预结算
|
||||||
return ['progress_advance', 'settlement_payment'].includes(this.form.paymentType);
|
return ['progress_advance', 'settlement_payment'].includes(this.form.paymentType);
|
||||||
@@ -926,6 +947,17 @@ export default {
|
|||||||
referenceType: this.form.preSettlementId ? 'pre' : 'formal',
|
referenceType: this.form.preSettlementId ? 'pre' : 'formal',
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
referenceSettlementNos() {
|
||||||
|
return this.referenceSettlementLinks.map(item => item.settlementNo).filter(Boolean);
|
||||||
|
},
|
||||||
|
referenceSettlementCount() {
|
||||||
|
return this.referenceSettlementNos.length;
|
||||||
|
},
|
||||||
|
// 结算单号展示:单张显示单号;多张仅提示数量,明细在下方「结算信息」表,悬停可看全部单号
|
||||||
|
referenceDisplayText() {
|
||||||
|
if (this.referenceSettlementCount > 1) return `已选 ${this.referenceSettlementCount} 张结算单`;
|
||||||
|
return this.referenceLabel;
|
||||||
|
},
|
||||||
settlementRows() {
|
settlementRows() {
|
||||||
if (this.referenceRows.length) {
|
if (this.referenceRows.length) {
|
||||||
return this.referenceRows.map(item => ({
|
return this.referenceRows.map(item => ({
|
||||||
@@ -1049,7 +1081,7 @@ export default {
|
|||||||
!this.form.preSettlementId &&
|
!this.form.preSettlementId &&
|
||||||
!this.form.preSettlementIds?.length
|
!this.form.preSettlementIds?.length
|
||||||
) {
|
) {
|
||||||
callback(new Error('请选择结算单'));
|
callback(new Error('请输入'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
callback();
|
callback();
|
||||||
@@ -1110,7 +1142,7 @@ export default {
|
|||||||
callback();
|
callback();
|
||||||
},
|
},
|
||||||
validateInvoices() {
|
validateInvoices() {
|
||||||
if (this.form.paymentType === 'project_advance') return;
|
if (!this.showInvoiceSection) return;
|
||||||
for (const invoice of this.form.invoices || []) {
|
for (const invoice of this.form.invoices || []) {
|
||||||
if (String(invoice.settlementNo || '').length > 100) {
|
if (String(invoice.settlementNo || '').length > 100) {
|
||||||
throw new Error('结算单号不能超过100个字符');
|
throw new Error('结算单号不能超过100个字符');
|
||||||
@@ -1201,7 +1233,7 @@ export default {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
const referenceRow = this.createReferenceRow(data);
|
const referenceRow = this.createReferenceRow(data);
|
||||||
this.referenceRows = referenceRow.settlementNo ? [referenceRow] : [];
|
this.referenceRows = this.restoreReferenceRows(data, referenceRow);
|
||||||
this.paymentRecords = data.paymentRecords || [];
|
this.paymentRecords = data.paymentRecords || [];
|
||||||
},
|
},
|
||||||
async initialize() {
|
async initialize() {
|
||||||
@@ -1224,7 +1256,7 @@ export default {
|
|||||||
invoices: data.invoices || [],
|
invoices: data.invoices || [],
|
||||||
};
|
};
|
||||||
const referenceRow = this.createReferenceRow(data);
|
const referenceRow = this.createReferenceRow(data);
|
||||||
this.referenceRows = referenceRow.settlementNo ? [referenceRow] : [];
|
this.referenceRows = this.restoreReferenceRows(data, referenceRow);
|
||||||
this.paymentRecords = data.paymentRecords || [];
|
this.paymentRecords = data.paymentRecords || [];
|
||||||
if (this.form.paymentType === 'project_advance') {
|
if (this.form.paymentType === 'project_advance') {
|
||||||
await this.loadProjectOptions();
|
await this.loadProjectOptions();
|
||||||
@@ -1442,6 +1474,33 @@ export default {
|
|||||||
this.$refs.formRef?.clearValidate();
|
this.$refs.formRef?.clearValidate();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
restoreReferenceRows(data, fallbackRow) {
|
||||||
|
const relations = Array.isArray(data?.settlements) ? data.settlements : [];
|
||||||
|
const rows = relations
|
||||||
|
.map(item => {
|
||||||
|
const isPre = item.preSettlementId !== undefined && item.preSettlementId !== null && item.preSettlementId !== '';
|
||||||
|
const settlementNo = item.preSettlementNo || item.formalSettlementNo || '';
|
||||||
|
if (!settlementNo) return null;
|
||||||
|
return {
|
||||||
|
id: isPre ? item.preSettlementId : item.formalSettlementId,
|
||||||
|
settlementNo,
|
||||||
|
referenceType: isPre ? 'pre' : 'formal',
|
||||||
|
settlementAmount: Number(Number(item.settlementAmount || 0).toFixed(2)),
|
||||||
|
payableAmount: Number(Number(item.payableAmount ?? item.settlementAmount ?? 0).toFixed(2)),
|
||||||
|
appliedAmount: Number(Number(item.appliedAmount || 0).toFixed(2)),
|
||||||
|
fixedAppliedAmount: this.readonly,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
if (rows.length > 1) {
|
||||||
|
this.form.preSettlementIds = rows.filter(item => item.referenceType === 'pre').map(item => item.id);
|
||||||
|
this.form.settlementIds = rows.filter(item => item.referenceType === 'formal').map(item => item.id);
|
||||||
|
if (this.form.preSettlementIds.length) this.form.preSettlementId = this.form.preSettlementIds[0];
|
||||||
|
if (this.form.settlementIds.length) this.form.settlementId = this.form.settlementIds[0];
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
return fallbackRow?.settlementNo ? [fallbackRow] : [];
|
||||||
|
},
|
||||||
createReferenceRow(row) {
|
createReferenceRow(row) {
|
||||||
const settlementAmount = Number(row.settlementAmount || 0);
|
const settlementAmount = Number(row.settlementAmount || 0);
|
||||||
const appliedAmount = Number(row.advanceAppliedAmount || row.appliedPaymentAmount || 0);
|
const appliedAmount = Number(row.advanceAppliedAmount || row.appliedPaymentAmount || 0);
|
||||||
@@ -1450,11 +1509,13 @@ export default {
|
|||||||
id: row.preSettlementId || row.settlementId || row.id,
|
id: row.preSettlementId || row.settlementId || row.id,
|
||||||
settlementNo: row.preSettlementNo || row.formalSettlementNo || row.settlementNo || '',
|
settlementNo: row.preSettlementNo || row.formalSettlementNo || row.settlementNo || '',
|
||||||
referenceType: isPre ? 'pre' : 'formal',
|
referenceType: isPre ? 'pre' : 'formal',
|
||||||
settlementAmount,
|
settlementAmount: Number(Number(settlementAmount || 0).toFixed(2)),
|
||||||
payableAmount:
|
payableAmount: Number(
|
||||||
row.payableAmount === undefined || row.payableAmount === null
|
(row.payableAmount === undefined || row.payableAmount === null
|
||||||
? Math.max(0, settlementAmount - appliedAmount)
|
? Math.max(0, settlementAmount - appliedAmount)
|
||||||
: Number(row.payableAmount || 0),
|
: Number(row.payableAmount || 0)
|
||||||
|
).toFixed(2)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
createInvoiceRows(invoices = [], settlementNo = '') {
|
createInvoiceRows(invoices = [], settlementNo = '') {
|
||||||
@@ -1512,9 +1573,24 @@ export default {
|
|||||||
}
|
}
|
||||||
const first = rows[0];
|
const first = rows[0];
|
||||||
this.form.invoices = [];
|
this.form.invoices = [];
|
||||||
this.referenceRows = rows.map(item =>
|
const paymentType =
|
||||||
this.createReferenceRow({ ...item, referenceType: 'pre' })
|
this.form.paymentType === 'settlement_payment' ? 'settlement_payment' : 'progress_advance';
|
||||||
|
const amountResponses = await Promise.all(
|
||||||
|
rows.map(item => {
|
||||||
|
const referenceId = item.preSettlementId || item.id;
|
||||||
|
if (!this.isValidReferenceId(referenceId)) return Promise.resolve(null);
|
||||||
|
return api.getReferenceAmount(paymentType, referenceId, this.form.id).catch(() => null);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
this.referenceRows = rows.map((item, index) => {
|
||||||
|
const amount = this.unwrapData(amountResponses[index]) || {};
|
||||||
|
return this.createReferenceRow({
|
||||||
|
...item,
|
||||||
|
referenceType: 'pre',
|
||||||
|
settlementAmount: amount.settlementAmount ?? item.settlementAmount,
|
||||||
|
payableAmount: amount.payableAmount,
|
||||||
|
});
|
||||||
|
});
|
||||||
const settlementAmount = this.referenceRows.reduce(
|
const settlementAmount = this.referenceRows.reduce(
|
||||||
(total, item) => total + Number(item.settlementAmount || 0),
|
(total, item) => total + Number(item.settlementAmount || 0),
|
||||||
0
|
0
|
||||||
@@ -1525,8 +1601,6 @@ export default {
|
|||||||
);
|
);
|
||||||
const paymentRatio = Number(this.form.paymentRatio || 0);
|
const paymentRatio = Number(this.form.paymentRatio || 0);
|
||||||
// 尾款付款场景下选择预结算单时保留尾款付款类型,不强制改为进度预付
|
// 尾款付款场景下选择预结算单时保留尾款付款类型,不强制改为进度预付
|
||||||
const paymentType =
|
|
||||||
this.form.paymentType === 'settlement_payment' ? 'settlement_payment' : 'progress_advance';
|
|
||||||
this.amountSyncing = true;
|
this.amountSyncing = true;
|
||||||
Object.assign(this.form, {
|
Object.assign(this.form, {
|
||||||
paymentType,
|
paymentType,
|
||||||
@@ -1589,8 +1663,19 @@ export default {
|
|||||||
this.attachmentFileName(this.invoiceAttachment(row)) || `${row.invoiceNo || '发票'}.pdf`
|
this.attachmentFileName(this.invoiceAttachment(row)) || `${row.invoiceNo || '发票'}.pdf`
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
viewInvoiceAttachment(row) {
|
async viewInvoiceAttachment(row) {
|
||||||
const attachment = this.invoiceAttachment(row);
|
const attachment = this.invoiceAttachment(row);
|
||||||
|
// 金蝶侧附件(path 型)经后端代理 A801-2 预览,OSS 直链维持本地预览组件
|
||||||
|
if (isKingdeePathAttachment(attachment)) {
|
||||||
|
if (this.kingdeeAttachmentBusy) return;
|
||||||
|
this.kingdeeAttachmentBusy = true;
|
||||||
|
try {
|
||||||
|
await viewKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row));
|
||||||
|
} finally {
|
||||||
|
this.kingdeeAttachmentBusy = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.attachmentFileUrl(attachment)) {
|
if (!this.attachmentFileUrl(attachment)) {
|
||||||
this.$message.warning('当前发票暂无可查看附件');
|
this.$message.warning('当前发票暂无可查看附件');
|
||||||
return;
|
return;
|
||||||
@@ -1600,8 +1685,20 @@ export default {
|
|||||||
[attachment]
|
[attachment]
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
downloadInvoiceAttachment(row) {
|
async downloadInvoiceAttachment(row) {
|
||||||
const url = this.invoiceAttachmentUrl(row);
|
const attachment = this.invoiceAttachment(row);
|
||||||
|
// 金蝶侧附件(path 型)经后端代理 A801-2 下载,OSS 直链维持前端直连下载
|
||||||
|
if (isKingdeePathAttachment(attachment)) {
|
||||||
|
if (this.kingdeeAttachmentBusy) return;
|
||||||
|
this.kingdeeAttachmentBusy = true;
|
||||||
|
try {
|
||||||
|
await downloadKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row));
|
||||||
|
} finally {
|
||||||
|
this.kingdeeAttachmentBusy = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = this.attachmentFileUrl(attachment);
|
||||||
if (!url) {
|
if (!url) {
|
||||||
this.$message.warning('当前发票暂无可下载附件');
|
this.$message.warning('当前发票暂无可下载附件');
|
||||||
return;
|
return;
|
||||||
@@ -1869,6 +1966,7 @@ export default {
|
|||||||
this.contractPaymentRatioExceeded = exceeded;
|
this.contractPaymentRatioExceeded = exceeded;
|
||||||
},
|
},
|
||||||
handleTypeChange() {
|
handleTypeChange() {
|
||||||
|
// 切换类型时清空发票;预付类型本身也不再展示/提交发票
|
||||||
this.form.invoices = [];
|
this.form.invoices = [];
|
||||||
this.paymentRecords = [];
|
this.paymentRecords = [];
|
||||||
Object.assign(this.form, {
|
Object.assign(this.form, {
|
||||||
@@ -2056,8 +2154,20 @@ export default {
|
|||||||
const project = this.projectOptions.find(item => String(item.id) === String(projectId));
|
const project = this.projectOptions.find(item => String(item.id) === String(projectId));
|
||||||
this.form.projectName = project?.projectName || project?.projectShortName || '';
|
this.form.projectName = project?.projectName || project?.projectShortName || '';
|
||||||
Object.assign(this.form, {
|
Object.assign(this.form, {
|
||||||
deptId: project?.undertakeDeptId || project?.businessDeptId || null,
|
deptId:
|
||||||
deptName: project?.undertakeDeptName || project?.businessDeptName || '',
|
String(project?.undertakeDeptId || '')
|
||||||
|
.split(/[,,]/)
|
||||||
|
.map(item => item.trim())
|
||||||
|
.find(Boolean) ||
|
||||||
|
project?.businessDeptId ||
|
||||||
|
null,
|
||||||
|
deptName:
|
||||||
|
String(project?.undertakeDeptName || '')
|
||||||
|
.split(/[,,、]/)
|
||||||
|
.map(item => item.trim())
|
||||||
|
.find(Boolean) ||
|
||||||
|
project?.businessDeptName ||
|
||||||
|
'',
|
||||||
contractId: null,
|
contractId: null,
|
||||||
contractNo: '',
|
contractNo: '',
|
||||||
contractName: '',
|
contractName: '',
|
||||||
@@ -2588,7 +2698,7 @@ export default {
|
|||||||
attachmentsJson: JSON.stringify(attachments || []),
|
attachmentsJson: JSON.stringify(attachments || []),
|
||||||
remark,
|
remark,
|
||||||
invoices:
|
invoices:
|
||||||
paymentType === 'project_advance'
|
paymentType === 'project_advance' || paymentType === 'progress_advance'
|
||||||
? []
|
? []
|
||||||
: invoices.map(invoice => ({
|
: invoices.map(invoice => ({
|
||||||
settlementNo: invoice.settlementNo || '',
|
settlementNo: invoice.settlementNo || '',
|
||||||
@@ -2644,14 +2754,17 @@ export default {
|
|||||||
bizId: this.form.id,
|
bizId: this.form.id,
|
||||||
subjectName: this.form.paymentNo || '',
|
subjectName: this.form.paymentNo || '',
|
||||||
approvalStatus: previousApprovalStatus,
|
approvalStatus: previousApprovalStatus,
|
||||||
|
mkEnabled: await loadMkEnabled().catch(() => false),
|
||||||
});
|
});
|
||||||
this.$message.success('提交成功');
|
this.$message.success('提交成功');
|
||||||
|
// 通知列表页(keep-alive)重新拉取数据
|
||||||
|
sessionStorage.setItem(paymentApplicationListRefreshKey, '1');
|
||||||
this.goBack();
|
this.goBack();
|
||||||
},
|
},
|
||||||
goBack() {
|
goBack() {
|
||||||
removeSettlementTransfer(this.$route.query.transferToken);
|
removeSettlementTransfer(this.$route.query.transferToken);
|
||||||
this.$router.$avueRouter?.closeTag?.();
|
this.$router.$avueRouter?.closeTag?.();
|
||||||
this.$router.push('/payment/payment-application');
|
this.$router.push(paymentApplicationListRoute);
|
||||||
},
|
},
|
||||||
formatMoney(value) {
|
formatMoney(value) {
|
||||||
const amount = Number(value);
|
const amount = Number(value);
|
||||||
@@ -2669,13 +2782,18 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
}
|
}
|
||||||
.payment-form-page__settlement-nos :deep(.el-input) {
|
.payment-form-page__settlement-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 120px;
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.payment-form-page__settlement-input :deep(.el-input__inner) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.payment-form-page__settlement-input.is-disabled :deep(.el-input__inner) {
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
.payment-form-page__settlement-select-btn {
|
.payment-form-page__settlement-select-btn {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|||||||
@@ -61,6 +61,13 @@
|
|||||||
}}</el-tag>
|
}}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #kingdeeLockStatus="{ row }">
|
||||||
|
<el-tooltip v-if="lockTag(row).show" :content="lockTag(row).tip" placement="top">
|
||||||
|
<el-tag :type="lockTag(row).type" class="status-text">{{ lockTag(row).label }}</el-tag>
|
||||||
|
</el-tooltip>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #menu="{ row }">
|
<template #menu="{ row }">
|
||||||
<el-link
|
<el-link
|
||||||
v-if="hasPermission('payment_application_view')"
|
v-if="hasPermission('payment_application_view')"
|
||||||
@@ -117,7 +124,11 @@
|
|||||||
>驳回</el-link
|
>驳回</el-link
|
||||||
>
|
>
|
||||||
<el-link
|
<el-link
|
||||||
v-if="hasPermission('payment_application_void') && row.approvalStatus === 'approved'"
|
v-if="
|
||||||
|
hasPermission('payment_application_void') &&
|
||||||
|
row.approvalStatus === 'approved' &&
|
||||||
|
row.kingdeeLockStatus !== 'locked'
|
||||||
|
"
|
||||||
type="danger"
|
type="danger"
|
||||||
@click="handleVoid(row)"
|
@click="handleVoid(row)"
|
||||||
>作废</el-link
|
>作废</el-link
|
||||||
@@ -141,6 +152,9 @@ import organizationSearch from '@/mixins/organization-search';
|
|||||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||||
|
|
||||||
|
const paymentApplicationListRoute = '/payment/payment-application';
|
||||||
|
const paymentApplicationListRefreshKey = 'payment-application-list-refresh';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PaymentApplication',
|
name: 'PaymentApplication',
|
||||||
mixins: [organizationSearch],
|
mixins: [organizationSearch],
|
||||||
@@ -149,6 +163,8 @@ export default {
|
|||||||
rows: [],
|
rows: [],
|
||||||
selection: [],
|
selection: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
|
syncLoading: false,
|
||||||
|
resultSyncing: false,
|
||||||
query: {},
|
query: {},
|
||||||
option: {},
|
option: {},
|
||||||
page: {
|
page: {
|
||||||
@@ -180,7 +196,28 @@ export default {
|
|||||||
this.mkEnabled = false;
|
this.mkEnabled = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
activated() {
|
||||||
|
this.consumeListRefreshFlag();
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route(to, from) {
|
||||||
|
if (
|
||||||
|
to.path === paymentApplicationListRoute &&
|
||||||
|
from?.path?.includes('/payment-application/form')
|
||||||
|
) {
|
||||||
|
this.consumeListRefreshFlag();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** 表单提交成功后带标记返回时,重新拉取列表 */
|
||||||
|
consumeListRefreshFlag() {
|
||||||
|
if (!sessionStorage.getItem(paymentApplicationListRefreshKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sessionStorage.removeItem(paymentApplicationListRefreshKey);
|
||||||
|
this.onLoad(this.page);
|
||||||
|
},
|
||||||
hasPermission(code) {
|
hasPermission(code) {
|
||||||
return this.permission?.[code] !== false;
|
return this.permission?.[code] !== false;
|
||||||
},
|
},
|
||||||
@@ -283,13 +320,19 @@ export default {
|
|||||||
bizId: row.id,
|
bizId: row.id,
|
||||||
subjectName: row.paymentNo || '',
|
subjectName: row.paymentNo || '',
|
||||||
approvalStatus: previousApprovalStatus,
|
approvalStatus: previousApprovalStatus,
|
||||||
|
mkEnabled: this.mkEnabled,
|
||||||
});
|
});
|
||||||
this.$message.success('提交成功');
|
this.$message.success('提交成功');
|
||||||
this.refreshChange();
|
this.refreshChange();
|
||||||
},
|
},
|
||||||
async handleApprove(row) {
|
async handleApprove(row) {
|
||||||
await api.approve({ id: row.id });
|
const res = await api.approve({ id: row.id });
|
||||||
this.$message.success('审批通过');
|
// data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
|
||||||
|
if (res.data.data === false) {
|
||||||
|
this.$message.warning(res.data.msg);
|
||||||
|
} else {
|
||||||
|
this.$message.success(res.data.msg || '审批通过');
|
||||||
|
}
|
||||||
this.refreshChange();
|
this.refreshChange();
|
||||||
},
|
},
|
||||||
handleMkAudit(row) {
|
handleMkAudit(row) {
|
||||||
@@ -325,18 +368,60 @@ export default {
|
|||||||
async handleSyncRow(row) {
|
async handleSyncRow(row) {
|
||||||
const data = this.unwrapData(await api.syncKingdee(row.id));
|
const data = this.unwrapData(await api.syncKingdee(row.id));
|
||||||
this.$message.success(`已生成金蝶付款单:${data}`);
|
this.$message.success(`已生成金蝶付款单:${data}`);
|
||||||
this.refreshChange();
|
await this.refreshChange();
|
||||||
|
this.warnAttachmentNotUploaded([row.id]);
|
||||||
},
|
},
|
||||||
async handleSync() {
|
async handleSync() {
|
||||||
|
if (this.syncLoading) return;
|
||||||
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
|
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
this.$message.warning('请选择至少一条审批通过的付款申请');
|
this.$message.warning('请选择至少一条审批通过的付款申请');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await api.syncKingdeeBatch(rows.map(row => row.id));
|
this.syncLoading = true;
|
||||||
this.$message.success(`已同步${rows.length}条付款申请`);
|
try {
|
||||||
this.refreshChange();
|
const data = this.unwrapData(await api.syncKingdeeBatch(rows.map(row => row.id))) || [];
|
||||||
|
const failedList = data.filter(item => String(item).includes('同步失败'));
|
||||||
|
if (failedList.length) {
|
||||||
|
this.$message({
|
||||||
|
type: 'warning',
|
||||||
|
message: `同步完成:成功${data.length - failedList.length}条,失败${failedList.length}条。${failedList.join(';')}`,
|
||||||
|
duration: 8000,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$message.success(`已同步${data.length}条付款申请`);
|
||||||
|
}
|
||||||
|
await this.refreshChange();
|
||||||
|
this.warnAttachmentNotUploaded(rows.map(row => row.id));
|
||||||
|
} finally {
|
||||||
|
this.syncLoading = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
// 附件联带同步提示:同步成功但附件未全部上传时追加 warning,重推即自动补传
|
||||||
|
warnAttachmentNotUploaded(ids) {
|
||||||
|
const idSet = new Set(ids.map(String));
|
||||||
|
const pendingCount = (this.rows || []).filter(row => idSet.has(String(row.id))
|
||||||
|
&& ['pending', 'partial'].includes(row.attachmentSyncStatus)).length;
|
||||||
|
if (pendingCount) {
|
||||||
|
this.$message({
|
||||||
|
type: 'warning',
|
||||||
|
message: `其中${pendingCount}单附件未全部上传,可重推补传`,
|
||||||
|
duration: 8000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async handleSyncResult() {
|
||||||
|
if (this.resultSyncing) return;
|
||||||
|
this.resultSyncing = true;
|
||||||
|
try {
|
||||||
|
const data = this.unwrapData(await api.syncKingdeeResult());
|
||||||
|
this.$message.success(data || '金蝶付款结果回写完成');
|
||||||
|
this.loadTable();
|
||||||
|
} finally {
|
||||||
|
this.resultSyncing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
handleExport() {
|
handleExport() {
|
||||||
const columns = this.option.column.filter(column => !column.hide && column.prop);
|
const columns = this.option.column.filter(column => !column.hide && column.prop);
|
||||||
const headers = columns.map(item => item.label);
|
const headers = columns.map(item => item.label);
|
||||||
@@ -361,6 +446,44 @@ export default {
|
|||||||
''
|
''
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
lockTag(row) {
|
||||||
|
if (row.kingdeeLockStatus === 'locked') {
|
||||||
|
return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' };
|
||||||
|
}
|
||||||
|
if (row.kingdeeLockStatus === 'failed') {
|
||||||
|
if ((row.kingdeeTryCount || 0) >= 5) {
|
||||||
|
return {
|
||||||
|
show: true,
|
||||||
|
type: 'danger',
|
||||||
|
label: '锁定失败·已达上限',
|
||||||
|
tip: row.kingdeeLockFailReason || '自动补偿次数已用完,请重推「同步金蝶」立即补锁',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
show: true,
|
||||||
|
type: 'warning',
|
||||||
|
label: '锁定失败·补偿中',
|
||||||
|
tip: row.kingdeeLockFailReason || '系统将自动补偿重锁',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 金蝶解锁终态标识:后端按 unlocked 锁记录回填(付款申请不作废,仅此标识可区分金蝶删单与推送失败)
|
||||||
|
if (row.kingdeeUnlockTime) {
|
||||||
|
const isDelete = row.kingdeeUnlockOperationName === '金蝶删单';
|
||||||
|
return {
|
||||||
|
show: true,
|
||||||
|
type: isDelete ? 'danger' : 'warning',
|
||||||
|
label: row.kingdeeUnlockOperationName || '金蝶解锁',
|
||||||
|
tip: `${row.kingdeeUnlockOperator || '-'} 于 ${this.$dayjs(row.kingdeeUnlockTime).format('YYYY-MM-DD HH:mm:ss')} 解锁`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { show: false };
|
||||||
|
},
|
||||||
|
displayValue(value) {
|
||||||
|
return value === null || value === undefined || value === '' ? '-' : value;
|
||||||
|
},
|
||||||
|
formatMoney(value) {
|
||||||
|
return Number(value || 0).toFixed(2);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -56,6 +56,16 @@
|
|||||||
<el-form-item v-show="searchExpanded" label="所属组织">
|
<el-form-item v-show="searchExpanded" label="所属组织">
|
||||||
<el-input v-model="query.claimerDeptName" clearable placeholder="请输入" />
|
<el-input v-model="query.claimerDeptName" clearable placeholder="请输入" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item v-show="searchExpanded" label="推送状态">
|
||||||
|
<el-select v-model="query.pushStatus" clearable placeholder="请选择">
|
||||||
|
<el-option
|
||||||
|
v-for="item in pushStatusOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item v-show="searchExpanded" label="金蝶单据状态">
|
<el-form-item v-show="searchExpanded" label="金蝶单据状态">
|
||||||
<el-select v-model="query.kingdeeBillStatus" clearable placeholder="请选择">
|
<el-select v-model="query.kingdeeBillStatus" clearable placeholder="请选择">
|
||||||
<el-option
|
<el-option
|
||||||
@@ -70,17 +80,22 @@
|
|||||||
<div class="claim-record-page__search-actions">
|
<div class="claim-record-page__search-actions">
|
||||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
<el-button @click="resetSearch">重置</el-button>
|
<el-button @click="resetSearch">重置</el-button>
|
||||||
<el-tooltip :content="searchExpanded ? '折叠' : '展开'" placement="top">
|
<el-button
|
||||||
<el-button text @click="searchExpanded = !searchExpanded">
|
v-if="showSearchToggle"
|
||||||
<el-icon><component :is="searchExpanded ? ArrowUp : ArrowDown" /></el-icon>
|
text
|
||||||
</el-button>
|
:icon="searchExpanded ? ArrowUp : ArrowDown"
|
||||||
</el-tooltip>
|
@click="searchExpanded = !searchExpanded"
|
||||||
|
>{{ searchExpanded ? '收起' : '展开' }}</el-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
</section-card>
|
</section-card>
|
||||||
|
|
||||||
<section class="claim-record-page__table-panel">
|
<section class="claim-record-page__table-panel">
|
||||||
<div class="claim-record-page__toolbar">
|
<div class="claim-record-page__toolbar">
|
||||||
|
<el-button :loading="refreshing" @click="handleRefreshKingdeeResult">
|
||||||
|
查询金蝶认领结果
|
||||||
|
</el-button>
|
||||||
<table-header-tools
|
<table-header-tools
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
v-model:visible-column-keys="visibleColumnKeys"
|
v-model:visible-column-keys="visibleColumnKeys"
|
||||||
@@ -116,7 +131,11 @@
|
|||||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-link
|
<el-link
|
||||||
v-if="hasPermission('receipt_claim_record_void') && row.claimStatus === 'claimed'"
|
v-if="
|
||||||
|
hasPermission('receipt_claim_record_void') &&
|
||||||
|
row.claimStatus === 'claimed' &&
|
||||||
|
row.pushStatus !== 'pushed'
|
||||||
|
"
|
||||||
type="danger"
|
type="danger"
|
||||||
@click="handleVoid(row)"
|
@click="handleVoid(row)"
|
||||||
>作废</el-link
|
>作废</el-link
|
||||||
@@ -162,6 +181,7 @@ const emptyQuery = () => ({
|
|||||||
claimDateRange: [],
|
claimDateRange: [],
|
||||||
claimerDeptName: '',
|
claimerDeptName: '',
|
||||||
kingdeeBillStatus: '',
|
kingdeeBillStatus: '',
|
||||||
|
pushStatus: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -173,14 +193,19 @@ export default {
|
|||||||
ArrowUp,
|
ArrowUp,
|
||||||
query: emptyQuery(),
|
query: emptyQuery(),
|
||||||
searchExpanded: false,
|
searchExpanded: false,
|
||||||
|
// 与 avue-crud 一致:搜索项数量超过 searchIndex(默认一行 4 项)时才显示“展开/收起”
|
||||||
|
searchIndex: 4,
|
||||||
|
searchItemCount: 11,
|
||||||
searchVisible: true,
|
searchVisible: true,
|
||||||
tableSize: 'default',
|
tableSize: 'default',
|
||||||
claimStatusOptions: api.claimStatusOptions,
|
claimStatusOptions: api.claimStatusOptions,
|
||||||
kingdeeBillStatusOptions: api.kingdeeBillStatusOptions,
|
kingdeeBillStatusOptions: api.kingdeeBillStatusOptions,
|
||||||
|
pushStatusOptions: api.pushStatusOptions,
|
||||||
columns: receiptClaimRecordTableColumns,
|
columns: receiptClaimRecordTableColumns,
|
||||||
visibleColumnKeys: receiptClaimRecordTableColumns.map(column => column.prop),
|
visibleColumnKeys: receiptClaimRecordTableColumns.map(column => column.prop),
|
||||||
rows: [],
|
rows: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
|
refreshing: false,
|
||||||
page: { current: 1, size: 10, total: 0 },
|
page: { current: 1, size: 10, total: 0 },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -189,6 +214,9 @@ export default {
|
|||||||
visibleColumns() {
|
visibleColumns() {
|
||||||
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
||||||
},
|
},
|
||||||
|
showSearchToggle() {
|
||||||
|
return this.searchItemCount > this.searchIndex;
|
||||||
|
},
|
||||||
isAdmin() {
|
isAdmin() {
|
||||||
return isAdministrator(this.userInfo);
|
return isAdministrator(this.userInfo);
|
||||||
},
|
},
|
||||||
@@ -198,8 +226,9 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
unwrapData(response) {
|
unwrapData(response) {
|
||||||
const body = response?.data || response || {};
|
// 数字 0 是合法返回值(如回写/同步 0 条),须用 ?? 判空避免 0 被信封对象顶掉导致 Number() 得 NaN
|
||||||
return body?.data || body;
|
const body = response?.data ?? response ?? {};
|
||||||
|
return body?.data ?? body;
|
||||||
},
|
},
|
||||||
queryParams() {
|
queryParams() {
|
||||||
const transactionRange = this.query.transactionDateRange || [];
|
const transactionRange = this.query.transactionDateRange || [];
|
||||||
@@ -243,14 +272,24 @@ export default {
|
|||||||
},
|
},
|
||||||
async handleVoid(row) {
|
async handleVoid(row) {
|
||||||
await this.$confirm(
|
await this.$confirm(
|
||||||
'作废后将回退本次认领金额,并生成审核通过状态的金蝶认领冲单,是否继续?',
|
'作废后将回退本次认领金额,是否继续?',
|
||||||
'作废认领记录',
|
'作废认领记录',
|
||||||
{ type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }
|
{ type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }
|
||||||
);
|
);
|
||||||
const kingdeeBillNo = this.unwrapData(await api.voidClaim(row.id));
|
await api.voidClaim(row.id);
|
||||||
this.$message.success(`作废成功,金蝶冲单号:${kingdeeBillNo}`);
|
this.$message.success('作废成功');
|
||||||
await this.loadTable();
|
await this.loadTable();
|
||||||
},
|
},
|
||||||
|
async handleRefreshKingdeeResult() {
|
||||||
|
this.refreshing = true;
|
||||||
|
try {
|
||||||
|
const updated = Number(this.unwrapData(await api.queryKingdeeResult()) || 0);
|
||||||
|
this.$message.success(`金蝶认领结果回写完成,更新${updated}条`);
|
||||||
|
await this.loadTable();
|
||||||
|
} finally {
|
||||||
|
this.refreshing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
hasPermission(code) {
|
hasPermission(code) {
|
||||||
return this.isAdmin || this.validData(this.permission?.[code], false);
|
return this.isAdmin || this.validData(this.permission?.[code], false);
|
||||||
},
|
},
|
||||||
@@ -297,7 +336,7 @@ export default {
|
|||||||
}
|
}
|
||||||
.claim-record-page__toolbar {
|
.claim-record-page__toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,12 @@
|
|||||||
<el-table-column label="已认领金额" min-width="130" align="right">
|
<el-table-column label="已认领金额" min-width="130" align="right">
|
||||||
<template #default="{ row }">{{ formatMoney(row.claimedAmount) }}</template>
|
<template #default="{ row }">{{ formatMoney(row.claimedAmount) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="金蝶未认领金额" min-width="140" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.unclaimedAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="剩余可认领金额" min-width="140" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(flowRemaining(row)) }}</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="summary" label="摘要" min-width="180" show-overflow-tooltip />
|
<el-table-column prop="summary" label="摘要" min-width="180" show-overflow-tooltip />
|
||||||
</el-table>
|
</el-table>
|
||||||
<el-row :gutter="32" class="receipt-claim-batch-form-page__claim-info">
|
<el-row :gutter="32" class="receipt-claim-batch-form-page__claim-info">
|
||||||
@@ -295,10 +301,21 @@ export default {
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
flowRemaining(flow) {
|
||||||
|
return Math.max(Number(flow.unclaimedAmount || 0) - Number(flow.claimedAmount || 0), 0);
|
||||||
|
},
|
||||||
maxAllocation(row) {
|
maxAllocation(row) {
|
||||||
const current = Number(row.settlement.allocations?.[row.flow.id] || 0);
|
const current = Number(row.settlement.allocations?.[row.flow.id] || 0);
|
||||||
const otherTotal = this.settlementAllocatedTotal(row.settlement) - current;
|
const otherTotal = this.settlementAllocatedTotal(row.settlement) - current;
|
||||||
return Math.max(this.settlementRemaining(row.settlement) - otherTotal, 0);
|
// 流水维度:该流水其余分摊已占用部分同样挤占剩余可认领金额
|
||||||
|
const flowOtherTotal = this.flowAllocatedTotal(row.flow.id) - current;
|
||||||
|
return Math.max(
|
||||||
|
Math.min(
|
||||||
|
this.settlementRemaining(row.settlement) - otherTotal,
|
||||||
|
this.flowRemaining(row.flow) - flowOtherTotal
|
||||||
|
),
|
||||||
|
0
|
||||||
|
);
|
||||||
},
|
},
|
||||||
handleAllocationChange(settlement) {
|
handleAllocationChange(settlement) {
|
||||||
const total = this.settlementAllocatedTotal(settlement);
|
const total = this.settlementAllocatedTotal(settlement);
|
||||||
@@ -336,11 +353,11 @@ export default {
|
|||||||
for (const flow of this.flows) {
|
for (const flow of this.flows) {
|
||||||
const total = this.flowAllocatedTotal(flow.id);
|
const total = this.flowAllocatedTotal(flow.id);
|
||||||
const remaining = Math.max(
|
const remaining = Math.max(
|
||||||
Number(flow.receiptAmount || 0) - Number(flow.claimedAmount || 0),
|
Number(flow.unclaimedAmount || 0) - Number(flow.claimedAmount || 0),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
if (total > remaining + 0.001) {
|
if (total > remaining + 0.001) {
|
||||||
this.$message.warning(`流水${flow.receiptNoticeNo}的分摊金额不能超过剩余可认领金额`);
|
this.$message.warning(`流水${flow.receiptNoticeNo}的累计认领金额不能超过公告未认领金额`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,14 @@
|
|||||||
><el-form-item label="已认领金额"
|
><el-form-item label="已认领金额"
|
||||||
><el-input :model-value="formatMoney(form.claimedAmount)" disabled /></el-form-item
|
><el-input :model-value="formatMoney(form.claimedAmount)" disabled /></el-form-item
|
||||||
></el-col>
|
></el-col>
|
||||||
|
<el-col :span="6"
|
||||||
|
><el-form-item label="金蝶未认领金额"
|
||||||
|
><el-input :model-value="formatMoney(form.unclaimedAmount)" disabled /></el-form-item
|
||||||
|
></el-col>
|
||||||
|
<el-col :span="6"
|
||||||
|
><el-form-item label="剩余可认领金额"
|
||||||
|
><el-input :model-value="formatMoney(claimableAmount)" disabled /></el-form-item
|
||||||
|
></el-col>
|
||||||
<el-col :span="6"
|
<el-col :span="6"
|
||||||
><el-form-item label="摘要"><el-input v-model="form.summary" disabled /></el-form-item
|
><el-form-item label="摘要"><el-input v-model="form.summary" disabled /></el-form-item
|
||||||
></el-col>
|
></el-col>
|
||||||
@@ -197,6 +205,7 @@ const emptyForm = () => ({
|
|||||||
transactionTime: '',
|
transactionTime: '',
|
||||||
detailSerialNo: '',
|
detailSerialNo: '',
|
||||||
claimedAmount: 0,
|
claimedAmount: 0,
|
||||||
|
unclaimedAmount: 0,
|
||||||
claimerName: '',
|
claimerName: '',
|
||||||
claimerDeptName: '',
|
claimerDeptName: '',
|
||||||
claimDate: '',
|
claimDate: '',
|
||||||
@@ -242,6 +251,12 @@ export default {
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
claimableAmount() {
|
||||||
|
return Math.max(
|
||||||
|
Number(this.form.unclaimedAmount || 0) - Number(this.form.claimedAmount || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (!this.batchMode) this.initialize();
|
if (!this.batchMode) this.initialize();
|
||||||
@@ -331,8 +346,8 @@ export default {
|
|||||||
this.$message.warning('分摊收款金额必须大于0');
|
this.$message.warning('分摊收款金额必须大于0');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.allocatedTotal > amount - Number(this.form.claimedAmount || 0) + 0.001) {
|
if (this.allocatedTotal > this.claimableAmount + 0.001) {
|
||||||
this.$message.warning('本次分摊金额不能超过流水剩余可认领金额');
|
this.$message.warning('累计认领金额不能超过公告未认领金额(剩余可认领金额)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const row of this.form.settlements)
|
for (const row of this.form.settlements)
|
||||||
|
|||||||
@@ -15,10 +15,17 @@
|
|||||||
<el-form-item label="对方账号">
|
<el-form-item label="对方账号">
|
||||||
<el-input v-model="query.counterpartyAccount" clearable placeholder="请输入" />
|
<el-input v-model="query.counterpartyAccount" clearable placeholder="请输入" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-show="searchExpanded" label="摘要">
|
<el-form-item label="推送状态">
|
||||||
<el-input v-model="query.summary" clearable placeholder="请输入" />
|
<el-select v-model="query.pushStatus" clearable placeholder="请选择">
|
||||||
|
<el-option
|
||||||
|
v-for="item in pushStatusOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-show="searchExpanded" label="认领状态">
|
<el-form-item label="认领状态">
|
||||||
<el-select v-model="query.claimStatus" clearable placeholder="请选择">
|
<el-select v-model="query.claimStatus" clearable placeholder="请选择">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in claimStatusOptions"
|
v-for="item in claimStatusOptions"
|
||||||
@@ -28,7 +35,7 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-show="searchExpanded" label="交易时间">
|
<el-form-item label="交易时间">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="query.transactionTimeRange"
|
v-model="query.transactionTimeRange"
|
||||||
type="datetimerange"
|
type="datetimerange"
|
||||||
@@ -39,13 +46,56 @@
|
|||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item v-show="searchExpanded" label="对方开户行">
|
||||||
|
<el-input v-model="query.counterpartyBank" clearable placeholder="请输入" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-show="searchExpanded" label="摘要">
|
||||||
|
<el-input v-model="query.summary" clearable placeholder="请输入" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-show="searchExpanded" label="明细流水号">
|
||||||
|
<el-input v-model="query.detailSerialNo" clearable placeholder="请输入" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-show="searchExpanded" label="金蝶状态">
|
||||||
|
<el-select v-model="query.kingdeeBillStatus" clearable placeholder="请选择">
|
||||||
|
<el-option
|
||||||
|
v-for="item in kingdeeBillStatusOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-show="searchExpanded" label="收款金额">
|
||||||
|
<div class="amount-range">
|
||||||
|
<el-input-number
|
||||||
|
v-model="query.receiptAmountStart"
|
||||||
|
:controls="false"
|
||||||
|
:min="0"
|
||||||
|
placeholder="最低金额"
|
||||||
|
/>
|
||||||
|
<span>~</span>
|
||||||
|
<el-input-number
|
||||||
|
v-model="query.receiptAmountEnd"
|
||||||
|
:controls="false"
|
||||||
|
:min="0"
|
||||||
|
placeholder="最高金额"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-show="searchExpanded" label="认领人">
|
||||||
|
<el-input v-model="query.claimerName" clearable placeholder="请输入" />
|
||||||
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
<div class="receipt-flow-page__search-actions">
|
<div class="receipt-flow-page__search-actions">
|
||||||
<el-button @click="searchExpanded = !searchExpanded">
|
|
||||||
{{ searchExpanded ? '折叠' : '展开' }}
|
|
||||||
</el-button>
|
|
||||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
<el-button @click="resetSearch">重置</el-button>
|
<el-button @click="resetSearch">重置</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="showSearchToggle"
|
||||||
|
text
|
||||||
|
:icon="searchExpanded ? ArrowUp : ArrowDown"
|
||||||
|
@click="searchExpanded = !searchExpanded"
|
||||||
|
>{{ searchExpanded ? '收起' : '展开' }}</el-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
</section>
|
</section>
|
||||||
@@ -100,7 +150,7 @@
|
|||||||
</el-link>
|
</el-link>
|
||||||
<el-tag
|
<el-tag
|
||||||
v-else-if="column.status"
|
v-else-if="column.status"
|
||||||
:type="statusType(row.claimStatus)"
|
:type="statusType(row, column)"
|
||||||
class="status-text"
|
class="status-text"
|
||||||
>
|
>
|
||||||
{{ displayValue(row[column.prop]) }}
|
{{ displayValue(row[column.prop]) }}
|
||||||
@@ -109,10 +159,23 @@
|
|||||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
<el-table-column label="操作" width="170" fixed="right" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-link v-if="canClaim(row)" type="primary" @click="openClaim(row)">认领</el-link>
|
<el-link
|
||||||
<span v-else>-</span>
|
v-if="canClaim(row)"
|
||||||
|
type="primary"
|
||||||
|
:disabled="pushingId === row.id"
|
||||||
|
@click="openClaim(row)"
|
||||||
|
>认领</el-link
|
||||||
|
>
|
||||||
|
<el-link
|
||||||
|
v-if="canPush(row)"
|
||||||
|
type="warning"
|
||||||
|
:disabled="pushingId === row.id"
|
||||||
|
@click="handlePush(row)"
|
||||||
|
>推送金蝶</el-link
|
||||||
|
>
|
||||||
|
<span v-if="!canClaim(row) && !canPush(row)">-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -163,7 +226,7 @@
|
|||||||
{{ displayValue(detailDialog.data.detailSerialNo) }}
|
{{ displayValue(detailDialog.data.detailSerialNo) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="认领状态">
|
<el-descriptions-item label="认领状态">
|
||||||
<el-tag :type="statusType(detailDialog.data.claimStatus)">
|
<el-tag :type="statusType(detailDialog.data, { prop: 'claimStatusName' })">
|
||||||
{{ displayValue(detailDialog.data.claimStatusName) }}
|
{{ displayValue(detailDialog.data.claimStatusName) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -173,6 +236,23 @@
|
|||||||
<el-descriptions-item label="剩余可认领金额">
|
<el-descriptions-item label="剩余可认领金额">
|
||||||
{{ formatMoney(detailDialog.data.remainingAmount) }}
|
{{ formatMoney(detailDialog.data.remainingAmount) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="金蝶未认领金额">
|
||||||
|
{{ formatMoney(detailDialog.data.unclaimedAmount) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="推送状态">
|
||||||
|
<el-tag :type="statusType(detailDialog.data, { prop: 'pushStatusName' })">
|
||||||
|
{{ displayValue(detailDialog.data.pushStatusName) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="金蝶收款单号">
|
||||||
|
{{ displayValue(detailDialog.data.kingdeeBillNo) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="金蝶状态">
|
||||||
|
{{ displayValue(detailDialog.data.kingdeeBillStatusName) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="推送失败原因">
|
||||||
|
{{ displayValue(detailDialog.data.pushFailReason) }}
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="摘要">
|
<el-descriptions-item label="摘要">
|
||||||
{{ displayValue(detailDialog.data.summary) }}
|
{{ displayValue(detailDialog.data.summary) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -187,7 +267,9 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="operatorName" label="认领人" min-width="120" align="center" />
|
<el-table-column prop="operatorName" label="认领人" min-width="120" align="center" />
|
||||||
<el-table-column label="状态变化" min-width="170" align="center">
|
<el-table-column label="状态变化" min-width="170" align="center">
|
||||||
<template #default="{ row }">{{ claimStatusChange(row) }}</template>
|
<template #default="{ row }">
|
||||||
|
{{ row.actionType === 'push' ? row.actionName : claimStatusChange(row) }}
|
||||||
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="content" label="认领说明" min-width="220" />
|
<el-table-column prop="content" label="认领说明" min-width="220" />
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -200,77 +282,26 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import * as api from '@/api/payment/receiptFlow';
|
import * as api from '@/api/payment/receiptFlow';
|
||||||
import { receiptFlowTableColumns } from '@/option/payment/receiptFlow';
|
import { receiptFlowTableColumns } from '@/option/payment/receiptFlow';
|
||||||
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||||||
import { isAdministrator } from '@/utils/auth';
|
import { isAdministrator } from '@/utils/auth';
|
||||||
|
|
||||||
const receiptFlowMockTemplates = [
|
|
||||||
{
|
|
||||||
receiptNoticeNo: 'SKTZ20260827001',
|
|
||||||
payerName: '华东供应链管理有限公司',
|
|
||||||
receiptAmount: 48000,
|
|
||||||
counterpartyName: '上海安捷运输有限公司',
|
|
||||||
counterpartyAccount: '310000000000001001',
|
|
||||||
counterpartyBank: '中国工商银行上海分行',
|
|
||||||
summary: '运输服务款',
|
|
||||||
transactionTime: '2026-08-27 09:30:00',
|
|
||||||
detailSerialNo: 'MOCK-RF-20260827-001',
|
|
||||||
sourceUpdatedTime: '2026-08-27 09:30:00',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
receiptNoticeNo: 'SKTZ20260827002',
|
|
||||||
payerName: '华东供应链管理有限公司',
|
|
||||||
receiptAmount: 26500,
|
|
||||||
counterpartyName: '江苏联运物流有限公司',
|
|
||||||
counterpartyAccount: '320100000000002002',
|
|
||||||
counterpartyBank: '中国建设银行南京分行',
|
|
||||||
summary: '物流服务款',
|
|
||||||
transactionTime: '2026-08-27 13:45:00',
|
|
||||||
detailSerialNo: 'MOCK-RF-20260827-002',
|
|
||||||
sourceUpdatedTime: '2026-08-27 13:45:00',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const randomDigits = length =>
|
|
||||||
Array.from({ length }, () => Math.floor(Math.random() * 10)).join('');
|
|
||||||
|
|
||||||
const formatMockDateTime = date => {
|
|
||||||
const pad = value => String(value).padStart(2, '0');
|
|
||||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(
|
|
||||||
date.getHours()
|
|
||||||
)}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createReceiptFlowMockRows = () => {
|
|
||||||
const count = 20 + Math.floor(Math.random() * 31);
|
|
||||||
return Array.from({ length: count }, (_, index) => {
|
|
||||||
const template = receiptFlowMockTemplates[index % receiptFlowMockTemplates.length];
|
|
||||||
const transactionDate = new Date();
|
|
||||||
transactionDate.setDate(transactionDate.getDate() - Math.floor(Math.random() * 7));
|
|
||||||
transactionDate.setHours(8 + Math.floor(Math.random() * 10), Math.floor(Math.random() * 60), 0);
|
|
||||||
const transactionTime = formatMockDateTime(transactionDate);
|
|
||||||
const dateCode = transactionTime.slice(0, 10).replaceAll('-', '');
|
|
||||||
return {
|
|
||||||
...template,
|
|
||||||
receiptNoticeNo: `SKTZ${dateCode}${randomDigits(5)}`,
|
|
||||||
receiptAmount: Math.floor(10000 + Math.random() * 90000),
|
|
||||||
counterpartyAccount: `${template.counterpartyAccount.slice(0, -6)}${randomDigits(6)}`,
|
|
||||||
transactionTime,
|
|
||||||
detailSerialNo: `MOCK-RF-${dateCode}-${index + 1}${randomDigits(4)}`,
|
|
||||||
sourceUpdatedTime: transactionTime,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const emptyQuery = () => ({
|
const emptyQuery = () => ({
|
||||||
receiptNoticeNo: '',
|
receiptNoticeNo: '',
|
||||||
counterpartyName: '',
|
counterpartyName: '',
|
||||||
counterpartyBank: '',
|
counterpartyBank: '',
|
||||||
counterpartyAccount: '',
|
counterpartyAccount: '',
|
||||||
summary: '',
|
summary: '',
|
||||||
|
detailSerialNo: '',
|
||||||
claimStatus: '',
|
claimStatus: '',
|
||||||
|
pushStatus: '',
|
||||||
|
kingdeeBillStatus: '',
|
||||||
|
receiptAmountStart: undefined,
|
||||||
|
receiptAmountEnd: undefined,
|
||||||
|
claimerName: '',
|
||||||
transactionTimeRange: [],
|
transactionTimeRange: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -279,8 +310,12 @@ export default {
|
|||||||
components: { TableHeaderTools },
|
components: { TableHeaderTools },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
ArrowDown,
|
||||||
|
ArrowUp,
|
||||||
query: emptyQuery(),
|
query: emptyQuery(),
|
||||||
claimStatusOptions: api.claimStatusOptions,
|
claimStatusOptions: api.claimStatusOptions,
|
||||||
|
pushStatusOptions: api.pushStatusOptions,
|
||||||
|
kingdeeBillStatusOptions: api.kingdeeBillStatusOptions,
|
||||||
columns: receiptFlowTableColumns,
|
columns: receiptFlowTableColumns,
|
||||||
visibleColumnKeys: receiptFlowTableColumns.map(column => column.prop),
|
visibleColumnKeys: receiptFlowTableColumns.map(column => column.prop),
|
||||||
searchVisible: true,
|
searchVisible: true,
|
||||||
@@ -289,7 +324,11 @@ export default {
|
|||||||
selectedRows: [],
|
selectedRows: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
syncing: false,
|
syncing: false,
|
||||||
|
pushingId: null,
|
||||||
searchExpanded: false,
|
searchExpanded: false,
|
||||||
|
// 与 avue-crud 一致:搜索项数量超过 searchIndex(默认一行 4 项)时才显示“展开/收起”
|
||||||
|
searchIndex: 4,
|
||||||
|
searchItemCount: 7,
|
||||||
page: { current: 1, size: 10, total: 0 },
|
page: { current: 1, size: 10, total: 0 },
|
||||||
detailDialog: { visible: false, loading: false, data: {}, claimRecords: [] },
|
detailDialog: { visible: false, loading: false, data: {}, claimRecords: [] },
|
||||||
};
|
};
|
||||||
@@ -299,6 +338,9 @@ export default {
|
|||||||
visibleColumns() {
|
visibleColumns() {
|
||||||
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
||||||
},
|
},
|
||||||
|
showSearchToggle() {
|
||||||
|
return this.searchItemCount > this.searchIndex;
|
||||||
|
},
|
||||||
isAdmin() {
|
isAdmin() {
|
||||||
return isAdministrator(this.userInfo);
|
return isAdministrator(this.userInfo);
|
||||||
},
|
},
|
||||||
@@ -308,8 +350,9 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
unwrapData(response) {
|
unwrapData(response) {
|
||||||
const body = response?.data || response || {};
|
// 数字 0 是合法返回值(如回写/同步 0 条),须用 ?? 判空避免 0 被信封对象顶掉导致 Number() 得 NaN
|
||||||
return body?.data || body;
|
const body = response?.data ?? response ?? {};
|
||||||
|
return body?.data ?? body;
|
||||||
},
|
},
|
||||||
async loadTable() {
|
async loadTable() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@@ -345,9 +388,8 @@ export default {
|
|||||||
async handleSync() {
|
async handleSync() {
|
||||||
this.syncing = true;
|
this.syncing = true;
|
||||||
try {
|
try {
|
||||||
const mockRows = createReceiptFlowMockRows();
|
const syncedCount = Number(this.unwrapData(await api.sync()) || 0);
|
||||||
const syncedCount = Number(this.unwrapData(await api.sync({ flows: mockRows })) || 0);
|
this.$message.success(`流水同步完成,共同步${syncedCount}条认领公告`);
|
||||||
this.$message.success(`流水同步完成,共同步${syncedCount}条`);
|
|
||||||
await this.loadTable();
|
await this.loadTable();
|
||||||
} finally {
|
} finally {
|
||||||
this.syncing = false;
|
this.syncing = false;
|
||||||
@@ -406,8 +448,41 @@ export default {
|
|||||||
canClaim(row) {
|
canClaim(row) {
|
||||||
return this.hasPermission('receipt_flow_claim') && Number(row.remainingAmount || 0) > 0;
|
return this.hasPermission('receipt_flow_claim') && Number(row.remainingAmount || 0) > 0;
|
||||||
},
|
},
|
||||||
statusType(status) {
|
canPush(row) {
|
||||||
return { claimed: 'success', partial: 'warning', unclaimed: 'info' }[status] || '';
|
return (
|
||||||
|
this.hasPermission('receipt_flow_push') &&
|
||||||
|
!!row.billId &&
|
||||||
|
row.claimStatus === 'claimed' &&
|
||||||
|
row.pushStatus !== 'pushed'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async handlePush(row) {
|
||||||
|
try {
|
||||||
|
await this.$confirm(
|
||||||
|
`确认将流水【${row.receiptNoticeNo || row.detailSerialNo || ''}】推送金蝶生成收款单?`,
|
||||||
|
'推送金蝶',
|
||||||
|
{ type: 'warning' }
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.pushingId = row.id;
|
||||||
|
try {
|
||||||
|
const data = this.unwrapData(await api.pushKingdee(row.id));
|
||||||
|
const billNo = data?.kingdeeBillNo;
|
||||||
|
this.$message.success(billNo ? `推送成功,金蝶收款单号:${billNo}` : '推送成功');
|
||||||
|
await this.loadTable();
|
||||||
|
} finally {
|
||||||
|
this.pushingId = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
statusType(row, column) {
|
||||||
|
if (column?.prop === 'pushStatusName') {
|
||||||
|
return { pushed: 'success', failed: 'danger' }[row.pushStatus] || 'info';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
{ claimed: 'success', partial: 'warning', unclaimed: 'info' }[row.claimStatus] || ''
|
||||||
|
);
|
||||||
},
|
},
|
||||||
claimStatusChange(row) {
|
claimStatusChange(row) {
|
||||||
const label = status =>
|
const label = status =>
|
||||||
@@ -440,7 +515,14 @@ export default {
|
|||||||
}
|
}
|
||||||
.receipt-flow-page__search :deep(.el-input),
|
.receipt-flow-page__search :deep(.el-input),
|
||||||
.receipt-flow-page__search :deep(.el-select),
|
.receipt-flow-page__search :deep(.el-select),
|
||||||
.receipt-flow-page__search :deep(.el-date-editor) {
|
.receipt-flow-page__search :deep(.el-date-editor),
|
||||||
|
.receipt-flow-page__search :deep(.el-input-number) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.receipt-flow-page__search .amount-range {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.receipt-flow-page__search-actions {
|
.receipt-flow-page__search-actions {
|
||||||
@@ -474,6 +556,9 @@ export default {
|
|||||||
.receipt-flow-page :deep(.el-table) {
|
.receipt-flow-page :deep(.el-table) {
|
||||||
--el-table-border-color: #eff1f7;
|
--el-table-border-color: #eff1f7;
|
||||||
}
|
}
|
||||||
|
.receipt-flow-page :deep(.el-table .cell .el-link + .el-link) {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
.receipt-flow-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
|
.receipt-flow-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
label-width="auto"
|
label-width="auto"
|
||||||
>
|
>
|
||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col v-for="field in fields" :key="field.prop" :span="6">
|
<el-col v-for="field in visibleFields" :key="field.prop" :span="6">
|
||||||
<el-form-item :label="field.label" :prop="field.prop">
|
<el-form-item :label="field.label" :prop="field.prop">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-if="field.type === 'date' && editable"
|
v-if="field.type === 'date' && editable"
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
type="date"
|
type="date"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
placeholder="请选择"
|
placeholder="请选择"
|
||||||
|
@change="fetchBfiExchangeRate"
|
||||||
/>
|
/>
|
||||||
<el-select
|
<el-select
|
||||||
v-else-if="field.type === 'project' && editable"
|
v-else-if="field.type === 'project' && editable"
|
||||||
@@ -63,6 +64,12 @@
|
|||||||
:precision="2"
|
:precision="2"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
/>
|
/>
|
||||||
|
<span
|
||||||
|
v-else-if="field.prop === 'formalSettlementNo'"
|
||||||
|
:class="{ 'form-readonly--code': form.formalSettlementNo }"
|
||||||
|
>
|
||||||
|
{{ form.formalSettlementNo || (form.id ? '-' : '系统自动生成') }}
|
||||||
|
</span>
|
||||||
<span v-else-if="field.money">{{ formatMoney(form[field.prop]) }}</span>
|
<span v-else-if="field.money">{{ formatMoney(form[field.prop]) }}</span>
|
||||||
<span v-else-if="field.type === 'contract'">{{
|
<span v-else-if="field.type === 'contract'">{{
|
||||||
displayValue(form.contractName)
|
displayValue(form.contractName)
|
||||||
@@ -245,24 +252,36 @@
|
|||||||
:index="detailIndexMethod"
|
:index="detailIndexMethod"
|
||||||
/>
|
/>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
v-for="column in detailTableColumns"
|
v-for="item in detailDisplayColumns"
|
||||||
:key="column.prop"
|
:key="item.key"
|
||||||
v-bind="column"
|
:label="item.kind === 'fee' ? item.name : item.column.label"
|
||||||
|
:prop="item.kind === 'fee' ? undefined : item.column.prop"
|
||||||
|
:min-width="item.kind === 'fee' ? 130 : item.column.minWidth"
|
||||||
align="center"
|
align="center"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
>
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
<span v-if="item.kind === 'fee'">
|
||||||
<span v-else-if="column.prop === 'transportQuantity'">
|
{{ formatMoney(parseFeeItems(row.feeItemsJson || row.feeItems)[item.name]) }}
|
||||||
{{ formatDetailTransportQuantity(row[column.prop]) }}
|
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="column.prop === 'mileage'">
|
<el-link
|
||||||
{{ formatDetailMileage(row[column.prop]) }}
|
v-else-if="item.column.link && row[item.column.prop] && !publicMode"
|
||||||
|
type="primary"
|
||||||
|
@click.stop="handleDetailLink(row, item.column)"
|
||||||
|
>
|
||||||
|
{{ row[item.column.prop] }}
|
||||||
|
</el-link>
|
||||||
|
<span v-else-if="item.column.money">{{ formatMoney(row[item.column.prop]) }}</span>
|
||||||
|
<span v-else-if="item.column.prop === 'transportQuantity'">
|
||||||
|
{{ formatDetailTransportQuantity(row[item.column.prop]) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="column.prop === 'transportType'">
|
<span v-else-if="item.column.prop === 'mileage'">
|
||||||
{{ transportTypeName(row[column.prop]) }}
|
{{ formatDetailMileage(row[item.column.prop]) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
<span v-else-if="item.column.prop === 'transportType'">
|
||||||
|
{{ transportTypeName(row[item.column.prop]) }}
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ displayValue(row[item.column.prop]) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="editable" label="操作" width="150" fixed="right" align="center">
|
<el-table-column v-if="editable" label="操作" width="150" fixed="right" align="center">
|
||||||
@@ -377,6 +396,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</section-card>
|
</section-card>
|
||||||
|
|
||||||
|
<project-advance-write-off
|
||||||
|
ref="projectAdvanceWriteOffRef"
|
||||||
|
v-model="projectAdvanceWriteOffs"
|
||||||
|
:editable="editable"
|
||||||
|
:contract-id="form.contractId"
|
||||||
|
:settlement-amount="form.settlementAmount"
|
||||||
|
settlement-type="formal"
|
||||||
|
:settlement-id="form.id"
|
||||||
|
/>
|
||||||
|
|
||||||
<section-card title="发票信息">
|
<section-card title="发票信息">
|
||||||
<div class="formal-editor__invoice-claim">
|
<div class="formal-editor__invoice-claim">
|
||||||
<el-form inline label-position="right" label-width="auto" @submit.prevent>
|
<el-form inline label-position="right" label-width="auto" @submit.prevent>
|
||||||
@@ -448,78 +477,61 @@
|
|||||||
<span v-else>{{ formatMoney(row.matchedAmount) }}</span>
|
<span v-else>{{ formatMoney(row.matchedAmount) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="220" align="center">
|
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||||
<template #default="{ row, $index }">
|
<template #default="{ row, $index }">
|
||||||
<div class="formal-editor__links">
|
<div class="formal-editor__links">
|
||||||
<el-link type="primary" @click="viewInvoiceAttachment(row)">查看</el-link>
|
<el-link
|
||||||
<el-link type="primary" @click="downloadInvoiceAttachment(row)">下载</el-link>
|
type="primary"
|
||||||
|
:disabled="kingdeeAttachmentBusy"
|
||||||
|
@click="viewInvoiceAttachment(row)"
|
||||||
|
>查看</el-link
|
||||||
|
>
|
||||||
|
<el-link
|
||||||
|
type="primary"
|
||||||
|
:disabled="kingdeeAttachmentBusy"
|
||||||
|
@click="downloadInvoiceAttachment(row)"
|
||||||
|
>下载</el-link
|
||||||
|
>
|
||||||
<el-link v-if="editable" type="danger" @click="removeInvoice($index)">删除</el-link>
|
<el-link v-if="editable" type="danger" @click="removeInvoice($index)">删除</el-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
|
<div v-if="issuedInvoices.length" class="formal-editor__issued-invoices">
|
||||||
|
<div class="formal-editor__issued-title">
|
||||||
|
开票发票(本结算单开票申请经金蝶开票回写,文件由金蝶发票云直链提供)
|
||||||
|
</div>
|
||||||
|
<el-table :data="issuedInvoices" border>
|
||||||
|
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||||
|
<el-table-column prop="invoiceNo" label="发票号" min-width="150" align="center" />
|
||||||
|
<el-table-column prop="invoiceDate" label="开票日期" min-width="130" align="center">
|
||||||
|
<template #default="{ row }">{{ displayValue(row.invoiceDate) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="invoiceAmount" label="价税合计" min-width="130" align="center">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="formal-editor__links">
|
||||||
|
<el-link type="primary" @click="viewIssuedInvoice(row)">查看</el-link>
|
||||||
|
<el-link type="primary" @click="downloadIssuedInvoice(row)">下载</el-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
</section-card>
|
</section-card>
|
||||||
|
|
||||||
<el-dialog
|
<kingdee-invoice-pool-dialog
|
||||||
v-model="invoiceClaimDialog.visible"
|
v-model="invoiceClaimDialog.visible"
|
||||||
title="查询可认领发票"
|
title="查询可认领发票"
|
||||||
width="92%"
|
confirm-text="确认认领"
|
||||||
append-to-body
|
multiple
|
||||||
destroy-on-close
|
:initial-invoice-no="String(invoiceClaimNo || '').trim()"
|
||||||
>
|
:query-api="invoicePoolQueryApi"
|
||||||
<el-table
|
@confirm="handleInvoiceClaimConfirm"
|
||||||
ref="invoiceClaimTable"
|
/>
|
||||||
v-loading="invoiceClaimDialog.loading"
|
|
||||||
:data="invoiceClaimPagedRows"
|
|
||||||
row-key="invoiceNo"
|
|
||||||
border
|
|
||||||
height="520"
|
|
||||||
@selection-change="invoiceClaimDialog.selected = $event"
|
|
||||||
>
|
|
||||||
<el-table-column
|
|
||||||
type="selection"
|
|
||||||
width="52"
|
|
||||||
fixed="left"
|
|
||||||
align="center"
|
|
||||||
reserve-selection
|
|
||||||
/>
|
|
||||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
|
||||||
<el-table-column prop="invoiceNo" label="发票号" min-width="180" align="center" />
|
|
||||||
<el-table-column prop="invoiceDate" label="开票日期" min-width="130" align="center" />
|
|
||||||
<el-table-column prop="invoiceType" label="发票类型" min-width="150" align="center" />
|
|
||||||
<el-table-column prop="issuerName" label="开票单位" min-width="180" align="center" />
|
|
||||||
<el-table-column prop="receiverName" label="受票单位" min-width="180" align="center" />
|
|
||||||
<el-table-column
|
|
||||||
prop="invoiceAmount"
|
|
||||||
label="发票金额(含税)"
|
|
||||||
min-width="160"
|
|
||||||
align="right"
|
|
||||||
>
|
|
||||||
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="matchedAmount" label="认领金额" min-width="140" align="right">
|
|
||||||
<template #default="{ row }">{{ formatMoney(row.matchedAmount) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div class="formal-editor__pagination">
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="invoiceClaimDialog.page.current"
|
|
||||||
v-model:page-size="invoiceClaimDialog.page.size"
|
|
||||||
:total="invoiceClaimDialog.page.total"
|
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="invoiceClaimDialog.visible = false">取消</el-button>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
:loading="invoiceClaimDialog.confirming"
|
|
||||||
@click="confirmInvoiceClaims"
|
|
||||||
>确认认领</el-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<section-card
|
<section-card
|
||||||
v-if="readonly"
|
v-if="readonly"
|
||||||
@@ -658,9 +670,10 @@
|
|||||||
</section-card>
|
</section-card>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="!pageMode" #footer>
|
<template v-if="!pageMode" #footer>
|
||||||
|
<el-button v-if="editable" @click="handleSyncPlaceholder">同步</el-button>
|
||||||
<el-button @click="visible = false">取消</el-button>
|
<el-button @click="visible = false">取消</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="editable && !recordId"
|
v-if="editable"
|
||||||
type="primary"
|
type="primary"
|
||||||
plain
|
plain
|
||||||
:loading="draftSaving"
|
:loading="draftSaving"
|
||||||
@@ -673,9 +686,10 @@
|
|||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="pageMode && !publicMode" class="formal-editor__page-actions">
|
<div v-if="pageMode && !publicMode" class="formal-editor__page-actions">
|
||||||
|
<el-button v-if="editable" @click="handleSyncPlaceholder">同步</el-button>
|
||||||
<el-button @click="visible = false">取消</el-button>
|
<el-button @click="visible = false">取消</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="editable && !recordId"
|
v-if="editable"
|
||||||
type="primary"
|
type="primary"
|
||||||
plain
|
plain
|
||||||
:loading="draftSaving"
|
:loading="draftSaving"
|
||||||
@@ -689,24 +703,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="candidate.visible" title="选择预结算单" width="88%" append-to-body>
|
<el-dialog v-model="candidate.visible" title="选择预结算单" width="88%" append-to-body>
|
||||||
<el-form
|
<el-card>
|
||||||
:model="candidate.query"
|
<el-form
|
||||||
inline
|
:model="candidate.query"
|
||||||
label-position="right"
|
inline
|
||||||
label-width="88px"
|
label-position="right"
|
||||||
class="formal-editor__candidate-search"
|
label-width="88px"
|
||||||
>
|
class="formal-editor__candidate-search"
|
||||||
<el-form-item label="预结算单号"
|
>
|
||||||
|
<el-form-item label="预结算单号"
|
||||||
><el-input v-model="candidate.query.preSettlementNo" clearable
|
><el-input v-model="candidate.query.preSettlementNo" clearable
|
||||||
/></el-form-item>
|
/></el-form-item>
|
||||||
<el-form-item label="合同编号"
|
<el-form-item label="合同编号"
|
||||||
><el-input v-model="candidate.query.contractNo" clearable
|
><el-input v-model="candidate.query.contractNo" clearable
|
||||||
/></el-form-item>
|
/></el-form-item>
|
||||||
<el-form-item
|
<el-form-item
|
||||||
><el-button @click="resetCandidates">重置</el-button
|
><el-button @click="resetCandidates">重置</el-button
|
||||||
><el-button type="primary" @click="loadCandidates">查询</el-button></el-form-item
|
><el-button type="primary" @click="loadCandidates">查询</el-button></el-form-item
|
||||||
>
|
>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
ref="candidateTable"
|
ref="candidateTable"
|
||||||
v-loading="candidate.loading"
|
v-loading="candidate.loading"
|
||||||
@@ -990,6 +1007,7 @@ import {
|
|||||||
getNextNo,
|
getNextNo,
|
||||||
getReceiptClaims,
|
getReceiptClaims,
|
||||||
claimInvoices as claimInvoicesApi,
|
claimInvoices as claimInvoicesApi,
|
||||||
|
queryInvoicePool,
|
||||||
save,
|
save,
|
||||||
} from '@/api/settlement/formalSettlement';
|
} from '@/api/settlement/formalSettlement';
|
||||||
import {
|
import {
|
||||||
@@ -997,6 +1015,12 @@ import {
|
|||||||
getDetailFees as getPreSettlementDetailFees,
|
getDetailFees as getPreSettlementDetailFees,
|
||||||
} from '@/api/settlement/preSettlement';
|
} from '@/api/settlement/preSettlement';
|
||||||
import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail';
|
import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail';
|
||||||
|
import {
|
||||||
|
resolveReceivablePayableDetailId,
|
||||||
|
resolveWaybillId,
|
||||||
|
settlementDetailPath,
|
||||||
|
} from '@/utils/settlement-detail-link';
|
||||||
|
import { getBfiExchangeRate } from '@/api/base/currency';
|
||||||
import {
|
import {
|
||||||
createFormalSettlementForm,
|
createFormalSettlementForm,
|
||||||
formalSettlementFormFields,
|
formalSettlementFormFields,
|
||||||
@@ -1013,13 +1037,24 @@ import { h } from 'vue';
|
|||||||
import { InfoFilled } from '@element-plus/icons-vue';
|
import { InfoFilled } from '@element-plus/icons-vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { formatContractOptionLabel } from '@/utils/contract';
|
import { formatContractOptionLabel } from '@/utils/contract';
|
||||||
|
import { normalizeCurrencyCode } from '@/utils/currency';
|
||||||
import { downloadFileByUrl } from '@/utils/util';
|
import { downloadFileByUrl } from '@/utils/util';
|
||||||
|
import {
|
||||||
|
downloadKingdeeInvoiceFile,
|
||||||
|
isKingdeePathAttachment,
|
||||||
|
viewKingdeeInvoiceFile,
|
||||||
|
} from '@/utils/kingdee-attachment';
|
||||||
|
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
|
||||||
|
import ProjectAdvanceWriteOff from './project-advance-write-off.vue';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import { isAdministrator } from '@/utils/auth';
|
import { isAdministrator } from '@/utils/auth';
|
||||||
|
|
||||||
|
// 结算币种为本位币(人民币)时汇率恒为1,汇率日期、结算汇率与按汇率换算的本位币合计都不展示
|
||||||
|
const LOCAL_CURRENCY_HIDDEN_FIELDS = ['exchangeRateDate', 'exchangeRate', 'localSettlementAmount'];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormalSettlementEditor',
|
name: 'FormalSettlementEditor',
|
||||||
components: { InfoFilled },
|
components: { InfoFilled, KingdeeInvoicePoolDialog, ProjectAdvanceWriteOff },
|
||||||
props: {
|
props: {
|
||||||
modelValue: Boolean,
|
modelValue: Boolean,
|
||||||
recordId: [String, Number],
|
recordId: [String, Number],
|
||||||
@@ -1048,18 +1083,20 @@ export default {
|
|||||||
receiptClaims: [],
|
receiptClaims: [],
|
||||||
adjustments: [],
|
adjustments: [],
|
||||||
changeRecords: [],
|
changeRecords: [],
|
||||||
|
projectAdvanceWriteOffs: [],
|
||||||
attachments: [],
|
attachments: [],
|
||||||
selectedAttachmentRows: [],
|
selectedAttachmentRows: [],
|
||||||
invoices: [],
|
invoices: [],
|
||||||
|
// 开票申请经金蝶回写的发票结果(只读展示,含板式文件/数电PDF下载地址)
|
||||||
|
issuedInvoices: [],
|
||||||
invoiceClaimNo: '',
|
invoiceClaimNo: '',
|
||||||
invoiceClaimLoading: false,
|
invoiceClaimLoading: false,
|
||||||
|
// 金蝶侧发票附件查看/下载进行中(行组级 loading 守卫)
|
||||||
|
kingdeeAttachmentBusy: false,
|
||||||
invoiceClaimDialog: {
|
invoiceClaimDialog: {
|
||||||
visible: false,
|
visible: false,
|
||||||
loading: false,
|
|
||||||
confirming: false,
|
|
||||||
rows: [],
|
|
||||||
selected: [],
|
selected: [],
|
||||||
page: { current: 1, size: 10, total: 0 },
|
confirming: false,
|
||||||
},
|
},
|
||||||
attachmentUploadFiles: [],
|
attachmentUploadFiles: [],
|
||||||
attachmentTypeOptions: [],
|
attachmentTypeOptions: [],
|
||||||
@@ -1185,6 +1222,34 @@ export default {
|
|||||||
title() {
|
title() {
|
||||||
return this.readonly ? '查看正式结算单' : this.recordId ? '编辑正式结算单' : '新增正式结算单';
|
return this.readonly ? '查看正式结算单' : this.recordId ? '编辑正式结算单' : '新增正式结算单';
|
||||||
},
|
},
|
||||||
|
detailFeeItemNames() {
|
||||||
|
const names = new Set();
|
||||||
|
this.details.forEach(row => {
|
||||||
|
Object.keys(this.parseFeeItems(row.feeItemsJson || row.feeItems)).forEach(name =>
|
||||||
|
names.add(name)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return Array.from(names);
|
||||||
|
},
|
||||||
|
detailDisplayColumns() {
|
||||||
|
const columns = this.detailTableColumns;
|
||||||
|
const unitPriceIndex = columns.findIndex(column => column.prop === 'unitPrice');
|
||||||
|
const beforeColumns = unitPriceIndex < 0 ? columns : columns.slice(0, unitPriceIndex + 1);
|
||||||
|
const afterColumns = unitPriceIndex < 0 ? [] : columns.slice(unitPriceIndex + 1);
|
||||||
|
return [
|
||||||
|
...beforeColumns.map(column => ({ kind: 'field', key: column.prop, column })),
|
||||||
|
...this.detailFeeItemNames.map(name => ({ kind: 'fee', key: `fee-${name}`, name })),
|
||||||
|
...afterColumns.map(column => ({ kind: 'field', key: column.prop, column })),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
// 币种为空按人民币处理,与后端预结算/正式结算对空币种的默认取值一致
|
||||||
|
isLocalCurrencySettlement() {
|
||||||
|
return normalizeCurrencyCode(this.form.currency) === 'RMB';
|
||||||
|
},
|
||||||
|
visibleFields() {
|
||||||
|
if (!this.isLocalCurrencySettlement) return this.fields;
|
||||||
|
return this.fields.filter(field => !LOCAL_CURRENCY_HIDDEN_FIELDS.includes(field.prop));
|
||||||
|
},
|
||||||
summaryTotal() {
|
summaryTotal() {
|
||||||
return this.summaryFees.reduce((total, row) => total + Number(row.settlementAmount || 0), 0);
|
return this.summaryFees.reduce((total, row) => total + Number(row.settlementAmount || 0), 0);
|
||||||
},
|
},
|
||||||
@@ -1216,11 +1281,6 @@ export default {
|
|||||||
});
|
});
|
||||||
return Array.from(names);
|
return Array.from(names);
|
||||||
},
|
},
|
||||||
invoiceClaimPagedRows() {
|
|
||||||
const { current, size } = this.invoiceClaimDialog.page;
|
|
||||||
const start = (current - 1) * size;
|
|
||||||
return this.invoiceClaimDialog.rows.slice(start, start + size);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -1282,9 +1342,17 @@ export default {
|
|||||||
}
|
}
|
||||||
this.adjustments = data.adjustments || [];
|
this.adjustments = data.adjustments || [];
|
||||||
this.changeRecords = data.changeRecords || [];
|
this.changeRecords = data.changeRecords || [];
|
||||||
|
this.projectAdvanceWriteOffs = (data.projectAdvanceWriteOffs || []).map(row => ({
|
||||||
|
...row,
|
||||||
|
writeOffAmount: Number(row.writeOffAmount || 0),
|
||||||
|
projectAdvanceAmount: Number(row.projectAdvanceAmount || 0),
|
||||||
|
availableWriteOffAmount: Number(row.availableWriteOffAmount || 0),
|
||||||
|
createTime: row.paymentCreateTime || row.createTime,
|
||||||
|
}));
|
||||||
this.attachments = this.parseAttachments(data.attachmentsJson);
|
this.attachments = this.parseAttachments(data.attachmentsJson);
|
||||||
this.selectedAttachmentRows = [];
|
this.selectedAttachmentRows = [];
|
||||||
this.invoices = data.invoices || [];
|
this.invoices = data.invoices || [];
|
||||||
|
this.issuedInvoices = data.issuedInvoices || [];
|
||||||
this.sortAttachments();
|
this.sortAttachments();
|
||||||
this.contracts = this.filterContracts(this.allContracts, this.form.projectId);
|
this.contracts = this.filterContracts(this.allContracts, this.form.projectId);
|
||||||
},
|
},
|
||||||
@@ -1311,12 +1379,13 @@ export default {
|
|||||||
this.receiptClaims = [];
|
this.receiptClaims = [];
|
||||||
this.adjustments = [];
|
this.adjustments = [];
|
||||||
this.changeRecords = [];
|
this.changeRecords = [];
|
||||||
|
this.projectAdvanceWriteOffs = [];
|
||||||
this.attachments = [];
|
this.attachments = [];
|
||||||
this.selectedAttachmentRows = [];
|
this.selectedAttachmentRows = [];
|
||||||
this.invoices = [];
|
this.invoices = [];
|
||||||
|
this.issuedInvoices = [];
|
||||||
this.invoiceClaimNo = '';
|
this.invoiceClaimNo = '';
|
||||||
this.invoiceClaimDialog.visible = false;
|
this.invoiceClaimDialog.visible = false;
|
||||||
this.invoiceClaimDialog.rows = [];
|
|
||||||
this.invoiceClaimDialog.selected = [];
|
this.invoiceClaimDialog.selected = [];
|
||||||
this.attachmentUploadFiles = [];
|
this.attachmentUploadFiles = [];
|
||||||
this.billingRuleDialog = {
|
this.billingRuleDialog = {
|
||||||
@@ -1340,6 +1409,7 @@ export default {
|
|||||||
if (this.initialData) await this.applyInitialData();
|
if (this.initialData) await this.applyInitialData();
|
||||||
Object.assign(this.form, newRecordAudit);
|
Object.assign(this.form, newRecordAudit);
|
||||||
await this.refreshFormalSettlementNo();
|
await this.refreshFormalSettlementNo();
|
||||||
|
await this.fetchBfiExchangeRate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@@ -1574,6 +1644,7 @@ export default {
|
|||||||
(settlementType === 'receivable' ? contract.partyA : contract.partyB),
|
(settlementType === 'receivable' ? contract.partyA : contract.partyB),
|
||||||
settlementType,
|
settlementType,
|
||||||
settlementTypeName: settlementType === 'receivable' ? '应收' : '应付',
|
settlementTypeName: settlementType === 'receivable' ? '应收' : '应付',
|
||||||
|
currency: contract.settlementCurrency || 'RMB',
|
||||||
});
|
});
|
||||||
this.sources = [];
|
this.sources = [];
|
||||||
this.details = [];
|
this.details = [];
|
||||||
@@ -1581,6 +1652,23 @@ export default {
|
|||||||
this.form.sourcePreSettlementIds = [];
|
this.form.sourcePreSettlementIds = [];
|
||||||
this.form.sourceDetailIds = [];
|
this.form.sourceDetailIds = [];
|
||||||
if (refreshSettlementNo) this.refreshFormalSettlementNo();
|
if (refreshSettlementNo) this.refreshFormalSettlementNo();
|
||||||
|
if (this.form.currency !== 'RMB') this.fetchBfiExchangeRate();
|
||||||
|
},
|
||||||
|
async fetchBfiExchangeRate() {
|
||||||
|
const currency = this.form.currency;
|
||||||
|
if (!currency || currency === 'RMB') return;
|
||||||
|
if (!this.form.exchangeRateDate) return;
|
||||||
|
try {
|
||||||
|
const { data } = await getBfiExchangeRate(currency, this.form.exchangeRateDate);
|
||||||
|
const rateData = data?.data;
|
||||||
|
if (rateData?.excval != null) {
|
||||||
|
this.form.exchangeRate = Number(rateData.excval);
|
||||||
|
} else {
|
||||||
|
this.$message.warning(`未查询到 ${currency} 的BFI汇率数据`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.warning('查询BFI汇率失败,请手动输入结算汇率');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
openCandidateDialog() {
|
openCandidateDialog() {
|
||||||
this.candidate.visible = true;
|
this.candidate.visible = true;
|
||||||
@@ -2018,6 +2106,21 @@ export default {
|
|||||||
this.appliedDetailQuery = { ...this.detailQuery };
|
this.appliedDetailQuery = { ...this.detailQuery };
|
||||||
this.detailPage.current = 1;
|
this.detailPage.current = 1;
|
||||||
},
|
},
|
||||||
|
// 结算明细的「单据号」跳到该应收应付单据在应收明细/应付明细页的详情,「运单号」跳到运单详情
|
||||||
|
async handleDetailLink(row, column) {
|
||||||
|
if (column.prop === 'documentNo') {
|
||||||
|
const settlementType = row.settlementType || this.form.settlementType || 'payable';
|
||||||
|
const detailId = await resolveReceivablePayableDetailId(row, settlementType);
|
||||||
|
if (!detailId) return this.$message.info('当前记录未找到对应的应收/应付明细');
|
||||||
|
this.$router.push({ path: settlementDetailPath(settlementType), query: { detailId } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (column.prop === 'waybillNo') {
|
||||||
|
const waybillId = await resolveWaybillId(row);
|
||||||
|
if (!waybillId) return this.$message.info('当前记录未找到对应的运单详情');
|
||||||
|
this.$router.push({ path: '/business/waybill-manage/detail', query: { id: waybillId } });
|
||||||
|
}
|
||||||
|
},
|
||||||
resetDetailQuery(apply = true) {
|
resetDetailQuery(apply = true) {
|
||||||
this.detailQuery = { documentNo: '', waybillNo: '', batchNo: '', cargoName: '' };
|
this.detailQuery = { documentNo: '', waybillNo: '', batchNo: '', cargoName: '' };
|
||||||
if (apply) this.handleDetailQuery();
|
if (apply) this.handleDetailQuery();
|
||||||
@@ -2305,79 +2408,19 @@ export default {
|
|||||||
this.adjust.saving = false;
|
this.adjust.saving = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
claimInvoices() {
|
|
||||||
this.invoiceClaimLoading = true;
|
|
||||||
try {
|
|
||||||
const keyword = String(this.invoiceClaimNo || '').trim();
|
|
||||||
const defaultNo = `MOCK${this.$dayjs().format('YYYYMMDD')}`;
|
|
||||||
const baseNo = (keyword || defaultNo).slice(0, 29);
|
|
||||||
const numbers = [baseNo, `${baseNo}-02`, `${baseNo}-03`];
|
|
||||||
const invoiceAmounts = [1000, 2000, 3000];
|
|
||||||
const availableAmounts = [1000, 1000, 3000];
|
|
||||||
const preferredMatchedAmounts = [1000, 1000, 2000];
|
|
||||||
let remainingAmount = Math.max(
|
|
||||||
Number(this.form.settlementAmount || this.summaryTotal || 0) -
|
|
||||||
this.invoices.reduce((total, item) => total + Number(item.matchedAmount || 0), 0),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const existingNumbers = new Set(this.invoices.map(item => String(item.invoiceNo)));
|
|
||||||
const mockRows = numbers
|
|
||||||
.map((invoiceNo, index) => {
|
|
||||||
const matchedAmount = Math.min(
|
|
||||||
preferredMatchedAmounts[index],
|
|
||||||
availableAmounts[index],
|
|
||||||
remainingAmount
|
|
||||||
);
|
|
||||||
remainingAmount = Number((remainingAmount - matchedAmount).toFixed(2));
|
|
||||||
return {
|
|
||||||
invoiceNo,
|
|
||||||
invoiceDate: this.$dayjs().subtract(index, 'day').format('YYYY-MM-DD'),
|
|
||||||
invoiceType: index === 1 ? '增值税普通发票' : '增值税专用发票',
|
|
||||||
taxRate: [3, 6, 9][index],
|
|
||||||
invoiceAmount: invoiceAmounts[index],
|
|
||||||
availableInvoiceAmount: availableAmounts[index],
|
|
||||||
matchedAmount,
|
|
||||||
attachmentJson: '',
|
|
||||||
mockData: true,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter(item => !existingNumbers.has(item.invoiceNo));
|
|
||||||
if (!mockRows.length) {
|
|
||||||
this.$message.warning('当前发票号码的Mock数据已认领');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.invoices = [...this.invoices, ...mockRows];
|
|
||||||
this.$message.success(`已生成${mockRows.length}条Mock发票数据`);
|
|
||||||
} finally {
|
|
||||||
this.invoiceClaimLoading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openInvoiceClaimDialog() {
|
openInvoiceClaimDialog() {
|
||||||
const stamp = Date.now();
|
|
||||||
this.invoiceClaimDialog.loading = true;
|
|
||||||
this.invoiceClaimDialog.rows = Array.from({ length: 80 }, (_, index) => {
|
|
||||||
const amount = Math.floor(1000 + Math.random() * 49000);
|
|
||||||
return {
|
|
||||||
invoiceNo: `MOCK${stamp}${String(index + 1).padStart(3, '0')}`.slice(0, 32),
|
|
||||||
invoiceDate: this.$dayjs()
|
|
||||||
.subtract(Math.floor(Math.random() * 90), 'day')
|
|
||||||
.format('YYYY-MM-DD'),
|
|
||||||
invoiceType: index % 2 ? '增值税普通发票' : '增值税专用发票',
|
|
||||||
issuerName: this.form.payerName || 'Mock开票单位',
|
|
||||||
receiverName: this.form.payeeName || 'Mock受票单位',
|
|
||||||
invoiceAmount: amount,
|
|
||||||
availableInvoiceAmount: amount,
|
|
||||||
matchedAmount: amount,
|
|
||||||
taxRate: [3, 6, 9, 13][index % 4],
|
|
||||||
attachmentJson: '',
|
|
||||||
mockData: true,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
this.invoiceClaimDialog.selected = [];
|
|
||||||
this.invoiceClaimDialog.page.current = 1;
|
|
||||||
this.invoiceClaimDialog.page.total = 80;
|
|
||||||
this.invoiceClaimDialog.visible = true;
|
this.invoiceClaimDialog.visible = true;
|
||||||
this.invoiceClaimDialog.loading = false;
|
},
|
||||||
|
invoicePoolQueryApi(params) {
|
||||||
|
return queryInvoicePool(params);
|
||||||
|
},
|
||||||
|
async handleInvoiceClaimConfirm(rows) {
|
||||||
|
// 统一票池行附件字段为复数 attachmentsJson,认领请求契约是单数 attachmentJson,中转处归一
|
||||||
|
this.invoiceClaimDialog.selected = rows.map(row => ({
|
||||||
|
...row,
|
||||||
|
attachmentJson: row.attachmentJson || row.attachmentsJson,
|
||||||
|
}));
|
||||||
|
await this.confirmInvoiceClaims();
|
||||||
},
|
},
|
||||||
async confirmInvoiceClaims() {
|
async confirmInvoiceClaims() {
|
||||||
if (!this.invoiceClaimDialog.selected.length) {
|
if (!this.invoiceClaimDialog.selected.length) {
|
||||||
@@ -2403,7 +2446,11 @@ export default {
|
|||||||
if (Number(this.form.settlementAmount || 0) > 0) {
|
if (Number(this.form.settlementAmount || 0) > 0) {
|
||||||
remaining = Math.max(0, remaining - matchedAmount);
|
remaining = Math.max(0, remaining - matchedAmount);
|
||||||
}
|
}
|
||||||
return { ...row, matchedAmount };
|
return {
|
||||||
|
...row,
|
||||||
|
invoiceType: row.invoiceTypeName || row.invoiceType || '',
|
||||||
|
matchedAmount,
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.filter(row => row.matchedAmount > 0);
|
.filter(row => row.matchedAmount > 0);
|
||||||
if (!invoices.length) {
|
if (!invoices.length) {
|
||||||
@@ -2489,16 +2536,64 @@ export default {
|
|||||||
const attachment = this.invoiceAttachment(row);
|
const attachment = this.invoiceAttachment(row);
|
||||||
return attachment.originalName || attachment.name || `${row.invoiceNo || '发票'}.pdf`;
|
return attachment.originalName || attachment.name || `${row.invoiceNo || '发票'}.pdf`;
|
||||||
},
|
},
|
||||||
viewInvoiceAttachment(row) {
|
async viewInvoiceAttachment(row) {
|
||||||
|
const attachment = this.invoiceAttachment(row);
|
||||||
|
// 金蝶侧附件(path 型)经后端代理 A801-2 预览,OSS 直链维持 window.open
|
||||||
|
if (isKingdeePathAttachment(attachment)) {
|
||||||
|
if (this.kingdeeAttachmentBusy) return;
|
||||||
|
this.kingdeeAttachmentBusy = true;
|
||||||
|
try {
|
||||||
|
await viewKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row));
|
||||||
|
} finally {
|
||||||
|
this.kingdeeAttachmentBusy = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
const url = this.invoiceAttachmentUrl(row);
|
const url = this.invoiceAttachmentUrl(row);
|
||||||
if (!url) return this.$message.warning('当前Mock发票暂无可查看附件');
|
if (!url) return this.$message.warning('当前发票暂无可查看附件');
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
},
|
},
|
||||||
downloadInvoiceAttachment(row) {
|
async downloadInvoiceAttachment(row) {
|
||||||
|
const attachment = this.invoiceAttachment(row);
|
||||||
|
// 金蝶侧附件(path 型)经后端代理 A801-2 下载,OSS 直链维持前端直连下载
|
||||||
|
if (isKingdeePathAttachment(attachment)) {
|
||||||
|
if (this.kingdeeAttachmentBusy) return;
|
||||||
|
this.kingdeeAttachmentBusy = true;
|
||||||
|
try {
|
||||||
|
await downloadKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row));
|
||||||
|
} finally {
|
||||||
|
this.kingdeeAttachmentBusy = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
const url = this.invoiceAttachmentUrl(row);
|
const url = this.invoiceAttachmentUrl(row);
|
||||||
if (!url) return this.$message.warning('当前Mock发票暂无可下载附件');
|
if (!url) return this.$message.warning('当前发票暂无可下载附件');
|
||||||
downloadFileByUrl(url, this.invoiceAttachmentName(row));
|
downloadFileByUrl(url, this.invoiceAttachmentName(row));
|
||||||
},
|
},
|
||||||
|
// 开票发票预览地址:数电PDF优先(OFD浏览器无法预览),退预览图、板式文件
|
||||||
|
issuedInvoicePreviewUrl(row) {
|
||||||
|
return row.invoicePdfFileUrl || row.invoiceImageUrl || row.invoiceFileUrl || '';
|
||||||
|
},
|
||||||
|
// 开票发票下载地址:数电PDF优先,退板式文件、数电XML
|
||||||
|
issuedInvoiceDownloadUrl(row) {
|
||||||
|
return row.invoicePdfFileUrl || row.invoiceFileUrl || row.invoiceXmlFileUrl || '';
|
||||||
|
},
|
||||||
|
// 下载文件名按直链扩展名推断(数电票 .pdf / 板式文件 .ofd|.pdf),缺省 .pdf
|
||||||
|
issuedInvoiceFileName(row, url) {
|
||||||
|
const match = String(url || '').split('?')[0].match(/\.([a-zA-Z0-9]+)$/);
|
||||||
|
const extension = match ? `.${match[1].toLowerCase()}` : '.pdf';
|
||||||
|
return `${row.invoiceNo || '发票'}${extension}`;
|
||||||
|
},
|
||||||
|
viewIssuedInvoice(row) {
|
||||||
|
const url = this.issuedInvoicePreviewUrl(row);
|
||||||
|
if (!url) return this.$message.warning('当前发票暂无可查看文件,可稍后重试开票结果同步');
|
||||||
|
window.open(url, '_blank');
|
||||||
|
},
|
||||||
|
downloadIssuedInvoice(row) {
|
||||||
|
const url = this.issuedInvoiceDownloadUrl(row);
|
||||||
|
if (!url) return this.$message.warning('当前发票暂无可下载文件,可稍后重试开票结果同步');
|
||||||
|
downloadFileByUrl(url, this.issuedInvoiceFileName(row, url));
|
||||||
|
},
|
||||||
buildSavePayload() {
|
buildSavePayload() {
|
||||||
return {
|
return {
|
||||||
id: this.form.id,
|
id: this.form.id,
|
||||||
@@ -2544,10 +2639,18 @@ export default {
|
|||||||
matchedAmount: Number(row.matchedAmount || 0),
|
matchedAmount: Number(row.matchedAmount || 0),
|
||||||
attachmentJson: row.attachmentJson || '',
|
attachmentJson: row.attachmentJson || '',
|
||||||
})),
|
})),
|
||||||
|
projectAdvanceWriteOffs: this.$refs.projectAdvanceWriteOffRef?.toSavePayload?.() || [],
|
||||||
remark: this.form.remark,
|
remark: this.form.remark,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
handleSyncPlaceholder() {
|
||||||
|
this.$message.info('开发中');
|
||||||
|
},
|
||||||
async handleDraftSave() {
|
async handleDraftSave() {
|
||||||
|
// 保存草稿:不做表单校验,直接落草稿并返回列表刷新
|
||||||
|
if (this.$refs.projectAdvanceWriteOffRef?.validateBeforeSave?.() === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.draftSaving = true;
|
this.draftSaving = true;
|
||||||
try {
|
try {
|
||||||
await save({
|
await save({
|
||||||
@@ -2555,8 +2658,8 @@ export default {
|
|||||||
approvalStatus: 'draft',
|
approvalStatus: 'draft',
|
||||||
});
|
});
|
||||||
this.$message.success('保存成功');
|
this.$message.success('保存成功');
|
||||||
this.visible = false;
|
|
||||||
this.$emit('success');
|
this.$emit('success');
|
||||||
|
this.visible = false;
|
||||||
} finally {
|
} finally {
|
||||||
this.draftSaving = false;
|
this.draftSaving = false;
|
||||||
}
|
}
|
||||||
@@ -2575,6 +2678,9 @@ export default {
|
|||||||
return this.$message.warning('请完善手工费用的费用类型和费用项');
|
return this.$message.warning('请完善手工费用的费用类型和费用项');
|
||||||
}
|
}
|
||||||
if (!this.validateInvoices()) return;
|
if (!this.validateInvoices()) return;
|
||||||
|
if (this.$refs.projectAdvanceWriteOffRef?.validateBeforeSave?.() === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 附件类型仅做提示,不阻断正式结算单新增/保存提交。
|
// 附件类型仅做提示,不阻断正式结算单新增/保存提交。
|
||||||
// this.validateAttachments();
|
// this.validateAttachments();
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
@@ -2855,6 +2961,9 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.form-readonly--code {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
.formal-editor--page {
|
.formal-editor--page {
|
||||||
padding-bottom: 72px;
|
padding-bottom: 72px;
|
||||||
}
|
}
|
||||||
@@ -2984,6 +3093,14 @@ export default {
|
|||||||
.formal-editor__invoice-claim :deep(.el-input) {
|
.formal-editor__invoice-claim :deep(.el-input) {
|
||||||
width: 360px;
|
width: 360px;
|
||||||
}
|
}
|
||||||
|
.formal-editor__issued-invoices {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.formal-editor__issued-title {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
.formal-editor__negative-amount {
|
.formal-editor__negative-amount {
|
||||||
color: var(--el-color-danger);
|
color: var(--el-color-danger);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
plain
|
plain
|
||||||
:disabled="selection.length !== 1"
|
:disabled="selection.length !== 1"
|
||||||
|
:loading="syncLoading"
|
||||||
@click="$emit('sync')"
|
@click="$emit('sync')"
|
||||||
>同步金蝶</el-button
|
>同步金蝶</el-button
|
||||||
>
|
>
|
||||||
@@ -91,6 +92,12 @@
|
|||||||
<span v-else-if="column.prop === 'kingdeeSyncStatus'">{{
|
<span v-else-if="column.prop === 'kingdeeSyncStatus'">{{
|
||||||
kingdeeSyncName(row.kingdeeSyncStatus)
|
kingdeeSyncName(row.kingdeeSyncStatus)
|
||||||
}}</span>
|
}}</span>
|
||||||
|
<template v-else-if="column.prop === 'kingdeeLockStatus'">
|
||||||
|
<el-tooltip v-if="lockTag(row).show" :content="lockTag(row).tip" placement="top">
|
||||||
|
<el-tag :type="lockTag(row).type" class="status-text">{{ lockTag(row).label }}</el-tag>
|
||||||
|
</el-tooltip>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -137,6 +144,7 @@ export default {
|
|||||||
rows: { type: Array, default: () => [] },
|
rows: { type: Array, default: () => [] },
|
||||||
columns: { type: Array, default: () => [] },
|
columns: { type: Array, default: () => [] },
|
||||||
loading: { type: Boolean, default: false },
|
loading: { type: Boolean, default: false },
|
||||||
|
syncLoading: { type: Boolean, default: false },
|
||||||
page: { type: Object, required: true },
|
page: { type: Object, required: true },
|
||||||
hasPermission: { type: Function, required: true },
|
hasPermission: { type: Function, required: true },
|
||||||
mkEnabled: { type: Boolean, default: false },
|
mkEnabled: { type: Boolean, default: false },
|
||||||
@@ -267,6 +275,38 @@ export default {
|
|||||||
kingdeeSyncName(value) {
|
kingdeeSyncName(value) {
|
||||||
return kingdeeSyncStatusOptions.find(item => item.value === value)?.label || value || '-';
|
return kingdeeSyncStatusOptions.find(item => item.value === value)?.label || value || '-';
|
||||||
},
|
},
|
||||||
|
lockTag(row) {
|
||||||
|
if (row.kingdeeLockStatus === 'locked') {
|
||||||
|
return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' };
|
||||||
|
}
|
||||||
|
if (row.kingdeeLockStatus === 'failed') {
|
||||||
|
if ((row.kingdeeTryCount || 0) >= 5) {
|
||||||
|
return {
|
||||||
|
show: true,
|
||||||
|
type: 'danger',
|
||||||
|
label: '锁定失败·已达上限',
|
||||||
|
tip: row.kingdeeLockFailReason || '自动补偿次数已用完,请重推「同步金蝶」立即补锁',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
show: true,
|
||||||
|
type: 'warning',
|
||||||
|
label: '锁定失败·补偿中',
|
||||||
|
tip: row.kingdeeLockFailReason || '系统将自动补偿重锁',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 金蝶解锁终态标识:删单解锁的源单已作废,此标签用于区分人工作废与金蝶删单
|
||||||
|
if (row.kingdeeUnlockTime) {
|
||||||
|
const isDelete = row.kingdeeUnlockOperationName === '金蝶删单';
|
||||||
|
return {
|
||||||
|
show: true,
|
||||||
|
type: isDelete ? 'danger' : 'warning',
|
||||||
|
label: row.kingdeeUnlockOperationName || '金蝶解锁',
|
||||||
|
tip: `${row.kingdeeUnlockOperator || '-'} 于 ${this.$dayjs(row.kingdeeUnlockTime).format('YYYY-MM-DD HH:mm:ss')} 解锁`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { show: false };
|
||||||
|
},
|
||||||
displayValue(value) {
|
displayValue(value) {
|
||||||
return value === null || value === undefined || value === '' ? '-' : value;
|
return value === null || value === undefined || value === '' ? '-' : value;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
class="pre-settlement-editor__form"
|
class="pre-settlement-editor__form"
|
||||||
>
|
>
|
||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col v-for="field in formFields" :key="field.prop" :span="field.span || 6">
|
<el-col v-for="field in visibleFormFields" :key="field.prop" :span="field.span || 6">
|
||||||
<el-form-item :label="field.label" :prop="field.prop">
|
<el-form-item :label="field.label" :prop="field.prop">
|
||||||
<el-input
|
<el-input
|
||||||
v-if="field.type === 'contract'"
|
v-if="field.type === 'contract'"
|
||||||
@@ -40,9 +40,9 @@
|
|||||||
type="date"
|
type="date"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
:disabled="!editable || form.currency === 'RMB'"
|
:disabled="!editable"
|
||||||
placeholder="请选择"
|
placeholder="请选择"
|
||||||
@change="recalculateLocalAmount"
|
@change="handleExchangeRateDateChange"
|
||||||
/>
|
/>
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-else-if="field.type === 'number'"
|
v-else-if="field.type === 'number'"
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
:min="0"
|
:min="0"
|
||||||
:precision="2"
|
:precision="2"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
:disabled="!editable || form.currency === 'RMB'"
|
:disabled="!editable"
|
||||||
@change="recalculateLocalAmount"
|
@change="recalculateLocalAmount"
|
||||||
/>
|
/>
|
||||||
<el-input
|
<el-input
|
||||||
@@ -66,7 +66,11 @@
|
|||||||
<span v-else-if="field.prop === 'settlementType'" class="form-readonly">
|
<span v-else-if="field.prop === 'settlementType'" class="form-readonly">
|
||||||
{{ settlementTypeName }}
|
{{ settlementTypeName }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="field.prop === 'preSettlementNo'" class="form-readonly">
|
<span
|
||||||
|
v-else-if="field.prop === 'preSettlementNo'"
|
||||||
|
class="form-readonly"
|
||||||
|
:class="{ 'form-readonly--code': form.preSettlementNo }"
|
||||||
|
>
|
||||||
{{ form.preSettlementNo || (!form.id ? '系统自动生成' : '-') }}
|
{{ form.preSettlementNo || (!form.id ? '系统自动生成' : '-') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="field.prop === 'createTime'" class="form-readonly">
|
<span v-else-if="field.prop === 'createTime'" class="form-readonly">
|
||||||
@@ -243,60 +247,52 @@
|
|||||||
:index="detailIndexMethod"
|
:index="detailIndexMethod"
|
||||||
/>
|
/>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
v-for="column in visibleDetailColumns"
|
v-for="item in detailDisplayColumns"
|
||||||
:key="column.prop"
|
:key="item.key"
|
||||||
:label="column.label"
|
:label="item.kind === 'fee' ? item.name : item.column.label"
|
||||||
:prop="column.prop"
|
:prop="item.kind === 'fee' ? undefined : item.column.prop"
|
||||||
:min-width="column.minWidth"
|
:min-width="item.kind === 'fee' ? 130 : item.column.minWidth"
|
||||||
align="center"
|
align="center"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
>
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<span v-if="item.kind === 'fee'">
|
||||||
|
{{
|
||||||
|
formatMoney(
|
||||||
|
parseFeeItems(row.feeItemsJson)[item.name],
|
||||||
|
row.currency || form.currency
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
<el-link
|
<el-link
|
||||||
v-if="column.link && row[column.prop]"
|
v-else-if="item.column.link && row[item.column.prop] && !publicMode"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="openAdjustDialog(row, true)"
|
@click.stop="handleDetailLink(row, item.column)"
|
||||||
>
|
>
|
||||||
{{ row[column.prop] }}
|
{{ row[item.column.prop] }}
|
||||||
</el-link>
|
</el-link>
|
||||||
<span v-else-if="column.money && column.prop === 'unitPrice'">
|
<span v-else-if="item.column.money && item.column.prop === 'unitPrice'">
|
||||||
{{ formatUnitPrice(row) }}
|
{{ formatUnitPrice(row) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="column.money">
|
<span v-else-if="item.column.money">
|
||||||
{{ formatMoney(row[column.prop], row.currency || form.currency) }}
|
{{ formatMoney(row[item.column.prop], row.currency || form.currency) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="column.prop === 'transportQuantityText'">
|
<span v-else-if="item.column.prop === 'transportQuantityText'">
|
||||||
{{ formatQuantity(row) }}
|
{{ formatQuantity(row) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="pageMode && column.prop === 'mileage'">
|
<span v-else-if="item.column.prop === 'mileage'">
|
||||||
{{ formatDetailMileage(row[column.prop]) }}
|
{{ formatDetailMileage(row[item.column.prop]) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="column.prop === 'transportType'">
|
<span v-else-if="item.column.prop === 'transportType'">
|
||||||
{{ transportTypeName(row[column.prop]) }}
|
{{ transportTypeName(row[item.column.prop]) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="column.prop === 'cargoName'">
|
<span v-else-if="item.column.prop === 'cargoName'">
|
||||||
{{ formatCargoNames(row) }}
|
{{ formatCargoNames(row) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="column.prop === 'cargoType'">
|
<span v-else-if="item.column.prop === 'cargoType'">
|
||||||
{{ formatCargoTypes(row) }}
|
{{ formatCargoTypes(row) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
<span v-else>{{ displayValue(row[item.column.prop]) }}</span>
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
|
||||||
v-for="feeItem in detailFeeItemNames"
|
|
||||||
:key="`fee-${feeItem}`"
|
|
||||||
:label="feeItem"
|
|
||||||
min-width="130"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{
|
|
||||||
formatMoney(
|
|
||||||
parseFeeItems(row.feeItemsJson)[feeItem],
|
|
||||||
row.currency || form.currency
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" :width="editable ? 150 : 90" fixed="right" align="center">
|
<el-table-column label="操作" :width="editable ? 150 : 90" fixed="right" align="center">
|
||||||
@@ -421,6 +417,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</section-card>
|
</section-card>
|
||||||
|
|
||||||
|
<project-advance-write-off
|
||||||
|
ref="projectAdvanceWriteOffRef"
|
||||||
|
v-model="projectAdvanceWriteOffs"
|
||||||
|
:editable="editable"
|
||||||
|
:contract-id="form.contractId"
|
||||||
|
:settlement-amount="form.settlementAmount"
|
||||||
|
settlement-type="pre"
|
||||||
|
:settlement-id="form.id"
|
||||||
|
/>
|
||||||
|
|
||||||
<section-card v-if="form.id && readonly" title="预付信息">
|
<section-card v-if="form.id && readonly" title="预付信息">
|
||||||
<el-table :data="advances" border>
|
<el-table :data="advances" border>
|
||||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||||
@@ -776,12 +782,13 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="里程(KM)" min-width="140" align="center">
|
<el-table-column label="里程(KM)" min-width="140" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<span v-if="adjustDialog.readonly">{{ formatDetailMileage(row.mileage) }}</span>
|
||||||
<el-input-number
|
<el-input-number
|
||||||
|
v-else
|
||||||
v-model="row.mileage"
|
v-model="row.mileage"
|
||||||
:min="0"
|
:min="0"
|
||||||
:precision="2"
|
:precision="2"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
:disabled="adjustDialog.readonly"
|
|
||||||
@change="recalculateAdjustRow(row, 'mileage')"
|
@change="recalculateAdjustRow(row, 'mileage')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -939,19 +946,28 @@ import { h } from 'vue';
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { InfoFilled } from '@element-plus/icons-vue';
|
import { InfoFilled } from '@element-plus/icons-vue';
|
||||||
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
|
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
|
||||||
|
import ProjectAdvanceWriteOff from './project-advance-write-off.vue';
|
||||||
import { getMkPublicDetail } from '@/api/mk-process';
|
import { getMkPublicDetail } from '@/api/mk-process';
|
||||||
import {
|
import {
|
||||||
adjustDetail,
|
adjustDetail,
|
||||||
getCandidateDetails,
|
getCandidateDetails,
|
||||||
getContractList,
|
getContractList,
|
||||||
|
getContractOptions,
|
||||||
getDetail,
|
getDetail,
|
||||||
getDetailFees,
|
getDetailFees,
|
||||||
getFeeOptions,
|
getFeeOptions,
|
||||||
|
getNextNo,
|
||||||
removeDetail,
|
removeDetail,
|
||||||
save,
|
save,
|
||||||
submit,
|
submit,
|
||||||
} from '@/api/settlement/preSettlement';
|
} from '@/api/settlement/preSettlement';
|
||||||
import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail';
|
import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail';
|
||||||
|
import {
|
||||||
|
resolveReceivablePayableDetailId,
|
||||||
|
resolveWaybillId,
|
||||||
|
settlementDetailPath,
|
||||||
|
} from '@/utils/settlement-detail-link';
|
||||||
|
import { getBfiExchangeRate } from '@/api/base/currency';
|
||||||
import { getDictionary } from '@/api/system/dictbiz';
|
import { getDictionary } from '@/api/system/dictbiz';
|
||||||
import {
|
import {
|
||||||
emptyPreSettlementForm,
|
emptyPreSettlementForm,
|
||||||
@@ -973,14 +989,18 @@ import {
|
|||||||
settlementSummaryColumns,
|
settlementSummaryColumns,
|
||||||
} from '@/option/settlement/preSettlementTable';
|
} from '@/option/settlement/preSettlementTable';
|
||||||
import { formatContractOptionLabel } from '@/utils/contract';
|
import { formatContractOptionLabel } from '@/utils/contract';
|
||||||
|
import { normalizeCurrencyCode } from '@/utils/currency';
|
||||||
import { downloadFileByUrl } from '@/utils/util';
|
import { downloadFileByUrl } from '@/utils/util';
|
||||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import { isAdministrator } from '@/utils/auth';
|
import { isAdministrator } from '@/utils/auth';
|
||||||
|
|
||||||
|
// 结算币种为本位币(人民币)时汇率恒为1,汇率日期、结算汇率与按汇率换算的本位币合计都不展示
|
||||||
|
const LOCAL_CURRENCY_HIDDEN_FIELDS = ['exchangeRateDate', 'exchangeRate', 'localSettlementAmount'];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PreSettlementEditor',
|
name: 'PreSettlementEditor',
|
||||||
components: { InfoFilled, ChangeRecordDetailDialog },
|
components: { InfoFilled, ChangeRecordDetailDialog, ProjectAdvanceWriteOff },
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -1083,6 +1103,7 @@ export default {
|
|||||||
size: 10,
|
size: 10,
|
||||||
},
|
},
|
||||||
advances: [],
|
advances: [],
|
||||||
|
projectAdvanceWriteOffs: [],
|
||||||
changeRecords: [],
|
changeRecords: [],
|
||||||
attachments: [],
|
attachments: [],
|
||||||
attachmentTypeOptions: [],
|
attachmentTypeOptions: [],
|
||||||
@@ -1187,6 +1208,14 @@ export default {
|
|||||||
settlementTypeName() {
|
settlementTypeName() {
|
||||||
return this.form.settlementType === 'receivable' ? '应收' : '应付';
|
return this.form.settlementType === 'receivable' ? '应收' : '应付';
|
||||||
},
|
},
|
||||||
|
// 币种为空按人民币处理,与后端 normalizeRate 的取默认值一致
|
||||||
|
isLocalCurrencySettlement() {
|
||||||
|
return normalizeCurrencyCode(this.form.currency) === 'RMB';
|
||||||
|
},
|
||||||
|
visibleFormFields() {
|
||||||
|
if (!this.isLocalCurrencySettlement) return this.formFields;
|
||||||
|
return this.formFields.filter(field => !LOCAL_CURRENCY_HIDDEN_FIELDS.includes(field.prop));
|
||||||
|
},
|
||||||
contractDialogCategory() {
|
contractDialogCategory() {
|
||||||
return '承运商合同';
|
return '承运商合同';
|
||||||
},
|
},
|
||||||
@@ -1208,6 +1237,17 @@ export default {
|
|||||||
});
|
});
|
||||||
return Array.from(names);
|
return Array.from(names);
|
||||||
},
|
},
|
||||||
|
detailDisplayColumns() {
|
||||||
|
const columns = this.visibleDetailColumns;
|
||||||
|
const unitPriceIndex = columns.findIndex(column => column.prop === 'unitPrice');
|
||||||
|
const beforeColumns = unitPriceIndex < 0 ? columns : columns.slice(0, unitPriceIndex + 1);
|
||||||
|
const afterColumns = unitPriceIndex < 0 ? [] : columns.slice(unitPriceIndex + 1);
|
||||||
|
return [
|
||||||
|
...beforeColumns.map(column => ({ kind: 'field', key: column.prop, column })),
|
||||||
|
...this.detailFeeItemNames.map(name => ({ kind: 'fee', key: `fee-${name}`, name })),
|
||||||
|
...afterColumns.map(column => ({ kind: 'field', key: column.prop, column })),
|
||||||
|
];
|
||||||
|
},
|
||||||
// 按 preSettlementDetailId 分组的明细数据(用于拼接货物名称和类型)
|
// 按 preSettlementDetailId 分组的明细数据(用于拼接货物名称和类型)
|
||||||
groupedDetails() {
|
groupedDetails() {
|
||||||
const groups = {};
|
const groups = {};
|
||||||
@@ -1282,6 +1322,11 @@ export default {
|
|||||||
},
|
},
|
||||||
async initialize() {
|
async initialize() {
|
||||||
this.resetEditor();
|
this.resetEditor();
|
||||||
|
// 新增页在保存前没有创建人/创建日期,先按当前登录用户与当前时间展示将要落库的值
|
||||||
|
if (!this.recordId) {
|
||||||
|
this.form.createUserName = this.userInfo?.realName || this.userInfo?.userName || '';
|
||||||
|
this.form.createTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
}
|
||||||
if (!this.publicMode) {
|
if (!this.publicMode) {
|
||||||
await this.loadFeeOptions();
|
await this.loadFeeOptions();
|
||||||
await this.loadFeeCategoryOptions();
|
await this.loadFeeCategoryOptions();
|
||||||
@@ -1289,7 +1334,19 @@ export default {
|
|||||||
await this.loadAttachmentTypeOptions();
|
await this.loadAttachmentTypeOptions();
|
||||||
}
|
}
|
||||||
if (this.recordId) await this.loadDetail();
|
if (this.recordId) await this.loadDetail();
|
||||||
else if (this.initialData) await this.applyInitialData();
|
else if (this.initialData) {
|
||||||
|
await this.applyInitialData();
|
||||||
|
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
|
||||||
|
await this.refreshPreSettlementNo();
|
||||||
|
await this.fetchBfiExchangeRate();
|
||||||
|
} else if (!this.publicMode) {
|
||||||
|
await this.refreshPreSettlementNo();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async refreshPreSettlementNo() {
|
||||||
|
if (this.form.id) return;
|
||||||
|
const response = await getNextNo();
|
||||||
|
this.form.preSettlementNo = response.data?.data || '';
|
||||||
},
|
},
|
||||||
async applyInitialData() {
|
async applyInitialData() {
|
||||||
const rows = Array.isArray(this.initialData?.rows) ? this.initialData.rows : [];
|
const rows = Array.isArray(this.initialData?.rows) ? this.initialData.rows : [];
|
||||||
@@ -1297,6 +1354,19 @@ export default {
|
|||||||
const first = rows[0];
|
const first = rows[0];
|
||||||
const settlementType = this.initialData.settlementType || first.settlementType || 'payable';
|
const settlementType = this.initialData.settlementType || first.settlementType || 'payable';
|
||||||
const contractId = first.contractId || '';
|
const contractId = first.contractId || '';
|
||||||
|
let contractPartyA = first.payerName || '';
|
||||||
|
let contractPartyB = first.payeeName || '';
|
||||||
|
if (contractId && (!contractPartyA || !contractPartyB)) {
|
||||||
|
try {
|
||||||
|
const { data } = await getContractOptions(first.contractNo || '');
|
||||||
|
const matched = (data?.data || []).find(item => String(item.id) === String(contractId));
|
||||||
|
contractPartyA = contractPartyA || matched?.partyA || '';
|
||||||
|
contractPartyB = contractPartyB || matched?.partyB || '';
|
||||||
|
} catch (error) {
|
||||||
|
contractPartyA = '';
|
||||||
|
contractPartyB = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!this.contractOptions.some(item => String(item.id) === String(contractId))) {
|
if (!this.contractOptions.some(item => String(item.id) === String(contractId))) {
|
||||||
this.contractOptions.push({
|
this.contractOptions.push({
|
||||||
id: contractId,
|
id: contractId,
|
||||||
@@ -1307,8 +1377,8 @@ export default {
|
|||||||
deptId: first.deptId,
|
deptId: first.deptId,
|
||||||
deptName: first.deptName,
|
deptName: first.deptName,
|
||||||
settlementType,
|
settlementType,
|
||||||
partyA: first.payerName,
|
partyA: contractPartyA,
|
||||||
partyB: first.payeeName,
|
partyB: contractPartyB,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.handleContractChange(contractId);
|
this.handleContractChange(contractId);
|
||||||
@@ -1320,8 +1390,8 @@ export default {
|
|||||||
projectName: first.projectName || this.form.projectName,
|
projectName: first.projectName || this.form.projectName,
|
||||||
deptId: first.deptId || this.form.deptId,
|
deptId: first.deptId || this.form.deptId,
|
||||||
deptName: first.deptName || this.form.deptName,
|
deptName: first.deptName || this.form.deptName,
|
||||||
payerName: first.payerName || this.form.payerName,
|
payerName: this.form.payerName || contractPartyA,
|
||||||
payeeName: first.payeeName || this.form.payeeName,
|
payeeName: this.form.payeeName || contractPartyB,
|
||||||
settlementType,
|
settlementType,
|
||||||
currency: first.currency || 'RMB',
|
currency: first.currency || 'RMB',
|
||||||
localCurrency: first.localCurrency || 'RMB',
|
localCurrency: first.localCurrency || 'RMB',
|
||||||
@@ -1391,6 +1461,7 @@ export default {
|
|||||||
this.appliedDetailQuery = { ...this.detailQuery };
|
this.appliedDetailQuery = { ...this.detailQuery };
|
||||||
this.detailPage = { current: 1, size: 10 };
|
this.detailPage = { current: 1, size: 10 };
|
||||||
this.advances = [];
|
this.advances = [];
|
||||||
|
this.projectAdvanceWriteOffs = [];
|
||||||
this.changeRecords = [];
|
this.changeRecords = [];
|
||||||
this.attachments = [];
|
this.attachments = [];
|
||||||
this.selectedAttachmentRows = [];
|
this.selectedAttachmentRows = [];
|
||||||
@@ -1430,6 +1501,13 @@ export default {
|
|||||||
createUserName: row.createUserName || detail.createUserName,
|
createUserName: row.createUserName || detail.createUserName,
|
||||||
createTime: row.createTime || detail.createTime,
|
createTime: row.createTime || detail.createTime,
|
||||||
}));
|
}));
|
||||||
|
this.projectAdvanceWriteOffs = (detail.projectAdvanceWriteOffs || []).map(row => ({
|
||||||
|
...row,
|
||||||
|
writeOffAmount: Number(row.writeOffAmount || 0),
|
||||||
|
projectAdvanceAmount: Number(row.projectAdvanceAmount || 0),
|
||||||
|
availableWriteOffAmount: Number(row.availableWriteOffAmount || 0),
|
||||||
|
createTime: row.paymentCreateTime || row.createTime,
|
||||||
|
}));
|
||||||
this.changeRecords = detail.changeRecords || [];
|
this.changeRecords = detail.changeRecords || [];
|
||||||
this.attachments = this.parseAttachments(detail.attachmentsJson);
|
this.attachments = this.parseAttachments(detail.attachmentsJson);
|
||||||
this.sortAttachments();
|
this.sortAttachments();
|
||||||
@@ -1574,6 +1652,7 @@ export default {
|
|||||||
this.form.payerName = '';
|
this.form.payerName = '';
|
||||||
this.form.payeeName = '';
|
this.form.payeeName = '';
|
||||||
this.summaryFees = [];
|
this.summaryFees = [];
|
||||||
|
this.projectAdvanceWriteOffs = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.form.contractId = contract.id;
|
this.form.contractId = contract.id;
|
||||||
@@ -1584,9 +1663,16 @@ export default {
|
|||||||
this.form.deptId = contract.deptId;
|
this.form.deptId = contract.deptId;
|
||||||
this.form.deptName = contract.deptName;
|
this.form.deptName = contract.deptName;
|
||||||
this.form.settlementType = contract.settlementType || 'payable';
|
this.form.settlementType = contract.settlementType || 'payable';
|
||||||
this.form.payerName = contract.partyA;
|
// 部分候选合同只带 id/名称,缺甲方乙方;此时保留已带出的收付方,不要清成 undefined
|
||||||
this.form.payeeName = contract.partyB;
|
if (contract.partyA) this.form.payerName = contract.partyA;
|
||||||
|
if (contract.partyB) this.form.payeeName = contract.partyB;
|
||||||
|
this.form.currency = contract.settlementCurrency || 'RMB';
|
||||||
this.summaryFees = [];
|
this.summaryFees = [];
|
||||||
|
this.projectAdvanceWriteOffs = [];
|
||||||
|
if (this.form.currency !== 'RMB') {
|
||||||
|
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
|
||||||
|
this.fetchBfiExchangeRate();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async saveDraft(shouldSubmit) {
|
async saveDraft(shouldSubmit) {
|
||||||
await this.$refs.formRef?.validate();
|
await this.$refs.formRef?.validate();
|
||||||
@@ -1603,6 +1689,9 @@ export default {
|
|||||||
this.$message.warning(`请完善结算合计第${invalidManualFeeIndex + 1}行的费用类型和费用项`);
|
this.$message.warning(`请完善结算合计第${invalidManualFeeIndex + 1}行的费用类型和费用项`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.$refs.projectAdvanceWriteOffRef?.validateBeforeSave?.() === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const stateKey = shouldSubmit ? 'submitting' : 'saving';
|
const stateKey = shouldSubmit ? 'submitting' : 'saving';
|
||||||
this[stateKey] = true;
|
this[stateKey] = true;
|
||||||
try {
|
try {
|
||||||
@@ -1613,13 +1702,14 @@ export default {
|
|||||||
if (shouldSubmit) {
|
if (shouldSubmit) {
|
||||||
const previousApprovalStatus = this.form.approvalStatus || '';
|
const previousApprovalStatus = this.form.approvalStatus || '';
|
||||||
await submit({ id: this.form.id });
|
await submit({ id: this.form.id });
|
||||||
await submitMkApprovalFlow({
|
const mkStarted = await submitMkApprovalFlow({
|
||||||
bizType: 'pre-settlement',
|
bizType: 'pre-settlement',
|
||||||
bizId: this.form.id,
|
bizId: this.form.id,
|
||||||
subjectName: this.form.preSettlementNo || this.form.contractName || '',
|
subjectName: this.form.preSettlementNo || this.form.contractName || '',
|
||||||
approvalStatus: previousApprovalStatus,
|
approvalStatus: previousApprovalStatus,
|
||||||
|
mkEnabled: await loadMkEnabled().catch(() => false),
|
||||||
});
|
});
|
||||||
this.$message.success('审批流程已发起');
|
this.$message.success(mkStarted ? '审批流程已发起' : '提交成功');
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
this.$emit('success', this.form.id);
|
this.$emit('success', this.form.id);
|
||||||
return;
|
return;
|
||||||
@@ -1649,6 +1739,7 @@ export default {
|
|||||||
remark: row.remark,
|
remark: row.remark,
|
||||||
manualFlag: row.manualFlag || 0,
|
manualFlag: row.manualFlag || 0,
|
||||||
})),
|
})),
|
||||||
|
projectAdvanceWriteOffs: this.$refs.projectAdvanceWriteOffRef?.toSavePayload?.() || [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
addSummaryFee() {
|
addSummaryFee() {
|
||||||
@@ -1705,7 +1796,7 @@ export default {
|
|||||||
return option?.dictValue || value;
|
return option?.dictValue || value;
|
||||||
},
|
},
|
||||||
formatDetailMileage(value) {
|
formatDetailMileage(value) {
|
||||||
if (value === undefined || value === null || value === '' || Number(value) === -1) return '';
|
if (value === undefined || value === null || value === '' || Number(value) === -1) return '-';
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
removeSummaryFee(index) {
|
removeSummaryFee(index) {
|
||||||
@@ -1861,6 +1952,26 @@ export default {
|
|||||||
.toFixed(2)
|
.toFixed(2)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
handleExchangeRateDateChange() {
|
||||||
|
this.fetchBfiExchangeRate();
|
||||||
|
this.recalculateLocalAmount();
|
||||||
|
},
|
||||||
|
async fetchBfiExchangeRate() {
|
||||||
|
if (!this.form.currency || this.form.currency === 'RMB') return;
|
||||||
|
if (!this.form.exchangeRateDate) return;
|
||||||
|
try {
|
||||||
|
const { data } = await getBfiExchangeRate(this.form.currency, this.form.exchangeRateDate);
|
||||||
|
const rateData = data?.data;
|
||||||
|
if (rateData?.excval != null) {
|
||||||
|
this.form.exchangeRate = Number(rateData.excval);
|
||||||
|
this.recalculateLocalAmount();
|
||||||
|
} else {
|
||||||
|
this.$message.warning(`未查询到 ${this.form.currency} 的BFI汇率数据`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.warning('查询BFI汇率失败,请手动输入结算汇率');
|
||||||
|
}
|
||||||
|
},
|
||||||
recalculateLocalAmount() {
|
recalculateLocalAmount() {
|
||||||
const rate = this.form.currency === 'RMB' ? 1 : Number(this.form.exchangeRate || 0);
|
const rate = this.form.currency === 'RMB' ? 1 : Number(this.form.exchangeRate || 0);
|
||||||
this.form.localSettlementAmount = (Number(this.form.settlementAmount || 0) * rate).toFixed(2);
|
this.form.localSettlementAmount = (Number(this.form.settlementAmount || 0) * rate).toFixed(2);
|
||||||
@@ -1957,6 +2068,21 @@ export default {
|
|||||||
this.appliedDetailQuery = { ...this.detailQuery };
|
this.appliedDetailQuery = { ...this.detailQuery };
|
||||||
this.detailPage.current = 1;
|
this.detailPage.current = 1;
|
||||||
},
|
},
|
||||||
|
// 结算明细的「单据号」跳到该应收应付单据在应收明细/应付明细页的详情,「运单号」跳到运单详情
|
||||||
|
async handleDetailLink(row, column) {
|
||||||
|
if (column.prop === 'documentNo') {
|
||||||
|
const settlementType = row.settlementType || this.form.settlementType || 'payable';
|
||||||
|
const detailId = await resolveReceivablePayableDetailId(row, settlementType);
|
||||||
|
if (!detailId) return this.$message.info('当前记录未找到对应的应收/应付明细');
|
||||||
|
this.$router.push({ path: settlementDetailPath(settlementType), query: { detailId } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (column.prop === 'waybillNo') {
|
||||||
|
const waybillId = await resolveWaybillId(row);
|
||||||
|
if (!waybillId) return this.$message.info('当前记录未找到对应的运单详情');
|
||||||
|
this.$router.push({ path: '/business/waybill-manage/detail', query: { id: waybillId } });
|
||||||
|
}
|
||||||
|
},
|
||||||
resetDetailQuery() {
|
resetDetailQuery() {
|
||||||
this.detailQuery = {
|
this.detailQuery = {
|
||||||
documentNo: '',
|
documentNo: '',
|
||||||
@@ -2962,6 +3088,10 @@ export default {
|
|||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-readonly--code {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-section-title {
|
.dialog-section-title {
|
||||||
|
|||||||
@@ -0,0 +1,300 @@
|
|||||||
|
<template>
|
||||||
|
<section-card title="项目预付核销">
|
||||||
|
<template v-if="editable" #extra>
|
||||||
|
<el-button type="primary" :disabled="!contractId" @click="openSelectDialog">选择</el-button>
|
||||||
|
</template>
|
||||||
|
<el-table :data="rows" border>
|
||||||
|
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||||
|
<el-table-column
|
||||||
|
prop="paymentNo"
|
||||||
|
label="预付单号"
|
||||||
|
min-width="160"
|
||||||
|
align="center"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
<el-table-column label="项目预付金额" min-width="140" align="center">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.projectAdvanceAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="可核销金额" min-width="140" align="center">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.availableWriteOffAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="本次核销金额" min-width="160" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input-number
|
||||||
|
v-if="editable"
|
||||||
|
v-model="row.writeOffAmount"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:controls="false"
|
||||||
|
class="project-advance-write-off__amount"
|
||||||
|
@change="handleAmountChange(row)"
|
||||||
|
/>
|
||||||
|
<span v-else>{{ formatMoney(row.writeOffAmount) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="kingdeeBillNo"
|
||||||
|
label="金蝶预付单号"
|
||||||
|
min-width="160"
|
||||||
|
align="center"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="{ row }">{{ row.kingdeeBillNo || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="createUserName"
|
||||||
|
label="创建人"
|
||||||
|
min-width="120"
|
||||||
|
align="center"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="{ row }">{{ row.createUserName || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center">
|
||||||
|
<template #default="{ row }">{{ formatDateTime(row.createTime || row.paymentCreateTime) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column v-if="editable" label="操作" width="90" align="center" fixed="right">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-link type="danger" @click="removeRow($index)">删除</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialog.visible"
|
||||||
|
title="选择项目预付单"
|
||||||
|
width="960px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
class="dialog-form-title"
|
||||||
|
@closed="handleDialogClosed"
|
||||||
|
>
|
||||||
|
<el-table
|
||||||
|
v-loading="dialog.loading"
|
||||||
|
:data="dialog.rows"
|
||||||
|
border
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="55" align="center" :selectable="rowSelectable" />
|
||||||
|
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||||
|
<el-table-column
|
||||||
|
prop="paymentNo"
|
||||||
|
label="预付单号"
|
||||||
|
min-width="160"
|
||||||
|
align="center"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
<el-table-column label="项目预付金额" min-width="140" align="center">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.projectAdvanceAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="可核销金额" min-width="140" align="center">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.availableWriteOffAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="kingdeeBillNo"
|
||||||
|
label="金蝶预付单号"
|
||||||
|
min-width="160"
|
||||||
|
align="center"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="{ row }">{{ row.kingdeeBillNo || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="createUserName"
|
||||||
|
label="创建人"
|
||||||
|
min-width="120"
|
||||||
|
align="center"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="{ row }">{{ row.createUserName || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center">
|
||||||
|
<template #default="{ row }">{{ formatDateTime(row.createTime) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialog.visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :disabled="!dialog.selection.length" @click="confirmSelect">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</section-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getProjectAdvanceWriteOffOptions } from '@/api/payment/paymentApplication';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ProjectAdvanceWriteOff',
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
contractId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
settlementAmount: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
settlementType: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
settlementId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dialog: {
|
||||||
|
visible: false,
|
||||||
|
loading: false,
|
||||||
|
rows: [],
|
||||||
|
selection: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
rows: {
|
||||||
|
get() {
|
||||||
|
return Array.isArray(this.modelValue) ? this.modelValue : [];
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('update:modelValue', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatMoney(value) {
|
||||||
|
const amount = Number(value);
|
||||||
|
return Number.isFinite(amount) ? amount.toFixed(2) : '0.00';
|
||||||
|
},
|
||||||
|
formatDateTime(value) {
|
||||||
|
if (!value) return '-';
|
||||||
|
return this.$dayjs(value).format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
},
|
||||||
|
money(value) {
|
||||||
|
const amount = Number(value);
|
||||||
|
return Number.isFinite(amount) ? Number(amount.toFixed(2)) : 0;
|
||||||
|
},
|
||||||
|
openSelectDialog() {
|
||||||
|
if (!this.contractId) {
|
||||||
|
this.$message.warning('请先选择合同');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.dialog.visible = true;
|
||||||
|
this.loadOptions();
|
||||||
|
},
|
||||||
|
async loadOptions() {
|
||||||
|
this.dialog.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await getProjectAdvanceWriteOffOptions({
|
||||||
|
contractId: this.contractId,
|
||||||
|
excludeSettlementType: this.settlementType,
|
||||||
|
excludeSettlementId: this.settlementId || undefined,
|
||||||
|
});
|
||||||
|
this.dialog.rows = res?.data?.data || [];
|
||||||
|
} finally {
|
||||||
|
this.dialog.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rowSelectable(row) {
|
||||||
|
const selectedIds = new Set(this.rows.map(item => String(item.paymentApplicationId)));
|
||||||
|
return !selectedIds.has(String(row.paymentApplicationId));
|
||||||
|
},
|
||||||
|
handleSelectionChange(list) {
|
||||||
|
this.dialog.selection = list || [];
|
||||||
|
},
|
||||||
|
confirmSelect() {
|
||||||
|
const nextRows = [...this.rows];
|
||||||
|
const existed = new Set(nextRows.map(item => String(item.paymentApplicationId)));
|
||||||
|
this.dialog.selection.forEach(item => {
|
||||||
|
const id = String(item.paymentApplicationId);
|
||||||
|
if (existed.has(id)) return;
|
||||||
|
nextRows.push({
|
||||||
|
paymentApplicationId: item.paymentApplicationId,
|
||||||
|
paymentNo: item.paymentNo,
|
||||||
|
projectAdvanceAmount: this.money(item.projectAdvanceAmount),
|
||||||
|
availableWriteOffAmount: this.money(item.availableWriteOffAmount),
|
||||||
|
writeOffAmount: this.money(item.availableWriteOffAmount),
|
||||||
|
kingdeeBillNo: item.kingdeeBillNo || '',
|
||||||
|
createUserName: item.createUserName || '',
|
||||||
|
createTime: item.createTime,
|
||||||
|
paymentCreateTime: item.createTime,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.rows = nextRows;
|
||||||
|
this.dialog.visible = false;
|
||||||
|
this.validateTotal();
|
||||||
|
},
|
||||||
|
handleDialogClosed() {
|
||||||
|
this.dialog.rows = [];
|
||||||
|
this.dialog.selection = [];
|
||||||
|
},
|
||||||
|
handleAmountChange(row) {
|
||||||
|
const available = this.money(row.availableWriteOffAmount);
|
||||||
|
let amount = this.money(row.writeOffAmount);
|
||||||
|
if (amount < 0) amount = 0;
|
||||||
|
if (amount > available) {
|
||||||
|
amount = available;
|
||||||
|
this.$message.warning('本次核销金额不能超过可核销金额');
|
||||||
|
}
|
||||||
|
row.writeOffAmount = amount;
|
||||||
|
this.validateTotal();
|
||||||
|
},
|
||||||
|
validateTotal() {
|
||||||
|
const total = this.rows.reduce((sum, row) => sum + this.money(row.writeOffAmount), 0);
|
||||||
|
const settlementAmount = this.money(this.settlementAmount);
|
||||||
|
if (settlementAmount > 0 && total > settlementAmount) {
|
||||||
|
this.$message.warning('项目预付核销合计不能超过结算单金额');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
validateBeforeSave() {
|
||||||
|
for (const row of this.rows) {
|
||||||
|
const amount = this.money(row.writeOffAmount);
|
||||||
|
if (amount <= 0) {
|
||||||
|
this.$message.warning(`预付单${row.paymentNo || ''}本次核销金额必须大于0`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (amount > this.money(row.availableWriteOffAmount)) {
|
||||||
|
this.$message.warning(`预付单${row.paymentNo || ''}本次核销金额不能超过可核销金额`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.validateTotal();
|
||||||
|
},
|
||||||
|
removeRow(index) {
|
||||||
|
const nextRows = [...this.rows];
|
||||||
|
nextRows.splice(index, 1);
|
||||||
|
this.rows = nextRows;
|
||||||
|
},
|
||||||
|
toSavePayload() {
|
||||||
|
return this.rows.map(row => ({
|
||||||
|
paymentApplicationId: row.paymentApplicationId,
|
||||||
|
writeOffAmount: this.money(row.writeOffAmount),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.project-advance-write-off__amount {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
:deep(.project-advance-write-off__amount .el-input__inner) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@
|
|||||||
:record-id="recordId"
|
:record-id="recordId"
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
:initial-data="editorInitialData"
|
:initial-data="editorInitialData"
|
||||||
|
@success="markListNeedRefresh"
|
||||||
/>
|
/>
|
||||||
</basic-container>
|
</basic-container>
|
||||||
</template>
|
</template>
|
||||||
@@ -16,6 +17,8 @@
|
|||||||
import FormalSettlementEditor from './components/formal-settlement-editor.vue';
|
import FormalSettlementEditor from './components/formal-settlement-editor.vue';
|
||||||
import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer';
|
import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer';
|
||||||
|
|
||||||
|
const LIST_REFRESH_KEY = 'formal-settlement-list-refresh';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormalSettlementForm',
|
name: 'FormalSettlementForm',
|
||||||
components: { FormalSettlementEditor },
|
components: { FormalSettlementEditor },
|
||||||
@@ -54,6 +57,9 @@ export default {
|
|||||||
this.syncTagTitle();
|
this.syncTagTitle();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
markListNeedRefresh() {
|
||||||
|
sessionStorage.setItem(LIST_REFRESH_KEY, '1');
|
||||||
|
},
|
||||||
goBack() {
|
goBack() {
|
||||||
removeSettlementTransfer(this.$route.query.transferToken);
|
removeSettlementTransfer(this.$route.query.transferToken);
|
||||||
this.$router.$avueRouter.closeTag();
|
this.$router.$avueRouter.closeTag();
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
:page="page"
|
:page="page"
|
||||||
:has-permission="hasPermissionFn"
|
:has-permission="hasPermissionFn"
|
||||||
|
:sync-loading="syncLoading"
|
||||||
:mk-enabled="mkEnabled"
|
:mk-enabled="mkEnabled"
|
||||||
v-model:search-visible="searchVisible"
|
v-model:search-visible="searchVisible"
|
||||||
@create="openCreate"
|
@create="openCreate"
|
||||||
@@ -84,6 +85,7 @@
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
:page="page"
|
:page="page"
|
||||||
:has-permission="hasPermissionFn"
|
:has-permission="hasPermissionFn"
|
||||||
|
:sync-loading="syncLoading"
|
||||||
:mk-enabled="mkEnabled"
|
:mk-enabled="mkEnabled"
|
||||||
v-model:search-visible="searchVisible"
|
v-model:search-visible="searchVisible"
|
||||||
@create="openCreate"
|
@create="openCreate"
|
||||||
@@ -221,6 +223,7 @@ export default {
|
|||||||
rows: [],
|
rows: [],
|
||||||
selection: [],
|
selection: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
|
syncLoading: false,
|
||||||
page: { current: 1, size: 10, total: 0 },
|
page: { current: 1, size: 10, total: 0 },
|
||||||
editor: { visible: false, id: null, readonly: false },
|
editor: { visible: false, id: null, readonly: false },
|
||||||
paymentDialog: {
|
paymentDialog: {
|
||||||
@@ -262,6 +265,12 @@ export default {
|
|||||||
this.mkEnabled = false;
|
this.mkEnabled = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
activated() {
|
||||||
|
if (sessionStorage.getItem('formal-settlement-list-refresh') === '1') {
|
||||||
|
sessionStorage.removeItem('formal-settlement-list-refresh');
|
||||||
|
this.loadTable();
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
hasPermission(code) {
|
hasPermission(code) {
|
||||||
return this.permission?.[code] !== false;
|
return this.permission?.[code] !== false;
|
||||||
@@ -359,6 +368,7 @@ export default {
|
|||||||
bizId: row.id,
|
bizId: row.id,
|
||||||
subjectName: row.formalSettlementNo || '',
|
subjectName: row.formalSettlementNo || '',
|
||||||
approvalStatus: previousApprovalStatus,
|
approvalStatus: previousApprovalStatus,
|
||||||
|
mkEnabled: this.mkEnabled,
|
||||||
});
|
});
|
||||||
this.$message.success('提交成功');
|
this.$message.success('提交成功');
|
||||||
this.loadTable();
|
this.loadTable();
|
||||||
@@ -387,8 +397,13 @@ export default {
|
|||||||
'提示',
|
'提示',
|
||||||
{ type: 'warning' }
|
{ type: 'warning' }
|
||||||
);
|
);
|
||||||
await api.approve({ id: row.id });
|
const res = await api.approve({ id: row.id });
|
||||||
this.$message.success('审批通过');
|
// data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
|
||||||
|
if (res.data.data === false) {
|
||||||
|
this.$message.warning(res.data.msg);
|
||||||
|
} else {
|
||||||
|
this.$message.success(res.data.msg || '审批通过');
|
||||||
|
}
|
||||||
this.loadTable();
|
this.loadTable();
|
||||||
},
|
},
|
||||||
async handleReturn(row) {
|
async handleReturn(row) {
|
||||||
@@ -415,12 +430,28 @@ export default {
|
|||||||
return this.selection[0];
|
return this.selection[0];
|
||||||
},
|
},
|
||||||
async handleSync() {
|
async handleSync() {
|
||||||
|
if (this.syncLoading) return;
|
||||||
const row = this.selectedOne('同步金蝶');
|
const row = this.selectedOne('同步金蝶');
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
const response = await api.syncKingdee(row.id);
|
|
||||||
const data = this.unwrapData(response);
|
this.syncLoading = true;
|
||||||
this.$message.success(`同步成功,金蝶单据号:${data}`);
|
try {
|
||||||
this.loadTable();
|
const response = await api.syncKingdee(row.id);
|
||||||
|
const data = this.unwrapData(response);
|
||||||
|
this.$message.success(`同步成功,金蝶单据号:${data}`);
|
||||||
|
await this.loadTable();
|
||||||
|
// 附件联带同步提示:同步成功但附件未全部上传时追加 warning,重推即自动补传
|
||||||
|
const freshRow = (this.rows || []).find(item => String(item.id) === String(row.id));
|
||||||
|
if (freshRow && ['pending', 'partial'].includes(freshRow.attachmentSyncStatus)) {
|
||||||
|
this.$message({
|
||||||
|
type: 'warning',
|
||||||
|
message: '该单附件未全部上传,可重推补传',
|
||||||
|
duration: 8000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.syncLoading = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async openPaymentDialog() {
|
async openPaymentDialog() {
|
||||||
if (!this.selection.length) return this.$message.warning('请至少选择一条正式结算单');
|
if (!this.selection.length) return this.$message.warning('请至少选择一条正式结算单');
|
||||||
|
|||||||
@@ -661,6 +661,7 @@ export default {
|
|||||||
bizId: row.id,
|
bizId: row.id,
|
||||||
subjectName: row.preSettlementNo || '',
|
subjectName: row.preSettlementNo || '',
|
||||||
approvalStatus: previousApprovalStatus,
|
approvalStatus: previousApprovalStatus,
|
||||||
|
mkEnabled: this.mkEnabled,
|
||||||
});
|
});
|
||||||
this.$message.success('提交成功');
|
this.$message.success('提交成功');
|
||||||
this.loadTable();
|
this.loadTable();
|
||||||
|
|||||||
@@ -520,7 +520,7 @@
|
|||||||
:index="transferIndexMethod"
|
:index="transferIndexMethod"
|
||||||
/>
|
/>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
v-for="column in tableColumns.slice(0, 13)"
|
v-for="column in transferTableColumns"
|
||||||
:key="column.prop"
|
:key="column.prop"
|
||||||
:prop="column.prop"
|
:prop="column.prop"
|
||||||
:label="column.label"
|
:label="column.label"
|
||||||
@@ -1174,6 +1174,13 @@ export default {
|
|||||||
columns.splice(totalIndex, 0, ...dynamicColumns);
|
columns.splice(totalIndex, 0, ...dynamicColumns);
|
||||||
return columns;
|
return columns;
|
||||||
},
|
},
|
||||||
|
// 批量转结算弹窗只展示前 13 列。预结算单号、正式结算单号正是本操作要生成的单号,
|
||||||
|
// 候选行上必为空,故不占列
|
||||||
|
transferTableColumns() {
|
||||||
|
return this.tableColumns
|
||||||
|
.filter(column => !['preSettlementNo', 'formalSettlementNo'].includes(column.prop))
|
||||||
|
.slice(0, 13);
|
||||||
|
},
|
||||||
columnSettingOptions() {
|
columnSettingOptions() {
|
||||||
return this.displayTableColumns.map(column => ({
|
return this.displayTableColumns.map(column => ({
|
||||||
prop: column.prop,
|
prop: column.prop,
|
||||||
@@ -1712,11 +1719,12 @@ export default {
|
|||||||
this.$router.push({ path, query: { detailNo } });
|
this.$router.push({ path, query: { detailNo } });
|
||||||
},
|
},
|
||||||
async openRouteDetail(detailId = this.$route.query.detailId) {
|
async openRouteDetail(detailId = this.$route.query.detailId) {
|
||||||
if (
|
// 结算明细点「单据号」会带 detailId 跳到本页;只有当前停在应收/应付明细页时才自动展开详情
|
||||||
!detailId ||
|
const routePath =
|
||||||
this.settlementType !== 'payable' ||
|
this.settlementType === 'payable'
|
||||||
this.$route.path !== '/settlement/payable-detail'
|
? '/settlement/payable-detail'
|
||||||
) {
|
: '/settlement/receivable-detail';
|
||||||
|
if (!detailId || this.$route.path !== routePath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let row = this.rows.find(item => String(item.id) === String(detailId));
|
let row = this.rows.find(item => String(item.id) === String(detailId));
|
||||||
@@ -1729,7 +1737,9 @@ export default {
|
|||||||
if (data.records?.[0]) row = this.decorateRow(data.records[0]);
|
if (data.records?.[0]) row = this.decorateRow(data.records[0]);
|
||||||
}
|
}
|
||||||
if (!row) {
|
if (!row) {
|
||||||
this.$message.info('当前记录未找到对应的应付明细');
|
this.$message.info(
|
||||||
|
`当前记录未找到对应的${this.settlementType === 'payable' ? '应付' : '应收'}明细`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.openDetailDialog(row);
|
await this.openDetailDialog(row);
|
||||||
@@ -2681,18 +2691,22 @@ export default {
|
|||||||
return `${Number(value).toFixed(2)} ${currency}`;
|
return `${Number(value).toFixed(2)} ${currency}`;
|
||||||
},
|
},
|
||||||
formatDetailCell(row, prop) {
|
formatDetailCell(row, prop) {
|
||||||
|
if (prop === 'mileage' && Number(row?.[prop]) === -1) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
prop === 'mileage' &&
|
prop === 'mileage' &&
|
||||||
(row?.[prop] === null ||
|
(row?.[prop] === null || row?.[prop] === undefined || row?.[prop] === '')
|
||||||
row?.[prop] === undefined ||
|
|
||||||
row?.[prop] === '' ||
|
|
||||||
Number(row[prop]) === -1)
|
|
||||||
) {
|
) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return this.formatCell(row?.[prop]);
|
return this.formatCell(row?.[prop]);
|
||||||
},
|
},
|
||||||
formatFeeDetailCell(row, prop) {
|
formatFeeDetailCell(row, prop) {
|
||||||
|
if (prop === 'transportQuantityText' || prop === 'transportQuantity') {
|
||||||
|
if (this.isBlankTransportQuantity(row)) return '-';
|
||||||
|
return this.formatDetailCell(row, prop);
|
||||||
|
}
|
||||||
if (prop !== 'originalAmountText') return this.formatDetailCell(row, prop);
|
if (prop !== 'originalAmountText') return this.formatDetailCell(row, prop);
|
||||||
|
|
||||||
const adjustAmount = Number(row?.adjustAmount || 0);
|
const adjustAmount = Number(row?.adjustAmount || 0);
|
||||||
@@ -2711,6 +2725,17 @@ export default {
|
|||||||
formatCell(value) {
|
formatCell(value) {
|
||||||
return value === null || value === undefined || value === '' ? '-' : value;
|
return value === null || value === undefined || value === '' ? '-' : value;
|
||||||
},
|
},
|
||||||
|
isBlankTransportQuantity(row = {}) {
|
||||||
|
const quantity = row.transportQuantity;
|
||||||
|
if (quantity !== null && quantity !== undefined && quantity !== '') {
|
||||||
|
const amount = Number(quantity);
|
||||||
|
return !Number.isFinite(amount) || amount === 0 || amount === -1;
|
||||||
|
}
|
||||||
|
const text = String(row.transportQuantityText || '').trim();
|
||||||
|
if (!text || text === '-') return true;
|
||||||
|
const amount = Number(text.split(/\s+/)[0]);
|
||||||
|
return Number.isFinite(amount) && (amount === 0 || amount === -1);
|
||||||
|
},
|
||||||
parseJson(value) {
|
parseJson(value) {
|
||||||
if (!value) return [];
|
if (!value) return [];
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<basic-container class="settlement-import-form-page">
|
||||||
|
<div class="settlement-import-form-page__title">{{ pageTitle }}</div>
|
||||||
|
<settlement-import-page />
|
||||||
|
</basic-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import SettlementImportPage from './components/settlement-import-page.vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const pageTitle = computed(() => (route.query.id ? '编辑导入结算单' : '新增导入结算单'));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.settlement-import-form-page {
|
||||||
|
:deep(.basic-container__card) {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.basic-container__card > .el-card__body) {
|
||||||
|
padding: 0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-import-form-page__title {
|
||||||
|
position: relative;
|
||||||
|
padding: 16px 24px 12px 36px;
|
||||||
|
border-bottom: 1px solid #eff1f7;
|
||||||
|
background: #fff;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 24px;
|
||||||
|
width: 4px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #409eff;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
<template>
|
||||||
|
<basic-container class="settlement-import-page">
|
||||||
|
<section v-show="searchVisible" class="settlement-import-page__search">
|
||||||
|
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
|
||||||
|
<div class="settlement-import-page__search-grid">
|
||||||
|
<el-form-item v-for="field in visibleSearchFields" :key="field.prop" :label="field.label">
|
||||||
|
<el-select
|
||||||
|
v-if="field.type === 'select'"
|
||||||
|
v-model="query[field.prop]"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
allow-create
|
||||||
|
default-first-option
|
||||||
|
placeholder="请选择"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in field.options"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
|
||||||
|
</el-form-item>
|
||||||
|
<div class="settlement-import-page__search-actions">
|
||||||
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button @click="resetSearch">重置</el-button>
|
||||||
|
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
|
||||||
|
{{ searchExpanded ? '收起' : '展开' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settlement-import-page__table-panel">
|
||||||
|
<div class="settlement-import-page__toolbar">
|
||||||
|
<div class="settlement-import-page__toolbar-left">
|
||||||
|
<el-button
|
||||||
|
v-if="hasPermission('settlement_import_add')"
|
||||||
|
type="primary"
|
||||||
|
@click="openCreate"
|
||||||
|
>
|
||||||
|
新增
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="rows" border class="settlement-import-page__table">
|
||||||
|
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||||
|
<el-table-column prop="batchNo" label="导入批次号" min-width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link type="primary" @click="openDetail(row)">{{ row.batchNo || '-' }}</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="settlementTypeName" label="导入类型" min-width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.settlementTypeName || settlementTypeLabel(row.settlementType) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="settlementBillTypeName" label="结算单" min-width="110">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.settlementBillTypeName || settlementBillTypeLabel(row.settlementBillType) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="projectName" label="项目" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="customerName" label="客户" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="carrierName" label="承运商" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column label="明细行数" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.waybillCount ?? row.detailCount ?? 0 }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createTime" label="创建时间" min-width="170" />
|
||||||
|
<el-table-column prop="createUserName" label="创建人" min-width="120">
|
||||||
|
<template #default="{ row }">{{ row.createUserName || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="160" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="settlement-import-page__row-actions">
|
||||||
|
<el-link type="primary" @click="openDetail(row)">详情</el-link>
|
||||||
|
<el-link
|
||||||
|
v-if="row.importStatus !== 'voided' && hasPermission('settlement_import_void')"
|
||||||
|
type="danger"
|
||||||
|
@click="openVoid(row)"
|
||||||
|
>
|
||||||
|
作废
|
||||||
|
</el-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="settlement-import-page__pagination">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
v-model:current-page="page.current"
|
||||||
|
v-model:page-size="page.size"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="page.total"
|
||||||
|
@current-change="loadTable"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<el-dialog v-model="detailVisible" title="导入结算单详情" width="640px" append-to-body>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="导入批次号">{{ detail.batchNo || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="导入类型">
|
||||||
|
{{ detail.settlementTypeName || settlementTypeLabel(detail.settlementType) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="结算单">
|
||||||
|
{{ detail.settlementBillTypeName || settlementBillTypeLabel(detail.settlementBillType) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="运单类型">
|
||||||
|
{{ detail.waybillModeName || waybillModeLabel(detail.waybillMode) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="项目">{{ detail.projectName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="客户">{{ detail.customerName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="客户合同">
|
||||||
|
{{ detail.customerContractName || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="承运商">{{ detail.carrierName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="承运商合同">
|
||||||
|
{{ detail.carrierContractName || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="明细行数">
|
||||||
|
{{ detail.waybillCount ?? detail.detailCount ?? 0 }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="导入状态">
|
||||||
|
{{ detail.importStatusName || detail.importStatus || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建人">{{ detail.createUserName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ detail.createTime || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备注" :span="2">{{ detail.remark || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="关联结算单" :span="2">
|
||||||
|
<el-link
|
||||||
|
v-if="detail.formalSettlementId"
|
||||||
|
type="primary"
|
||||||
|
@click="goFormalSettlement(detail)"
|
||||||
|
>
|
||||||
|
{{ detail.formalSettlementNo || detail.formalSettlementId }}
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
v-else-if="detail.preSettlementId"
|
||||||
|
type="primary"
|
||||||
|
@click="goPreSettlement(detail)"
|
||||||
|
>
|
||||||
|
{{ detail.preSettlementNo || detail.preSettlementId }}
|
||||||
|
</el-link>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="detailVisible = false">关闭</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="detail.importStatus === 'draft'"
|
||||||
|
type="primary"
|
||||||
|
@click="editDraft(detail)"
|
||||||
|
>
|
||||||
|
编辑草稿
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="voidVisible" title="作废导入结算单" width="480px" append-to-body>
|
||||||
|
<el-form
|
||||||
|
ref="voidFormRef"
|
||||||
|
:model="voidForm"
|
||||||
|
:rules="voidRules"
|
||||||
|
label-position="right"
|
||||||
|
label-width="auto"
|
||||||
|
>
|
||||||
|
<el-form-item label="作废原因" prop="reason">
|
||||||
|
<el-input
|
||||||
|
v-model="voidForm.reason"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入作废原因"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="voidVisible = false">取消</el-button>
|
||||||
|
<el-button type="danger" :loading="voidLoading" @click="submitVoid">确认作废</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</basic-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import * as api from '@/api/settlement/settlementImport';
|
||||||
|
|
||||||
|
const emptyQuery = () => ({
|
||||||
|
batchNo: '',
|
||||||
|
settlementType: '',
|
||||||
|
projectName: '',
|
||||||
|
customerName: '',
|
||||||
|
carrierName: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const SETTLEMENT_TYPE_OPTIONS = [
|
||||||
|
{ label: '全部', value: '' },
|
||||||
|
{ label: '应收正式结算单', value: 'receivable' },
|
||||||
|
{ label: '应付正式结算单', value: 'payable' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SETTLEMENT_IMPORT_LIST_REFRESH_KEY = 'settlement-import-list-refresh';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SettlementImport',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
ArrowDown,
|
||||||
|
ArrowUp,
|
||||||
|
searchVisible: true,
|
||||||
|
searchExpanded: false,
|
||||||
|
loading: false,
|
||||||
|
query: emptyQuery(),
|
||||||
|
rows: [],
|
||||||
|
page: { current: 1, size: 10, total: 0 },
|
||||||
|
batchNoOptions: [],
|
||||||
|
detailVisible: false,
|
||||||
|
detail: {},
|
||||||
|
voidVisible: false,
|
||||||
|
voidLoading: false,
|
||||||
|
voidForm: { id: null, reason: '' },
|
||||||
|
voidRules: {
|
||||||
|
reason: [
|
||||||
|
{ required: true, message: '请输入作废原因', trigger: 'blur' },
|
||||||
|
{ max: 200, message: '作废原因不能超过200个字', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['permission', 'userInfo']),
|
||||||
|
isAdmin() {
|
||||||
|
const authority = this.userInfo?.authority || '';
|
||||||
|
return String(authority).includes('admin');
|
||||||
|
},
|
||||||
|
searchFields() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
prop: 'batchNo',
|
||||||
|
label: '导入批次号',
|
||||||
|
type: 'select',
|
||||||
|
options: this.batchNoOptions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'settlementType',
|
||||||
|
label: '导入类型',
|
||||||
|
type: 'select',
|
||||||
|
options: SETTLEMENT_TYPE_OPTIONS,
|
||||||
|
},
|
||||||
|
{ prop: 'projectName', label: '项目', type: 'input' },
|
||||||
|
{ prop: 'customerName', label: '客户', type: 'input' },
|
||||||
|
{ prop: 'carrierName', label: '承运商', type: 'input' },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
visibleSearchFields() {
|
||||||
|
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadOptions();
|
||||||
|
this.loadTable();
|
||||||
|
},
|
||||||
|
activated() {
|
||||||
|
this.consumeListRefresh();
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route(to, from) {
|
||||||
|
if (
|
||||||
|
to.path === '/settlement/settlement-import' &&
|
||||||
|
from?.path === '/settlement/settlement-import/form'
|
||||||
|
) {
|
||||||
|
this.consumeListRefresh();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
consumeListRefresh() {
|
||||||
|
if (sessionStorage.getItem(SETTLEMENT_IMPORT_LIST_REFRESH_KEY) !== '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sessionStorage.removeItem(SETTLEMENT_IMPORT_LIST_REFRESH_KEY);
|
||||||
|
this.page.current = 1;
|
||||||
|
this.loadOptions();
|
||||||
|
this.loadTable();
|
||||||
|
},
|
||||||
|
hasPermission(code) {
|
||||||
|
return this.isAdmin || this.validData(this.permission?.[code], false);
|
||||||
|
},
|
||||||
|
settlementTypeLabel(value) {
|
||||||
|
return (
|
||||||
|
SETTLEMENT_TYPE_OPTIONS.find(item => item.value === value)?.label || value || '-'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
settlementBillTypeLabel(value) {
|
||||||
|
if (value === 'pre') return '预结算单';
|
||||||
|
return '正式结算单';
|
||||||
|
},
|
||||||
|
waybillModeLabel(value) {
|
||||||
|
if (value === 'history') return '历史运单';
|
||||||
|
return '新运单';
|
||||||
|
},
|
||||||
|
unwrapData(response) {
|
||||||
|
const body = response?.data || response || {};
|
||||||
|
return body?.data !== undefined ? body.data : body;
|
||||||
|
},
|
||||||
|
async loadOptions() {
|
||||||
|
try {
|
||||||
|
const response = await api.getOptions();
|
||||||
|
const options = this.unwrapData(response) || {};
|
||||||
|
const batchNos = Array.isArray(options.batchNos) ? options.batchNos : [];
|
||||||
|
this.batchNoOptions = [
|
||||||
|
{ label: '全部', value: '' },
|
||||||
|
...batchNos.map(item => {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
return { label: item, value: item };
|
||||||
|
}
|
||||||
|
const value = item.id ?? item.value ?? item.name ?? item.batchNo ?? '';
|
||||||
|
const label = item.name ?? item.label ?? item.batchNo ?? value;
|
||||||
|
return { label: String(label || ''), value: String(value || '') };
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
} catch (error) {
|
||||||
|
this.batchNoOptions = [{ label: '全部', value: '' }];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadTable() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const response = await api.getList(this.page.current, this.page.size, {
|
||||||
|
batchNo: this.query.batchNo || undefined,
|
||||||
|
settlementType: this.query.settlementType || undefined,
|
||||||
|
projectName: this.query.projectName || undefined,
|
||||||
|
customerName: this.query.customerName || undefined,
|
||||||
|
carrierName: this.query.carrierName || undefined,
|
||||||
|
});
|
||||||
|
const data = this.unwrapData(response) || {};
|
||||||
|
this.rows = data.records || [];
|
||||||
|
this.page.total = Number(data.total || 0);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.page.current = 1;
|
||||||
|
this.loadTable();
|
||||||
|
},
|
||||||
|
resetSearch() {
|
||||||
|
this.query = emptyQuery();
|
||||||
|
this.handleSearch();
|
||||||
|
},
|
||||||
|
toggleSearch() {
|
||||||
|
this.searchExpanded = !this.searchExpanded;
|
||||||
|
},
|
||||||
|
handleSizeChange() {
|
||||||
|
this.page.current = 1;
|
||||||
|
this.loadTable();
|
||||||
|
},
|
||||||
|
openCreate() {
|
||||||
|
this.$router.push({ path: '/settlement/settlement-import/form' });
|
||||||
|
},
|
||||||
|
async openDetail(row) {
|
||||||
|
if (!row?.id) return;
|
||||||
|
try {
|
||||||
|
const response = await api.getDetail(row.id);
|
||||||
|
this.detail = this.unwrapData(response) || { ...row };
|
||||||
|
} catch (error) {
|
||||||
|
this.detail = { ...row };
|
||||||
|
}
|
||||||
|
this.detailVisible = true;
|
||||||
|
},
|
||||||
|
editDraft(row) {
|
||||||
|
this.detailVisible = false;
|
||||||
|
this.$router.push({
|
||||||
|
path: '/settlement/settlement-import/form',
|
||||||
|
query: { id: row.id },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
goFormalSettlement(row) {
|
||||||
|
if (!row?.formalSettlementId) return;
|
||||||
|
this.detailVisible = false;
|
||||||
|
this.$router.push({
|
||||||
|
path: '/settlement/formal-settlement/form',
|
||||||
|
query: {
|
||||||
|
mode: 'view',
|
||||||
|
id: row.formalSettlementId,
|
||||||
|
name: '正式结算详情',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
goPreSettlement(row) {
|
||||||
|
if (!row?.preSettlementId) return;
|
||||||
|
this.detailVisible = false;
|
||||||
|
this.$router.push({
|
||||||
|
path: '/settlement/pre-settlement/form',
|
||||||
|
query: {
|
||||||
|
mode: 'view',
|
||||||
|
id: row.preSettlementId,
|
||||||
|
name: '预结算详情',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
openVoid(row) {
|
||||||
|
this.voidForm = { id: row.id, reason: '' };
|
||||||
|
this.voidVisible = true;
|
||||||
|
this.$nextTick(() => this.$refs.voidFormRef?.clearValidate());
|
||||||
|
},
|
||||||
|
async submitVoid() {
|
||||||
|
await this.$refs.voidFormRef?.validate();
|
||||||
|
this.voidLoading = true;
|
||||||
|
try {
|
||||||
|
await api.voidBatch({
|
||||||
|
id: this.voidForm.id,
|
||||||
|
reason: String(this.voidForm.reason || '').trim(),
|
||||||
|
});
|
||||||
|
this.$message.success('作废成功');
|
||||||
|
this.voidVisible = false;
|
||||||
|
this.loadTable();
|
||||||
|
this.loadOptions();
|
||||||
|
} finally {
|
||||||
|
this.voidLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.settlement-import-page__search {
|
||||||
|
padding: 12px 12px 4px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-import-page__search-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 8px 24px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-import-page__search :deep(.el-form-item) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-import-page__search :deep(.el-form-item__label) {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-import-page__search :deep(.el-input),
|
||||||
|
.settlement-import-page__search :deep(.el-select) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-import-page__search-actions {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-import-page__table-panel {
|
||||||
|
margin-top: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-import-page__toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-import-page__toolbar-left {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-import-page__table {
|
||||||
|
--el-table-border-color: #eff1f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-import-page__table :deep(.el-table__body tr:nth-child(even) > td.el-table__cell),
|
||||||
|
.settlement-import-page__table
|
||||||
|
:deep(.el-table__body tr:nth-child(even) > td.el-table-fixed-column--left),
|
||||||
|
.settlement-import-page__table
|
||||||
|
:deep(.el-table__body tr:nth-child(even) > td.el-table-fixed-column--right) {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-import-page__row-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-import-page__pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-import-page :deep(.basic-container__card > .el-card__body) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.settlement-import-page__search-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.settlement-import-page__search-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -165,6 +165,9 @@
|
|||||||
<span class="oa-sync-stat__value oa-sync-stat__value--skip">{{ oaSyncProgress.skipped }}</span>
|
<span class="oa-sync-stat__value oa-sync-stat__value--skip">{{ oaSyncProgress.skipped }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="oaSyncProgress.dumpFilePath" class="oa-sync-meta">
|
||||||
|
原始数据 JSON:{{ oaSyncProgress.dumpFilePath }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
@@ -253,6 +256,7 @@ export default {
|
|||||||
total: 0,
|
total: 0,
|
||||||
synced: 0,
|
synced: 0,
|
||||||
skipped: 0,
|
skipped: 0,
|
||||||
|
dumpFilePath: '',
|
||||||
},
|
},
|
||||||
parentId: 0,
|
parentId: 0,
|
||||||
page: {
|
page: {
|
||||||
@@ -658,7 +662,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
handleOaOrgFullSync() {
|
handleOaOrgFullSync() {
|
||||||
this.$confirm(
|
this.$confirm(
|
||||||
'全量同步将拉取 OA 全部公司/部门;已存在且 OA 更新时间一致的记录会跳过。是否清除后同步?选择“是”将保留顶级「外部组织」与集团锚点(id=135/GWWL),删除其余组织后再同步;选择“否”则直接全量同步。',
|
'全量同步将拉取 OA 公司/部门(仅保留二级公司 oaId=2、135 及其子公司与部门);已存在且 OA 更新时间一致的记录会跳过。是否清除后同步?选择“是”将保留顶级「外部组织」与集团锚点(id=1/GWWL),删除其余组织后再同步;选择“否”则直接全量同步。',
|
||||||
'全量同步',
|
'全量同步',
|
||||||
{
|
{
|
||||||
confirmButtonText: '是',
|
confirmButtonText: '是',
|
||||||
@@ -709,6 +713,7 @@ export default {
|
|||||||
total: 0,
|
total: 0,
|
||||||
synced: 0,
|
synced: 0,
|
||||||
skipped: 0,
|
skipped: 0,
|
||||||
|
dumpFilePath: '',
|
||||||
};
|
};
|
||||||
this.runOaOrgSyncPages();
|
this.runOaOrgSyncPages();
|
||||||
},
|
},
|
||||||
@@ -742,6 +747,9 @@ export default {
|
|||||||
this.oaSyncProgress.total = page.total || 0;
|
this.oaSyncProgress.total = page.total || 0;
|
||||||
this.oaSyncProgress.synced += page.syncedCount || 0;
|
this.oaSyncProgress.synced += page.syncedCount || 0;
|
||||||
this.oaSyncProgress.skipped += page.skippedCount || 0;
|
this.oaSyncProgress.skipped += page.skippedCount || 0;
|
||||||
|
if (page.dumpFilePath) {
|
||||||
|
this.oaSyncProgress.dumpFilePath = page.dumpFilePath;
|
||||||
|
}
|
||||||
if (page.finished) {
|
if (page.finished) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -364,6 +364,9 @@
|
|||||||
<span class="oa-sync-stat__value oa-sync-stat__value--skip">{{ oaSyncProgress.skipped }}</span>
|
<span class="oa-sync-stat__value oa-sync-stat__value--skip">{{ oaSyncProgress.skipped }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="oaSyncProgress.dumpFilePath" class="oa-sync-meta">
|
||||||
|
原始数据 JSON:{{ oaSyncProgress.dumpFilePath }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
@@ -450,6 +453,7 @@ export default {
|
|||||||
total: 0,
|
total: 0,
|
||||||
synced: 0,
|
synced: 0,
|
||||||
skipped: 0,
|
skipped: 0,
|
||||||
|
dumpFilePath: '',
|
||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@@ -712,6 +716,7 @@ export default {
|
|||||||
total: 0,
|
total: 0,
|
||||||
synced: 0,
|
synced: 0,
|
||||||
skipped: 0,
|
skipped: 0,
|
||||||
|
dumpFilePath: '',
|
||||||
};
|
};
|
||||||
this.runOaSyncPages();
|
this.runOaSyncPages();
|
||||||
},
|
},
|
||||||
@@ -746,6 +751,9 @@ export default {
|
|||||||
this.oaSyncProgress.total = page.total || 0;
|
this.oaSyncProgress.total = page.total || 0;
|
||||||
this.oaSyncProgress.synced += page.syncedCount || 0;
|
this.oaSyncProgress.synced += page.syncedCount || 0;
|
||||||
this.oaSyncProgress.skipped += page.skippedCount || 0;
|
this.oaSyncProgress.skipped += page.skippedCount || 0;
|
||||||
|
if (page.dumpFilePath) {
|
||||||
|
this.oaSyncProgress.dumpFilePath = page.dumpFilePath;
|
||||||
|
}
|
||||||
if (page.finished) {
|
if (page.finished) {
|
||||||
this.oaSyncStatus = 'done';
|
this.oaSyncStatus = 'done';
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ import { exportBlob } from '@/api/common';
|
|||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { getToken, isAdministrator } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
import { applyIncomingSearch, normalizeSearchRangeParams } from '@/utils/search-range';
|
||||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { excelOption, exportColumns, option } from '@/option/vehicle/accident-record';
|
import { excelOption, exportColumns, option } from '@/option/vehicle/accident-record';
|
||||||
@@ -550,7 +550,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
buildQuery(params = {}) {
|
buildQuery(params = {}) {
|
||||||
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
|
return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
|
||||||
},
|
},
|
||||||
handleTemplate() {
|
handleTemplate() {
|
||||||
exportBlob(
|
exportBlob(
|
||||||
|
|||||||
@@ -468,7 +468,11 @@ import {
|
|||||||
publish,
|
publish,
|
||||||
changeStatus,
|
changeStatus,
|
||||||
} from '@/api/vehicle/credit-score-quantification';
|
} from '@/api/vehicle/credit-score-quantification';
|
||||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
import {
|
||||||
|
applyIncomingSearch,
|
||||||
|
normalizeSearchRangeParams,
|
||||||
|
omitInvalidListStatus,
|
||||||
|
} from '@/utils/search-range';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { isAdministrator } from '@/utils/auth';
|
import { isAdministrator } from '@/utils/auth';
|
||||||
|
|
||||||
@@ -1324,7 +1328,9 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
buildQuery(params = {}) {
|
buildQuery(params = {}) {
|
||||||
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
|
return omitInvalidListStatus(
|
||||||
|
normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -109,6 +109,12 @@
|
|||||||
{{ row.status === 1 ? '启用' : '停用' }}
|
{{ row.status === 1 ? '启用' : '停用' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
|
<template #kingdeeStatus="{ row }">
|
||||||
|
<el-tag v-if="row.kingdeeStatus === 'synced'" type="success">已推送</el-tag>
|
||||||
|
<el-tag v-else-if="row.kingdeeStatus === 'exist'" type="primary">金蝶已有</el-tag>
|
||||||
|
<el-tag v-else-if="row.kingdeeStatus === 'failed'" type="danger">推送失败</el-tag>
|
||||||
|
<el-tag v-else type="info">未推送</el-tag>
|
||||||
|
</template>
|
||||||
<template #menu="{ row }">
|
<template #menu="{ row }">
|
||||||
<el-link
|
<el-link
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -138,7 +144,7 @@
|
|||||||
type="warning"
|
type="warning"
|
||||||
v-if="
|
v-if="
|
||||||
hasPermission('customer_archive_submit') &&
|
hasPermission('customer_archive_submit') &&
|
||||||
['draft', 'reviewing'].includes(row.approvalStatus)
|
row.approvalStatus === 'reviewing'
|
||||||
"
|
"
|
||||||
@click="handleWithdrawApproval(row)"
|
@click="handleWithdrawApproval(row)"
|
||||||
>
|
>
|
||||||
@@ -196,6 +202,18 @@
|
|||||||
>
|
>
|
||||||
{{ row.status === 1 ? '停用' : '启用' }}
|
{{ row.status === 1 ? '停用' : '启用' }}
|
||||||
</el-link>
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
type="primary"
|
||||||
|
:disabled="kingdeeSyncingId === row.id"
|
||||||
|
v-if="
|
||||||
|
hasPermission('customer_archive_sync') &&
|
||||||
|
row.approvalStatus === 'approved' &&
|
||||||
|
!['synced', 'exist'].includes(row.kingdeeStatus)
|
||||||
|
"
|
||||||
|
@click="handleSyncKingdee(row)"
|
||||||
|
>
|
||||||
|
{{ kingdeeSyncingId === row.id ? '推送中…' : '同步金蝶' }}
|
||||||
|
</el-link>
|
||||||
<el-link
|
<el-link
|
||||||
type="danger"
|
type="danger"
|
||||||
v-if="hasPermission('customer_archive_delete') && row.approvalStatus === 'draft'"
|
v-if="hasPermission('customer_archive_delete') && row.approvalStatus === 'draft'"
|
||||||
@@ -297,6 +315,7 @@
|
|||||||
placeholder="请输入"
|
placeholder="请输入"
|
||||||
maxlength="18"
|
maxlength="18"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
|
:disabled="kingdeeLocked && Boolean(archiveForm.id)"
|
||||||
@input="
|
@input="
|
||||||
archiveForm.unifiedCreditCode = String(
|
archiveForm.unifiedCreditCode = String(
|
||||||
archiveForm.unifiedCreditCode || ''
|
archiveForm.unifiedCreditCode || ''
|
||||||
@@ -312,6 +331,7 @@
|
|||||||
placeholder="请输入"
|
placeholder="请输入"
|
||||||
maxlength="100"
|
maxlength="100"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
|
:disabled="kingdeeLocked"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -335,7 +355,12 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="企业法人" prop="legalPerson">
|
<el-form-item label="企业法人" prop="legalPerson">
|
||||||
<el-input v-model="archiveForm.legalPerson" maxlength="10" show-word-limit />
|
<el-input
|
||||||
|
v-model="archiveForm.legalPerson"
|
||||||
|
maxlength="10"
|
||||||
|
show-word-limit
|
||||||
|
:disabled="kingdeeLocked"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
@@ -461,7 +486,7 @@
|
|||||||
:options="deptTree"
|
:options="deptTree"
|
||||||
:props="deptCascaderProps"
|
:props="deptCascaderProps"
|
||||||
disabled
|
disabled
|
||||||
placeholder="桂物物流集团"
|
placeholder="广西现代物流集团有限公司"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -476,6 +501,7 @@
|
|||||||
<el-input
|
<el-input
|
||||||
v-model="archiveForm.registeredCapital"
|
v-model="archiveForm.registeredCapital"
|
||||||
maxlength="18"
|
maxlength="18"
|
||||||
|
:disabled="kingdeeLocked"
|
||||||
@input="value => handleDecimalInput('registeredCapital', value)"
|
@input="value => handleDecimalInput('registeredCapital', value)"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -1528,6 +1554,8 @@ import {
|
|||||||
remove,
|
remove,
|
||||||
getScoreTemplate,
|
getScoreTemplate,
|
||||||
recognizeBusinessLicenseOcr,
|
recognizeBusinessLicenseOcr,
|
||||||
|
queryKingdeeCustomer,
|
||||||
|
syncKingdee,
|
||||||
} from '@/api/vehicle/customer-archive';
|
} from '@/api/vehicle/customer-archive';
|
||||||
import {
|
import {
|
||||||
getList as getCreditScoreQuantificationList,
|
getList as getCreditScoreQuantificationList,
|
||||||
@@ -1560,7 +1588,7 @@ const customerArchiveListRoute = '/vehicle/customer-archive';
|
|||||||
const customerArchiveFormRoute = '/vehicle/customer-archive/form';
|
const customerArchiveFormRoute = '/vehicle/customer-archive/form';
|
||||||
const customerArchiveListRefreshKey = 'customer-archive-list-refresh';
|
const customerArchiveListRefreshKey = 'customer-archive-list-refresh';
|
||||||
/** 客商所属组织固定为顶层组织 */
|
/** 客商所属组织固定为顶层组织 */
|
||||||
const FIXED_OWNER_DEPT_NAME = '桂物物流集团';
|
const FIXED_OWNER_DEPT_NAME = '广西现代物流集团有限公司';
|
||||||
|
|
||||||
const createTimeRangeMap = {
|
const createTimeRangeMap = {
|
||||||
createTimeRange: ['createTimeStart', 'createTimeEnd'],
|
createTimeRange: ['createTimeStart', 'createTimeEnd'],
|
||||||
@@ -1617,23 +1645,13 @@ export default {
|
|||||||
const code = String(value || '')
|
const code = String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
.toUpperCase();
|
.toUpperCase();
|
||||||
const charset = '0123456789ABCDEFGHJKLMNPQRTUWXY';
|
|
||||||
const weights = [1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28];
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
callback();
|
callback();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (code.length !== 18 || ![...code].every(char => charset.includes(char))) {
|
const error = this.unifiedCreditCodeError(code);
|
||||||
callback(new Error('统一社会信用代码必须为18位有效字符'));
|
if (error) {
|
||||||
return;
|
callback(new Error(error));
|
||||||
}
|
|
||||||
const sum = [...code.slice(0, 17)].reduce(
|
|
||||||
(total, char, index) => total + charset.indexOf(char) * weights[index],
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const checkCode = charset[(31 - (sum % 31)) % 31];
|
|
||||||
if (code[17] !== checkCode) {
|
|
||||||
callback(new Error('统一社会信用代码校验码错误'));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
callback();
|
callback();
|
||||||
@@ -1664,6 +1682,9 @@ export default {
|
|||||||
originalAccessType: '',
|
originalAccessType: '',
|
||||||
activeTab: 'scores',
|
activeTab: 'scores',
|
||||||
archiveForm: this.emptyArchive(),
|
archiveForm: this.emptyArchive(),
|
||||||
|
kingdeeBlocked: false,
|
||||||
|
kingdeeQueryTimer: null,
|
||||||
|
kingdeeSyncingId: null,
|
||||||
currentScore: { details: [] },
|
currentScore: { details: [] },
|
||||||
scoreRecordIndex: -1,
|
scoreRecordIndex: -1,
|
||||||
scoreAttachmentFiles: [],
|
scoreAttachmentFiles: [],
|
||||||
@@ -1770,6 +1791,8 @@ export default {
|
|||||||
},
|
},
|
||||||
/** 业务字典 mk_template「开启MK」=1 */
|
/** 业务字典 mk_template「开启MK」=1 */
|
||||||
mkEnabled: false,
|
mkEnabled: false,
|
||||||
|
/** 是否已完成 MK 开关加载,避免提交时误判 */
|
||||||
|
_mkEnabledResolved: false,
|
||||||
formRules: {
|
formRules: {
|
||||||
customerCode: [{ max: 20, message: '客商编号最多20个字符', trigger: 'blur' }],
|
customerCode: [{ max: 20, message: '客商编号最多20个字符', trigger: 'blur' }],
|
||||||
fullName: [{ required: true, message: '请输入客商名称', trigger: 'blur' }],
|
fullName: [{ required: true, message: '请输入客商名称', trigger: 'blur' }],
|
||||||
@@ -1850,7 +1873,7 @@ export default {
|
|||||||
editBtn: false,
|
editBtn: false,
|
||||||
selection: true,
|
selection: true,
|
||||||
dialogClickModal: false,
|
dialogClickModal: false,
|
||||||
menuWidth: 320,
|
menuWidth: 400,
|
||||||
column: [
|
column: [
|
||||||
{
|
{
|
||||||
label: '客商编号',
|
label: '客商编号',
|
||||||
@@ -2006,6 +2029,12 @@ export default {
|
|||||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
minWidth: 170,
|
minWidth: 170,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '金蝶同步状态',
|
||||||
|
prop: 'kingdeeStatus',
|
||||||
|
slot: true,
|
||||||
|
minWidth: 110,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '状态',
|
label: '状态',
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
@@ -2014,7 +2043,6 @@ export default {
|
|||||||
search: true,
|
search: true,
|
||||||
searchOrder: 6,
|
searchOrder: 6,
|
||||||
searchValue: '',
|
searchValue: '',
|
||||||
dataType: 'number',
|
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
dicData: [
|
dicData: [
|
||||||
{ label: '全部', value: '' },
|
{ label: '全部', value: '' },
|
||||||
@@ -2090,6 +2118,9 @@ export default {
|
|||||||
const authority = this.userInfo.authority || '';
|
const authority = this.userInfo.authority || '';
|
||||||
return isAdministrator(this.userInfo);
|
return isAdministrator(this.userInfo);
|
||||||
},
|
},
|
||||||
|
kingdeeLocked() {
|
||||||
|
return this.archiveForm.kingdeeStatus === 'exist';
|
||||||
|
},
|
||||||
permissionList() {
|
permissionList() {
|
||||||
return {
|
return {
|
||||||
addBtn: false,
|
addBtn: false,
|
||||||
@@ -2295,6 +2326,9 @@ export default {
|
|||||||
this.onLoad(this.page, this.query);
|
this.onLoad(this.page, this.query);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'archiveForm.unifiedCreditCode'(val) {
|
||||||
|
this.handleKingdeeCreditCodeChange(val);
|
||||||
|
},
|
||||||
'archiveForm.registeredDetailAddress'() {
|
'archiveForm.registeredDetailAddress'() {
|
||||||
this.$nextTick(() => this.checkOverflow('registeredDetailInput', 'registeredDetailOverflow'));
|
this.$nextTick(() => this.checkOverflow('registeredDetailInput', 'registeredDetailOverflow'));
|
||||||
},
|
},
|
||||||
@@ -2307,7 +2341,13 @@ export default {
|
|||||||
sessionStorage.setItem(customerArchiveListRefreshKey, '1');
|
sessionStorage.setItem(customerArchiveListRefreshKey, '1');
|
||||||
},
|
},
|
||||||
hasPermission(code) {
|
hasPermission(code) {
|
||||||
return this.isAdmin || this.validData(this.permission[code], false);
|
if (this.isAdmin) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const permission = this.permission || {};
|
||||||
|
const value = permission[code];
|
||||||
|
// 不依赖 Avue validData,避免权限值为 true 以外的真值时被误判
|
||||||
|
return value === true || value === 'true' || value === 1 || value === '1';
|
||||||
},
|
},
|
||||||
mergeSelectOptions(target, values) {
|
mergeSelectOptions(target, values) {
|
||||||
const list = Array.isArray(values) ? values : values ? [values] : [];
|
const list = Array.isArray(values) ? values : values ? [values] : [];
|
||||||
@@ -2348,6 +2388,119 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
unifiedCreditCodeError(value) {
|
||||||
|
const code = String(value || '')
|
||||||
|
.trim()
|
||||||
|
.toUpperCase();
|
||||||
|
const charset = '0123456789ABCDEFGHJKLMNPQRTUWXY';
|
||||||
|
const weights = [1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28];
|
||||||
|
if (code.length !== 18 || ![...code].every(char => charset.includes(char))) {
|
||||||
|
return '统一社会信用代码必须为18位有效字符';
|
||||||
|
}
|
||||||
|
const sum = [...code.slice(0, 17)].reduce(
|
||||||
|
(total, char, index) => total + charset.indexOf(char) * weights[index],
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const checkCode = charset[(31 - (sum % 31)) % 31];
|
||||||
|
if (code[17] !== checkCode) {
|
||||||
|
return '统一社会信用代码校验码错误';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
resetKingdeeQueryState() {
|
||||||
|
this.kingdeeBlocked = false;
|
||||||
|
clearTimeout(this.kingdeeQueryTimer);
|
||||||
|
this.kingdeeQueryTimer = null;
|
||||||
|
},
|
||||||
|
// 新增态下重新输入统一社会信用代码后,此前金蝶回填的数据即失效,重置回填状态并允许对新代码重新查询
|
||||||
|
resetKingdeeBackfill() {
|
||||||
|
const form = this.archiveForm;
|
||||||
|
form.kingdeeStatus = 'none';
|
||||||
|
form.fullName = '';
|
||||||
|
form.legalPerson = '';
|
||||||
|
form.registeredCapital = '';
|
||||||
|
form.registeredDetailAddress = '';
|
||||||
|
form.businessScope = [];
|
||||||
|
form.businessTermType = '固定期限';
|
||||||
|
form.businessEndDate = '';
|
||||||
|
},
|
||||||
|
// 新增态下统一社会信用代码录入完成后自动查询金蝶(A902-1),防抖 500ms;
|
||||||
|
// 代码一经变化即重新查询(含改回历史代码),不做同码去重
|
||||||
|
handleKingdeeCreditCodeChange(val) {
|
||||||
|
this.kingdeeBlocked = false;
|
||||||
|
if (this.readonly || this.archiveForm.id) return;
|
||||||
|
if (this.archiveForm.kingdeeStatus === 'exist') {
|
||||||
|
this.resetKingdeeBackfill();
|
||||||
|
}
|
||||||
|
const code = String(val || '')
|
||||||
|
.trim()
|
||||||
|
.toUpperCase();
|
||||||
|
if (code.length !== 18 || this.unifiedCreditCodeError(code)) return;
|
||||||
|
clearTimeout(this.kingdeeQueryTimer);
|
||||||
|
this.kingdeeQueryTimer = setTimeout(() => this.fetchKingdeeCustomer(code), 500);
|
||||||
|
},
|
||||||
|
async fetchKingdeeCustomer(code) {
|
||||||
|
let customer = null;
|
||||||
|
try {
|
||||||
|
const res = await queryKingdeeCustomer(code);
|
||||||
|
customer = res?.data?.data || null;
|
||||||
|
} catch (error) {
|
||||||
|
this.$message.warning('金蝶查询失败,可继续填写,审批通过后系统将自动复查');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 防止响应返回时用户已修改代码
|
||||||
|
if (String(this.archiveForm.unifiedCreditCode || '').trim().toUpperCase() !== code) return;
|
||||||
|
// 金蝶不存在(rows 为空,data 为 null 或空对象):静默返回,正常填写,不做提示
|
||||||
|
if (!customer || !customer.status) return;
|
||||||
|
if (customer.status === 'C') {
|
||||||
|
this.applyKingdeeCustomer(customer);
|
||||||
|
this.$alert(
|
||||||
|
'金蝶已创建该客户,将获取金蝶的工商信息,相关字段已自动回填并锁定。如信用代码有误,可直接修改,系统将重新查询。',
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.kingdeeBlocked = true;
|
||||||
|
this.$alert('金蝶审核中,请稍后重新添加。', '提示', { confirmButtonText: '确定' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
applyKingdeeCustomer(customer) {
|
||||||
|
const form = this.archiveForm;
|
||||||
|
form.kingdeeStatus = 'exist';
|
||||||
|
// 工商信息以金蝶为准,直接覆盖(后续字段锁定)
|
||||||
|
if (customer.name) form.fullName = customer.name;
|
||||||
|
if (customer.artificialperson) form.legalPerson = customer.artificialperson;
|
||||||
|
if (customer.bizpartnerAddress) form.registeredDetailAddress = customer.bizpartnerAddress;
|
||||||
|
if (customer.regcapital) {
|
||||||
|
const capital = String(customer.regcapital).replace(/,/g, '').match(/\d+(?:\.\d+)?/);
|
||||||
|
if (capital) form.registeredCapital = capital[0];
|
||||||
|
}
|
||||||
|
if (customer.businessterm) this.applyBusinessTermRecognition(customer.businessterm);
|
||||||
|
if (customer.businessscope) {
|
||||||
|
const matchedScopes = this.businessScopeOptions
|
||||||
|
.filter(
|
||||||
|
item =>
|
||||||
|
customer.businessscope.includes(item.label) ||
|
||||||
|
customer.businessscope.includes(item.value)
|
||||||
|
)
|
||||||
|
.map(item => item.value);
|
||||||
|
if (matchedScopes.length) form.businessScope = matchedScopes;
|
||||||
|
}
|
||||||
|
if (!form.shortName && customer.simplename) form.shortName = customer.simplename;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
[
|
||||||
|
'unifiedCreditCode',
|
||||||
|
'fullName',
|
||||||
|
'legalPerson',
|
||||||
|
'registeredCapital',
|
||||||
|
'businessEndDate',
|
||||||
|
'businessScope',
|
||||||
|
'registeredDetailAddress',
|
||||||
|
].forEach(prop => this.$refs.archiveForm?.validateField(prop));
|
||||||
|
});
|
||||||
|
},
|
||||||
// 检测输入框文本是否溢出,仅溢出时才允许 tooltip 显示完整地址
|
// 检测输入框文本是否溢出,仅溢出时才允许 tooltip 显示完整地址
|
||||||
checkOverflow(refName, flag) {
|
checkOverflow(refName, flag) {
|
||||||
const inst = this.$refs[refName];
|
const inst = this.$refs[refName];
|
||||||
@@ -2377,6 +2530,9 @@ export default {
|
|||||||
invoices: [],
|
invoices: [],
|
||||||
scores: [],
|
scores: [],
|
||||||
changeRecords: [],
|
changeRecords: [],
|
||||||
|
kingdeeStatus: 'none',
|
||||||
|
kingdeeFailReason: '',
|
||||||
|
kingdeeSyncTime: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getDetailList(type) {
|
getDetailList(type) {
|
||||||
@@ -2951,12 +3107,15 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
initMkEnabled() {
|
initMkEnabled() {
|
||||||
|
this._mkEnabledResolved = false;
|
||||||
loadMkEnabled()
|
loadMkEnabled()
|
||||||
.then(enabled => {
|
.then(enabled => {
|
||||||
this.mkEnabled = !!enabled;
|
this.mkEnabled = !!enabled;
|
||||||
|
this._mkEnabledResolved = true;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.mkEnabled = false;
|
this.mkEnabled = false;
|
||||||
|
this._mkEnabledResolved = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
initScoreQuantificationOptions() {
|
initScoreQuantificationOptions() {
|
||||||
@@ -3056,7 +3215,7 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 客商所属组织固定为顶层「桂物物流集团」,表单不可编辑。
|
* 客商所属组织固定为顶层「广西现代物流集团有限公司」,表单不可编辑。
|
||||||
*/
|
*/
|
||||||
applyFixedOwnerDept() {
|
applyFixedOwnerDept() {
|
||||||
if (!this.isArchivePage || this.readonly) {
|
if (!this.isArchivePage || this.readonly) {
|
||||||
@@ -4134,6 +4293,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.archiveForm = this.emptyArchive();
|
this.archiveForm = this.emptyArchive();
|
||||||
this.originalAccessType = '';
|
this.originalAccessType = '';
|
||||||
|
this.resetKingdeeQueryState();
|
||||||
this.qualificationFiles = [];
|
this.qualificationFiles = [];
|
||||||
this.qualificationUploadFiles = [];
|
this.qualificationUploadFiles = [];
|
||||||
this.ocrQualificationUploadFiles = [];
|
this.ocrQualificationUploadFiles = [];
|
||||||
@@ -4143,6 +4303,7 @@ export default {
|
|||||||
},
|
},
|
||||||
resetArchive() {
|
resetArchive() {
|
||||||
this.readonly = false;
|
this.readonly = false;
|
||||||
|
this.resetKingdeeQueryState();
|
||||||
this.changeRecordDetailVisible = false;
|
this.changeRecordDetailVisible = false;
|
||||||
this.changeRecordDetail = null;
|
this.changeRecordDetail = null;
|
||||||
this.changeRecordDetailRows = [];
|
this.changeRecordDetailRows = [];
|
||||||
@@ -4266,6 +4427,10 @@ export default {
|
|||||||
return archive;
|
return archive;
|
||||||
},
|
},
|
||||||
saveArchive() {
|
saveArchive() {
|
||||||
|
if (this.kingdeeBlocked) {
|
||||||
|
this.$message.warning('金蝶审核中,请稍后重新添加');
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.$refs.archiveForm.validate(valid => {
|
this.$refs.archiveForm.validate(valid => {
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
this.$message.warning('请先完整填写必填项');
|
this.$message.warning('请先完整填写必填项');
|
||||||
@@ -4279,6 +4444,10 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
saveAndSubmitArchive() {
|
saveAndSubmitArchive() {
|
||||||
|
if (this.kingdeeBlocked) {
|
||||||
|
this.$message.warning('金蝶审核中,请稍后重新添加');
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.$refs.archiveForm.validate(valid => {
|
this.$refs.archiveForm.validate(valid => {
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
this.$message.warning('请先完整填写必填项');
|
this.$message.warning('请先完整填写必填项');
|
||||||
@@ -4322,15 +4491,34 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 列表 / 新增 / 编辑提交共用:开启 MK 时提交审核流,并更新客商审批状态
|
* 列表 / 新增 / 编辑提交共用:
|
||||||
|
* 未开启 MK 时仅走本地提交审批(与新增一致,不请求 MK 流程);
|
||||||
|
* 开启 MK 时先提交审核流,再更新客商审批状态。
|
||||||
*/
|
*/
|
||||||
submitCustomerApproval(id, fullName = '', approvalStatus = '') {
|
submitCustomerApproval(id, fullName = '', approvalStatus = '') {
|
||||||
return submitMkApprovalFlow({
|
const submitLocal = () => submitApproval(id);
|
||||||
bizType: 'customer-archive',
|
const run = enabled => {
|
||||||
bizId: id,
|
this.mkEnabled = !!enabled;
|
||||||
subjectName: fullName,
|
this._mkEnabledResolved = true;
|
||||||
approvalStatus,
|
if (!this.mkEnabled) {
|
||||||
}).then(() => submitApproval(id));
|
return submitLocal();
|
||||||
|
}
|
||||||
|
return submitMkApprovalFlow({
|
||||||
|
bizType: 'customer-archive',
|
||||||
|
bizId: id,
|
||||||
|
subjectName: fullName,
|
||||||
|
approvalStatus,
|
||||||
|
mkEnabled: true,
|
||||||
|
}).then(submitLocal);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 与列表按钮一致:以页面 mkEnabled 为准,未开启时不请求 MK
|
||||||
|
if (this._mkEnabledResolved) {
|
||||||
|
return run(this.mkEnabled);
|
||||||
|
}
|
||||||
|
return loadMkEnabled()
|
||||||
|
.then(enabled => run(enabled))
|
||||||
|
.catch(() => run(false));
|
||||||
},
|
},
|
||||||
openReviewScore() {
|
openReviewScore() {
|
||||||
const scores = this.archiveForm.scores || [];
|
const scores = this.archiveForm.scores || [];
|
||||||
@@ -5035,17 +5223,47 @@ export default {
|
|||||||
this.$message.error('获取审核跳转地址失败');
|
this.$message.error('获取审核跳转地址失败');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleApprove(row) {
|
async handleApprove(row) {
|
||||||
this.$confirm('是否确认审核通过?', '提示', {
|
await this.$confirm('是否确认审核通过?', '提示', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
})
|
});
|
||||||
.then(() => approve(row.id))
|
const res = await approve(row.id);
|
||||||
.then(() => {
|
// data=false 表示审核已通过但推送金蝶失败,msg 带具体原因
|
||||||
this.onLoad(this.page);
|
if (res.data.data === false) {
|
||||||
this.$message({ type: 'success', message: '审核通过!' });
|
this.$message.warning(res.data.msg);
|
||||||
|
} else {
|
||||||
|
this.$message.success(res.data.msg || '审核通过!');
|
||||||
|
}
|
||||||
|
this.onLoad(this.page);
|
||||||
|
},
|
||||||
|
async handleSyncKingdee(row) {
|
||||||
|
if (this.kingdeeSyncingId) return;
|
||||||
|
try {
|
||||||
|
await this.$confirm('是否确认推送金蝶?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
});
|
});
|
||||||
|
} catch (exception) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.kingdeeSyncingId = row.id;
|
||||||
|
try {
|
||||||
|
const res = await syncKingdee(row.id);
|
||||||
|
const archive = res?.data?.data || {};
|
||||||
|
if (archive.kingdeeStatus === 'synced') {
|
||||||
|
this.$message.success('推送成功,金蝶客户编码与客商编号一致');
|
||||||
|
} else if (archive.kingdeeStatus === 'exist') {
|
||||||
|
this.$message.info('金蝶已有该客商档案,无需推送');
|
||||||
|
} else {
|
||||||
|
this.$message.warning(`推送失败:${archive.kingdeeFailReason || '未知原因'}`);
|
||||||
|
}
|
||||||
|
this.onLoad(this.page);
|
||||||
|
} finally {
|
||||||
|
this.kingdeeSyncingId = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
handleReject(row) {
|
handleReject(row) {
|
||||||
this.$confirm('是否确认审核不通过?', '提示', {
|
this.$confirm('是否确认审核不通过?', '提示', {
|
||||||
@@ -5078,19 +5296,9 @@ export default {
|
|||||||
this.onLoad(this.page);
|
this.onLoad(this.page);
|
||||||
},
|
},
|
||||||
searchChange(params, done) {
|
searchChange(params, done) {
|
||||||
// 搜索区所属组织级联返回的是 ID 路径数组,取末级 ID 转成部门名称传给后端(与列表接口 deptName 字段一致)
|
|
||||||
if (Array.isArray(params.deptName)) {
|
|
||||||
const path = params.deptName.filter(
|
|
||||||
item => item !== undefined && item !== null && item !== ''
|
|
||||||
);
|
|
||||||
const deptId = path.length ? path[path.length - 1] : '';
|
|
||||||
params.deptName = deptId
|
|
||||||
? this.deptOptions.find(item => String(item.value) === String(deptId))?.rawLabel || ''
|
|
||||||
: '';
|
|
||||||
}
|
|
||||||
this.query = this.buildQuery(params);
|
this.query = this.buildQuery(params);
|
||||||
this.page.currentPage = 1;
|
this.page.currentPage = 1;
|
||||||
this.onLoad(this.page);
|
this.onLoad(this.page, this.query);
|
||||||
done();
|
done();
|
||||||
},
|
},
|
||||||
selectionChange(list) {
|
selectionChange(list) {
|
||||||
@@ -5151,7 +5359,38 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
buildQuery(params = {}) {
|
buildQuery(params = {}) {
|
||||||
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
|
// 新条件覆盖旧条件;本次显式清空的字段必须从查询里去掉,避免第二次搜索仍带着上一次的客商名称。
|
||||||
|
const merged = { ...this.query, ...params };
|
||||||
|
Object.keys(params).forEach(key => {
|
||||||
|
const value = params[key];
|
||||||
|
if (
|
||||||
|
value === '' ||
|
||||||
|
value === null ||
|
||||||
|
value === undefined ||
|
||||||
|
(Array.isArray(value) && value.length === 0)
|
||||||
|
) {
|
||||||
|
delete merged[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (Array.isArray(merged.deptName)) {
|
||||||
|
const path = merged.deptName.filter(
|
||||||
|
item => item !== undefined && item !== null && item !== ''
|
||||||
|
);
|
||||||
|
const deptId = path.length ? path[path.length - 1] : '';
|
||||||
|
merged.deptName = deptId
|
||||||
|
? this.deptOptions.find(item => String(item.value) === String(deptId))?.rawLabel || ''
|
||||||
|
: '';
|
||||||
|
if (!merged.deptName) {
|
||||||
|
delete merged.deptName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const values = normalizeSearchRangeParams(merged, createTimeRangeMap);
|
||||||
|
// 状态下拉「全部」会被数字类型转成 0/-1,二次搜索会把列表滤空。
|
||||||
|
const status = values.status;
|
||||||
|
if (status === 0 || status === '0' || status === -1 || status === '-1') {
|
||||||
|
delete values.status;
|
||||||
|
}
|
||||||
|
return values;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ import { downloadXls } from '@/utils/util';
|
|||||||
import { handleImportExcel } from '@/utils/import-excel';
|
import { handleImportExcel } from '@/utils/import-excel';
|
||||||
import { getToken, isAdministrator } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
import { getUploadHeaders } from '@/utils/upload';
|
import { getUploadHeaders } from '@/utils/upload';
|
||||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
import { applyIncomingSearch, normalizeSearchRangeParams } from '@/utils/search-range';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { ElLoading } from 'element-plus';
|
import { ElLoading } from 'element-plus';
|
||||||
import { excelOption, exportColumns, option } from '@/option/vehicle/insurance-record';
|
import { excelOption, exportColumns, option } from '@/option/vehicle/insurance-record';
|
||||||
@@ -409,7 +409,7 @@ export default {
|
|||||||
: integerPart;
|
: integerPart;
|
||||||
},
|
},
|
||||||
buildQuery(params = {}) {
|
buildQuery(params = {}) {
|
||||||
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
|
return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
|
||||||
},
|
},
|
||||||
validateRow(row) {
|
validateRow(row) {
|
||||||
if (row.startDate && row.endDate && row.endDate < row.startDate) {
|
if (row.startDate && row.endDate && row.endDate < row.startDate) {
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ import { downloadXls } from '@/utils/util';
|
|||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
import { getToken, isAdministrator } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
import { applyIncomingSearch, normalizeSearchRangeParams } from '@/utils/search-range';
|
||||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
@@ -903,7 +903,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
buildQuery(params = {}) {
|
buildQuery(params = {}) {
|
||||||
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
|
return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
|
||||||
},
|
},
|
||||||
handleTemplate() {
|
handleTemplate() {
|
||||||
exportBlob(
|
exportBlob(
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ import { downloadXls } from '@/utils/util';
|
|||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
import { getToken, isAdministrator } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
import { applyIncomingSearch, normalizeSearchRangeParams } from '@/utils/search-range';
|
||||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
@@ -371,9 +371,8 @@ export default {
|
|||||||
placeholder: '日期时间选择器',
|
placeholder: '日期时间选择器',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '里程/航程数(公里)',
|
label: '里程/航程数',
|
||||||
prop: 'mileage',
|
prop: 'mileage',
|
||||||
renderHeader: () => '里程/航程数',
|
|
||||||
type: 'input',
|
type: 'input',
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
slot: true,
|
slot: true,
|
||||||
@@ -391,6 +390,7 @@ export default {
|
|||||||
],
|
],
|
||||||
value: '公里',
|
value: '公里',
|
||||||
hide: true,
|
hide: true,
|
||||||
|
display: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '备注',
|
label: '备注',
|
||||||
@@ -565,12 +565,10 @@ export default {
|
|||||||
const mileageUnit = vehicleType === '船舶' ? '海里' : '公里';
|
const mileageUnit = vehicleType === '船舶' ? '海里' : '公里';
|
||||||
const vehicleTypeColumn = this.findColumn(this.option.column, 'vehicleType');
|
const vehicleTypeColumn = this.findColumn(this.option.column, 'vehicleType');
|
||||||
const vehicleNoColumn = this.findColumn(this.option.column, 'vehicleNo');
|
const vehicleNoColumn = this.findColumn(this.option.column, 'vehicleNo');
|
||||||
const mileageColumn = this.findColumn(this.option.column, 'mileage');
|
|
||||||
const mileageUnitColumn = this.findColumn(this.option.column, 'mileageUnit');
|
const mileageUnitColumn = this.findColumn(this.option.column, 'mileageUnit');
|
||||||
vehicleNoColumn.placeholder =
|
vehicleNoColumn.placeholder =
|
||||||
vehicleType === '船舶' ? '请选择船号' : '请选择车牌号';
|
vehicleType === '船舶' ? '请选择船号' : '请选择车牌号';
|
||||||
vehicleTypeColumn.disabled = Boolean(this.form.id);
|
vehicleTypeColumn.disabled = Boolean(this.form.id);
|
||||||
mileageColumn.label = `里程/航程数(${mileageUnit})`;
|
|
||||||
mileageUnitColumn.value = mileageUnit;
|
mileageUnitColumn.value = mileageUnit;
|
||||||
this.form.mileageUnit = mileageUnit;
|
this.form.mileageUnit = mileageUnit;
|
||||||
if (!this.isDetailLoading && oldValue && vehicleType !== oldValue) {
|
if (!this.isDetailLoading && oldValue && vehicleType !== oldValue) {
|
||||||
@@ -833,7 +831,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
buildQuery(params = {}) {
|
buildQuery(params = {}) {
|
||||||
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
|
return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
|
||||||
},
|
},
|
||||||
handleTemplate() {
|
handleTemplate() {
|
||||||
exportBlob(
|
exportBlob(
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ import { exportBlob } from '@/api/common';
|
|||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { getToken, isAdministrator } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
import { applyIncomingSearch, normalizeSearchRangeParams } from '@/utils/search-range';
|
||||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { excelOption, exportColumns, option } from '@/option/vehicle/mileage-record';
|
import { excelOption, exportColumns, option } from '@/option/vehicle/mileage-record';
|
||||||
@@ -607,7 +607,7 @@ export default {
|
|||||||
},
|
},
|
||||||
buildQuery(params = {}) {
|
buildQuery(params = {}) {
|
||||||
return normalizeSearchRangeParams(
|
return normalizeSearchRangeParams(
|
||||||
{ ...params, ...this.query, vehicleType: '车辆' },
|
{ ...applyIncomingSearch(this.query, params), vehicleType: '车辆' },
|
||||||
createTimeRangeMap
|
createTimeRangeMap
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ import { exportBlob } from '@/api/common';
|
|||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { getToken, isAdministrator } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
import { applyIncomingSearch, normalizeSearchRangeParams } from '@/utils/search-range';
|
||||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { excelOption, exportColumns, option } from '@/option/vehicle/tire-replacement-record';
|
import { excelOption, exportColumns, option } from '@/option/vehicle/tire-replacement-record';
|
||||||
@@ -500,7 +500,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
buildQuery(params = {}) {
|
buildQuery(params = {}) {
|
||||||
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
|
return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
|
||||||
},
|
},
|
||||||
handleTemplate() {
|
handleTemplate() {
|
||||||
exportBlob(
|
exportBlob(
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ import { exportBlob } from '@/api/common';
|
|||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { getToken, isAdministrator } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
import { applyIncomingSearch, normalizeSearchRangeParams } from '@/utils/search-range';
|
||||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { excelOption, exportColumns, option } from '@/option/vehicle/transport-change-record';
|
import { excelOption, exportColumns, option } from '@/option/vehicle/transport-change-record';
|
||||||
@@ -478,7 +478,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
buildQuery(params = {}) {
|
buildQuery(params = {}) {
|
||||||
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
|
return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
|
||||||
},
|
},
|
||||||
handleTemplate() {
|
handleTemplate() {
|
||||||
exportBlob(
|
exportBlob(
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ import { exportBlob } from '@/api/common';
|
|||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { getToken, isAdministrator } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
import { applyIncomingSearch, normalizeSearchRangeParams } from '@/utils/search-range';
|
||||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
@@ -673,7 +673,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
buildQuery(params = {}) {
|
buildQuery(params = {}) {
|
||||||
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
|
return normalizeSearchRangeParams(applyIncomingSearch(this.query, params), createTimeRangeMap);
|
||||||
},
|
},
|
||||||
handleTemplate() {
|
handleTemplate() {
|
||||||
exportBlob(
|
exportBlob(
|
||||||
|
|||||||
+17
-15
@@ -18,6 +18,8 @@ export default ({ mode, command }) => {
|
|||||||
target: 'esnext',
|
target: 'esnext',
|
||||||
minify: isProd ? 'terser' : 'esbuild', // 根据环境选择压缩工具
|
minify: isProd ? 'terser' : 'esbuild', // 根据环境选择压缩工具
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
|
// @napi-rs/canvas 是 Node 原生模块(@open-file-viewer 传递依赖),浏览器 bundle 打不了,跳过
|
||||||
|
external: [/^@napi-rs\//],
|
||||||
output: {
|
output: {
|
||||||
assetFileNames,
|
assetFileNames,
|
||||||
},
|
},
|
||||||
@@ -47,28 +49,28 @@ export default ({ mode, command }) => {
|
|||||||
__VUE_I18N_LEGACY_API__: true,
|
__VUE_I18N_LEGACY_API__: true,
|
||||||
__INTLIFY_PROD_DEVTOOLS__: false,
|
__INTLIFY_PROD_DEVTOOLS__: false,
|
||||||
},
|
},
|
||||||
// server: {
|
|
||||||
// port: 2888,
|
|
||||||
// proxy: {
|
|
||||||
// '/api': {
|
|
||||||
// // 本地网关默认 80 端口(blade-gateway bootstrap.yml);开发时需先启动网关及 blade-auth/blade-system 等服务
|
|
||||||
// // 注意:本机 8080 一般是 Nacos,不是业务网关
|
|
||||||
// target: 'http://localhost',
|
|
||||||
// // target: 'http://172.16.203.228:8000', // 仅联调远程网关时启用(且不要 rewrite)
|
|
||||||
// changeOrigin: true,
|
|
||||||
// rewrite: path => path.replace(/^\/api/, ''),
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
server: {
|
server: {
|
||||||
port: 2889,
|
port: 2888,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://172.16.203.221:8000',
|
// 本地网关默认 80 端口(blade-gateway bootstrap.yml);开发时需先启动网关及 blade-auth/blade-system 等服务
|
||||||
|
// 注意:本机 8080 一般是 Nacos,不是业务网关
|
||||||
|
target: 'http://localhost',
|
||||||
|
// target: 'http://172.16.203.228:8000', // 仅联调远程网关时启用(且不要 rewrite)
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
rewrite: path => path.replace(/^\/api/, ''),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// server: {
|
||||||
|
// port: 2889,
|
||||||
|
// proxy: {
|
||||||
|
// '/api': {
|
||||||
|
// target: 'http://172.16.203.228:8000',
|
||||||
|
// changeOrigin: true,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'~': resolve(__dirname, './'),
|
'~': resolve(__dirname, './'),
|
||||||
|
|||||||
Reference in New Issue
Block a user