Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a554ccf38e | |||
| a748e52551 | |||
| e89e78b527 | |||
| 2a03991297 | |||
| 58aaf79445 | |||
| f067518c0e | |||
| 11460f3378 | |||
| 7f543937fb | |||
| 6a87066097 | |||
| 79aa4f9fae | |||
| f1e2313e4c | |||
| acbc9d3322 | |||
| dbeb174ae2 | |||
| 764c0f4581 | |||
| 1a9cd55d66 | |||
| 38e53e01af | |||
| 6ff672211f | |||
| 1bbc97758d | |||
| c1813e308f | |||
| 716ce43862 | |||
| 14c6ad0a31 | |||
| 1972115173 | |||
| 006dc95bf0 | |||
| d187da68d2 | |||
| 70bb94a2e6 | |||
| f78e61b433 | |||
| 508d73fc0d | |||
| e73e1da877 | |||
| 4d5b99da82 | |||
| f3021d4bae | |||
| 7eb6cce2db | |||
| be2888d428 | |||
| 06f6950434 | |||
| 09f1af9a0a | |||
| 6d4961f404 | |||
| cba2ad3b3b | |||
| a7315b4491 | |||
| 0d070d4b34 | |||
| 6a6d10dc79 | |||
| 3bac9b6472 | |||
| 96f2fa0eb5 | |||
| e789f1647a | |||
| c63c7c3e33 | |||
| 23b23dc8e5 | |||
| a0a3d53927 | |||
| 42e4925fd6 | |||
| 024e77801e | |||
| 397490a165 | |||
| 9e14f31604 | |||
| 40925cb21f | |||
| 6f0f1842cb | |||
| 685a37b2df | |||
| b83e3c358d | |||
| 94da08e7a7 | |||
| 008ec7a6a8 | |||
| 429b5548dd | |||
| 6d4434f5c9 | |||
| 4ffceb0413 | |||
| 6eff4b7383 | |||
| a8986690bb | |||
| 87ec01c8dd | |||
| f25e38d67e | |||
| 8a0db99572 | |||
| 9c2a2ee6e6 | |||
| 252df72f87 | |||
| 80a15fc626 | |||
| f858eb2bcd |
@@ -0,0 +1,187 @@
|
|||||||
|
# 2026-09-02 工作日志
|
||||||
|
|
||||||
|
## 合同/运力页「到期快捷筛选」标签统一改为独立一行
|
||||||
|
- 需求:把 avue-crud 搜索栏 `#search-menu` 里的 `<el-check-tag>` 到期快捷筛选组,移出搜索栏、单独成一行(搜索卡下方、表格上方),不加卡片包裹。
|
||||||
|
- 实现:改用 avue-crud 的 `#header` 插槽承载该组;CSS 由 `order:-1; margin-right:12px; min-width:0`(搜索按钮行内联)改为 `margin-bottom:12px`(独立行)。
|
||||||
|
- 涉及文件(4 处,均已改):
|
||||||
|
- `src/views/business/contract-manage.vue`(首改,用 `.contract-manage-page__expiry-search`)
|
||||||
|
- `src/views/transportCapacity/vehicle.vue`(`.transport-vehicle-page__expiry-tags`)
|
||||||
|
- `src/views/transportCapacity/driver.vue`(`.driver-page__expiry-tags`)
|
||||||
|
- `src/views/transportCapacity/ship.vue`(`.transport-ship-page__expiry-tags`)
|
||||||
|
- 各页变量名保留原样(contract-manage 用 contractExpiryScope/changeExpiryScope/contractExpiryStats;运力页用 query.expireStatus/handleExpireChange/expiryStat)。
|
||||||
|
- 已补改并修正:`payment/bill-ledger` 页。用户要求"放在卡片下面"(搜索白卡之外、表格之上独立一行),非卡片内分行。最终方案:把到期快捷筛选整组(标签 DOM + `shortcutOptions` 计算属性 + `handleShortcutChange`)从子组件 `bill-ledger-search.vue` 提升到页面 `bill-ledger.vue`,置于 `<bill-ledger-search>` 与 `<bill-ledger-table>` 之间(卡片下、表格上)。
|
||||||
|
- `bill-ledger-search.vue`:删除标签 DOM、`.bill-ledger-search__actions`/`.bill-ledger-search__actions-tags` CSS、`counts` prop、`shortcutOptions`/`handleShortcutChange`;按钮行 `.bill-ledger-search__actions-buttons` 恢复单行右对齐(`margin-bottom:8px`)。
|
||||||
|
- `bill-ledger.vue`:模板插入 `<div class="bill-ledger-page__expiry-tags">`(绑定 `query.expiryShortcut`,@change 调 `handleShortcutChange`→`handleSearch`);脚本加 `shortcutOptions` computed(用 `counts`)+ `handleShortcutChange` method;新增 `<style scoped>` 写入 `.bill-ledger-page__expiry-tags`(flex 行、`margin:8px 0`)。`emptyQuery()` 已含 `expiryShortcut:'all'`,无回归。
|
||||||
|
- 选中态样式统一:去掉 `.bill-ledger-page__expiry-tags :deep(.el-check-tag.is-checked)` 的 `background-color:#409eff` + `color:#fff` 蓝底白字覆盖,改用 Element Plus 默认浅蓝底深蓝字,与合同页一致。
|
||||||
|
|
||||||
|
## transportCapacity/ship 新增弹窗 input 宽度统一
|
||||||
|
- 问题:新增/修改弹窗内输入控件宽度不一致(文本框明显窄于下拉框、日期框)。
|
||||||
|
- 根因:`src/views/transportCapacity/ship.vue` 末尾 scoped 样式中独有 `.el-input__wrapper{width:150px !important}`,把文本框包裹层强制压成 150px,而 `el-select`(用 `.el-select__wrapper`)、日期/数字框不受影响 → 文本框与下拉框宽度不齐。同模块的 driver.vue / vehicle.vue 均无此规则。
|
||||||
|
- 修复:
|
||||||
|
1. 删除 `.el-input__wrapper{width:150px!important}` 异常规则。
|
||||||
|
2. 增强 `.ship-form` 内输入控件的 100% 宽度覆盖,补齐 `.el-select__wrapper` / `.el-cascader` / `.el-input__wrapper`,使文本框/下拉/日期/数字框均 100% 填满所在栅格列。
|
||||||
|
- 说明:弹窗仍保留不同栅格列宽(span6/8/12/16)的密集布局,同行字段已等宽;若需跨行全部 input 绝对像素等宽(统一栅格列宽),需进一步重排。
|
||||||
|
- 二次排查(用户反馈仍不一致):把 `.ship-form` 内控件 100% 覆盖从通用 `:deep(.el-input)` 改为更具体的 `:deep(.el-form-item__content > .el-input)` 并加 `!important`,同步覆盖 `.el-input__wrapper`/`.el-select__wrapper`,确保不会被 Element Plus 变量或更具体选择器压回默认/较窄宽度。请用户刷新浏览器验证。
|
||||||
|
- 三次修复(用户反馈第一行 span=6+6+12 三个字段宽度仍不齐):放弃「width: 100% 撑满 col」方案,改用「对齐 vehicle/customer-archive 的 `.archive-form` 风格」——label 固定 `calc(6em + 24px)` + 控件统一 `250px + max-width: 100%`。根因:`el-cascader` 默认 `display: inline-block`,即使 width: 100% 也不易撑满 col-content(与 input 视觉差 ~150px vs ~280px);span=6+6+12 下两列 form-item__content 实际不等宽(label-width=100 被长 label 撑开)。新方案固定 250px 后三字段视觉一致,参照客商档案。
|
||||||
|
- 同时清理模板中冗余 `label-width="88px"`(会被新 CSS 强制覆盖失效)。
|
||||||
|
|
||||||
|
## transportCapacity/ship 证书信息分组:附件下放到对应分组末尾 + label 缩短
|
||||||
|
- 需求:1. 「船舶国籍证书」「内河船舶最低安全配员证书」分组里 label 「证书有效期自」偏长,改短为「有效期自」(与下面统一)。2. 底部集中的 `.cert-upload-grid` 上传区调整到各证书分组末尾,让附件与字段同组。
|
||||||
|
- 修改:
|
||||||
|
- `src/views/transportCapacity/ship.vue` 模板 line 392/431:2 处 `label="证书有效期自"` → `label="有效期自"`。
|
||||||
|
- 模板结构重组:删除底部 `<el-row class="cert-upload-grid">...</el-row>`(原 line 472-548),把 6 个 `<image-upload-field>` 分别插入对应 `cert-block` 末尾(每个用 `class="ship-upload-item"`,独立 `<el-row :gutter="18"><el-col :span="8">`):
|
||||||
|
- 船舶所有权证书分组 → `ownershipCertImage`
|
||||||
|
- 内河船舶安全与环保证书分组 → `safetyCertImage`
|
||||||
|
- 船舶国籍证书分组 → `nationalityCertImage`
|
||||||
|
- 内河船舶最低安全配员证书分组 → `safeManningCertImage`
|
||||||
|
- 光船租赁登记证明书分组 → `leaseContractImage`(label="光船租赁登记证书")
|
||||||
|
- 船舶营业运输证书分组 → `businessTransportCertImage`(label="船舶营业运输证")
|
||||||
|
- 样式清理:删除无用的 `.cert-upload-grid { padding-top:8px }` / `.cert-upload-spacer { height:1px }`;保留 `.cert-block` / `.cert-block__title` / `.ship-upload-item`(`margin-bottom:14px`)。
|
||||||
|
- 验证:用户刷新浏览器看新布局。
|
||||||
|
|
||||||
|
## transportCapacity/ship 组合控件宽度对齐 250px 体系
|
||||||
|
- 问题:基础船舶信息里「安放龙骨日期/建造完工日期」的起止两个 date-picker、「航区」的 select 都是 `flex:1` 撑满所在 span=12 整列(约 498px),明显比单字段 250px 宽,视觉不协调。
|
||||||
|
- 修复(`src/views/transportCapacity/ship.vue` 末尾 scoped 样式):
|
||||||
|
- `.date-range-inline :deep(.el-date-editor)` 由 `flex:1; min-width:0` 增加 `max-width: 250px` → 两个 date-picker 在窗够宽时各对齐 250px、彼此等宽,窄窗时 flex:1 收缩不溢出。
|
||||||
|
- `.navigation-area-field :deep(.el-select)` 由 `flex:1; min-width:0` 增加 `max-width: 250px` → 航区 select 对齐 250px 单字段。
|
||||||
|
- 说明:`flex:1` 保底防溢出,`max-width:250px` 把组合控件拉回与单字段一致的 250px 体系(受所在列宽限制,安放龙骨 span=12 在常规窗宽下两个 date-picker 约 226px 等宽,更宽窗自动到 250px)。
|
||||||
|
|
||||||
|
## business/project-apply.vue 表单「业务部门/承办部门」级联选择器宽度统一
|
||||||
|
- 问题:项目新增/编辑表单(含独立表单页 `/business/project-apply/form`)中,业务部门、承办部门两个 `el-cascader` 的按钮宽度与同行的 select/input 不一致。
|
||||||
|
- 根因:scoped 样式里强制 100% 宽度的控件列表(`el-input` / `el-select` / `el-tree-select` / `el-date-editor`)漏了 `el-cascader`,导致级联选择器按内容宽度收缩,未填满栅格列。
|
||||||
|
- 修复:`src/views/business/project-apply.vue` 末尾 scoped 样式 `.project-apply-form` 的 100% 宽度规则补上 `:deep(.el-cascader)`。Vite HMR 已自动生效,无需重启 dev。
|
||||||
|
|
||||||
|
## 登录页隐藏租户栏
|
||||||
|
- 需求:登录页 `000000` 租户输入框隐藏。
|
||||||
|
- 根因:`src/page/login/userlogin.vue` 第 10 行 `el-form-item` 由 `tenantMode` 控制显示,初始值 `this.website.tenantMode`,`getTenant()` 请求 `/api/system/tenant/info` 成功时会把 `tenantMode=false` 自动隐藏并回填 `loginForm.tenantId`。截图里仍显示租户栏,说明该接口未正常返回(域名解析/后端/跨域),沿用了初始 `tenantMode`。
|
||||||
|
- 修复:
|
||||||
|
1. 删除 `userlogin.vue` 模板第 10-21 行整段租户 `el-form-item`(`v-if="tenantMode" prop="tenantId"`),不留 `v-if` 兜底,直接移除 DOM。
|
||||||
|
2. 同步删除 `loginRules.tenantId` 校验规则。
|
||||||
|
- 保留:`loginForm.tenantId: '000000'` 默认值、`getTenant()` 方法。提交登录时仍带 `tenantId` 字段,后端行为不变;接口正常时仍会用返回的 tenantId 覆盖默认值。
|
||||||
|
- 后续若要重新启用:恢复 `el-form-item` 即可(或改 `data()` 中 `tenantMode: this.website.tenantMode` 走配置开关)。
|
||||||
|
|
||||||
|
## 客商档案评分详情表控件/label 宽度对齐
|
||||||
|
- 问题:评分详情表弹窗内输入框、选择框、日期框宽度不一致;label 要求固定 88px 且超长换行。
|
||||||
|
- 修复(`src/views/vehicle/customer-archive.vue`):
|
||||||
|
- 模板 `label-width="88"` 改为 `label-width="88px"`。
|
||||||
|
- `.score-detail-form` 样式:
|
||||||
|
- `el-form-item__label` 强制 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important`,保留 `white-space: normal !important; word-break: break-all; line-height: 1.4` 实现超长换行。
|
||||||
|
- `el-form-item__content > .el-input / .el-select / .el-date-editor` 由固定 `width: 150px` 改为 `width: 100%`,单字段撑满等宽;`.score-detail-form__range` 内双日期框保持 `flex: 1; min-width: 0` 均分。
|
||||||
|
- 参考:客商档案 `.archive-form` 的 label 换行与控件等宽处理思路。
|
||||||
|
|
||||||
|
## 评分详情表弹窗输入框/选择框宽度对齐 + label 88px 换行
|
||||||
|
- 问题:`src/views/vehicle/customer-archive.vue` 中「评分详情表」弹窗表单的输入框、选择框、日期框视觉宽度不一致;label 区域未严格固定 88px。
|
||||||
|
- 修复:
|
||||||
|
1. `el-form` 的 `label-width="88"` 改为 `label-width="88px"`。
|
||||||
|
2. `.score-detail-form` 中 `.el-form-item__label` 强制 `flex:0 0 88px; width:88px; min-width:88px` 并保留 `white-space: normal` 换行。
|
||||||
|
3. 将单字段控件(`.el-form-item__content > .el-input/.el-select/.el-date-editor`)宽度由固定 `150px` 改为 `100%`,使其在固定 label 后自动等宽填满 content 区域。
|
||||||
|
- 保留:「临时额度申请使用期限」组合控件继续用 `.score-detail-form__range` 双 date-picker flex 均分布局。
|
||||||
|
|
||||||
|
|
||||||
|
## customer-archive 评分详情表:label 固定 88px 换行 + 控件宽度统一
|
||||||
|
- 问题:评分详情表弹窗内输入框与选择框宽度不一致,且 label 区域未按 88px 固定换行。
|
||||||
|
- 修复(`src/views/vehicle/customer-archive.vue`):
|
||||||
|
1. `score-detail-form` 的 `label-width="88"` 改为 `label-width="88px"`。
|
||||||
|
2. `.score-detail-form :deep(.el-form-item__label)` 强制 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important;`,并保留 `white-space: normal !important; word-break: break-all; line-height: 1.4;` 实现长 label 自动换行。
|
||||||
|
3. 单字段控件(`.el-form-item__content > .el-input / .el-select / .el-date-editor`)统一 `width: 100%`,使输入框与选择框在相同栅格列内等宽。
|
||||||
|
- 说明:「临时额度申请使用期限」双日期组合仍由 `.score-detail-form__range` 控制 `flex:1` 均分,不受单字段 100% 影响。
|
||||||
|
|
||||||
|
## 评分详情表 label 宽度与控件宽度对齐
|
||||||
|
- 需求:评分详情弹窗内输入框与选择框宽度一致;label 宽度固定 88px,超长自动换行。
|
||||||
|
- 修改 `src/views/vehicle/customer-archive.vue`:
|
||||||
|
1. `<el-form>` 的 `label-width="88"` 改为 `label-width="88px"`。
|
||||||
|
2. `.score-detail-form` 的 label 样式强制 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important;`,并保留 `white-space: normal !important; word-break: break-all; line-height: 1.4;` 实现换行。
|
||||||
|
3. 单字段控件(`.el-form-item__content > .el-input/.el-select/.el-date-editor`)由固定 `width: 150px` 改为 `width: 100%`,使输入框、下拉框、日期框均撑满同一 content 区,视觉宽度一致。
|
||||||
|
- 说明:「临时额度申请使用期限」两个 date-picker 仍由 `.score-detail-form__range` 的 `flex: 1; min-width: 0;` 控制,不受本次单字段 100% 影响。
|
||||||
|
|
||||||
|
## 客商档案「评分详情表」弹窗表单控件宽度对齐
|
||||||
|
- 问题:`src/views/vehicle/customer-archive.vue` 的「评分详情表」弹窗中,评定日期的 date-picker、拟申请额度等 input、评分量化表 select 等控件宽度不一致;label 需固定 88px 且长 label 换行。
|
||||||
|
- 修复:
|
||||||
|
1. `<el-form>` `label-width="88"` 改为 `label-width="88px"`。
|
||||||
|
2. `.score-detail-form` 样式:`.el-form-item__label` 强制 `flex:0 0 88px` + `width:88px` + `white-space:normal`,确保长 label 换行且宽度恒定。
|
||||||
|
3. 控件由固定 `width:150px` 改为 `.el-form-item__content > .el-input/.el-select/.el-date-editor { width:100% }`,单字段 input/select/date-picker 在 span=6 下等宽撑满 content 区。
|
||||||
|
- 备注:「临时额度申请使用期限」组合区保持 `.score-detail-form__range` 的 `flex:1` 双 date-picker 布局,不受单字段 100% 影响。
|
||||||
|
|
||||||
|
## customer-archive 评分详情表控件宽度与 label 换行对齐
|
||||||
|
- 问题:评分详情弹窗内 input、select、date-picker 宽度不一致;长 label(如「拟申请总资金使用额度(万元)」)未按 88px 换行。
|
||||||
|
- 修改 `src/views/vehicle/customer-archive.vue`:
|
||||||
|
1. `<el-form label-width="88">` → `label-width="88px"`,与全站风格一致。
|
||||||
|
2. `.score-detail-form` 样式:
|
||||||
|
- `el-form-item__label` 强制 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important;`,并保留 `white-space: normal` + `word-break: break-all` + `line-height: 1.4` 实现超长 label 换行。
|
||||||
|
- 单字段控件(`.el-form-item__content > .el-input/.el-select/.el-date-editor`)由固定 `width: 150px` 改为 `width: 100%`,使其在 88px label 下等宽撑满 content 区。
|
||||||
|
- 说明:双日期范围字段(临时额度申请使用期限)仍由 `.score-detail-form__range` 用 `flex:1; min-width:0` 均分,不受单字段 100% 规则影响。
|
||||||
|
|
||||||
|
## 评分详情表弹窗 label/控件宽度对齐
|
||||||
|
- 文件:`src/views/vehicle/customer-archive.vue`。
|
||||||
|
- 问题:评分详情表弹窗内 input 与 select/date-picker 宽度不一致;label 宽度未严格固定 88px、长 label 未换行。
|
||||||
|
- 修复:
|
||||||
|
1. `el-form` 的 `label-width="88"` 改为 `label-width="88px"`。
|
||||||
|
2. `.score-detail-form` 样式里 label 强制 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important;`,并保留 `white-space: normal`、`word-break: break-all` 换行。
|
||||||
|
3. 单字段控件(`.el-form-item__content > .el-input/.el-select/.el-date-editor`)统一 `width: 100%`,取代原先未生效/不齐的 `width: 150px`。
|
||||||
|
- 备注:「临时额度申请使用期限」两个 date-picker 仍由 `.score-detail-form__range` 的 `flex:1` 均分,不受 100% 宽度影响。
|
||||||
|
|
||||||
|
## 客商档案「评分详情表」弹窗控件宽度对齐与 label 换行
|
||||||
|
- 问题:评分详情表弹窗中 input、select、date-picker 宽度不一致;长 label 未换行。
|
||||||
|
- 修复 `src/views/vehicle/customer-archive.vue`:
|
||||||
|
1. `el-form` 的 `label-width="88"` 改为 `label-width="88px"`。
|
||||||
|
2. `.score-detail-form` 样式:label 强制 `flex:0 0 88px`、`width:88px`、`white-space:normal`,超出换行。
|
||||||
|
3. 单字段控件(`.el-form-item__content > .el-input/.el-select/.el-date-editor`)统一 `width:100%`,撑满 content 区,与选择框等宽。
|
||||||
|
- 保留 `.score-detail-form__range` 内双 date-picker `flex:1; min-width:0` 的「起—止」布局。
|
||||||
|
|
||||||
|
## 评分详情表弹窗 label 与输入控件宽度对齐
|
||||||
|
- 文件:`src/views/vehicle/customer-archive.vue`
|
||||||
|
- 问题:评分详情表弹窗内输入框、下拉框、日期框宽度不一致;长 label 未换行。
|
||||||
|
- 修改:
|
||||||
|
1. `<el-form label-width="88">` 改为 `label-width="88px"`。
|
||||||
|
2. `.score-detail-form` label 样式强制 `width: 88px !important; flex: 0 0 88px !important; min-width: 88px !important;`,并保留 `white-space: normal` / `word-break: break-all` 换行。
|
||||||
|
3. 单字段控件(`.el-form-item__content > .el-input / .el-select / .el-date-editor`)统一 `width: 100%`,填满 label 剩余区域,保证 input/select/date-picker 视觉等宽。
|
||||||
|
- 说明:组合日期区间(`.score-detail-form__range`)保持两个 date-picker `flex:1` 均分,不受单字段 100% 影响。
|
||||||
|
|
||||||
|
## customer-archive 评分详情表 label/控件宽度对齐
|
||||||
|
- 问题:评分详情弹窗表单的输入框与选择框宽度不一致;label 宽度要求 88px 且超长换行。
|
||||||
|
- 修复:`src/views/vehicle/customer-archive.vue`
|
||||||
|
- `label-width="88"` → `label-width="88px"`。
|
||||||
|
- `.score-detail-form` 样式:`.el-form-item__label` 强制 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important;`,并保留 `white-space: normal !important` 等换行样式。
|
||||||
|
- 控件由固定 `width: 150px` 改为 `.el-form-item__content > .el-input/.el-select/.el-date-editor { width: 100%; }`,使输入框、下拉框、日期框在各自列内等宽撑满。
|
||||||
|
|
||||||
|
## 评分详情表弹窗:label 固定 88px 换行 + 输入/选择框宽度一致
|
||||||
|
- 问题:`src/views/vehicle/customer-archive.vue` 评分详情弹窗内输入框与选择框宽度不一致,label 长文本未换行。
|
||||||
|
- 修复:
|
||||||
|
1. `label-width="88"` → `label-width="88px"`。
|
||||||
|
2. `.score-detail-form` 样式:`.el-form-item__label` 强制 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important; white-space: normal !important;` 确保 88px 固定宽度并自动换行。
|
||||||
|
3. 单字段控件 `.el-form-item__content > .el-input / .el-select / .el-date-editor` 由固定 `150px` 改为 `width: 100%`,使输入框与选择框在各自栅格列内等宽撑满。
|
||||||
|
- 备注:「临时额度申请使用期限」双日期组合仍走 `.score-detail-form__range` 的 `flex:1; min-width:0`,不受单字段 100% 影响。
|
||||||
|
|
||||||
|
## 客商档案评分详情表控件宽度统一 + label 88px 换行
|
||||||
|
- 问题:`src/views/vehicle/customer-archive.vue` 评分详情弹窗内输入框与选择框宽度不一致;label 区域未严格固定 88px,长 label 换行效果不稳定。
|
||||||
|
- 修复:
|
||||||
|
1. 模板 `el-form` 的 `label-width="88"` 明确为 `label-width="88px"`。
|
||||||
|
2. scoped 样式 `.score-detail-form`:
|
||||||
|
- `.el-form-item__label` 强制 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important;`,并保留 `white-space: normal !important` / `word-break: break-all` 让超长 label 自动换行。
|
||||||
|
- 控件统一由固定 `width: 150px` 改为 `.el-form-item__content > .el-input/.el-select/.el-date-editor { width: 100%; }`,使输入框、下拉框、日期框在各自栅格列内等宽撑满。
|
||||||
|
- 说明:「临时额度申请使用期限」双日期组合区仍由 `.score-detail-form__range` 控制 flex:1 均分,不受单控件 100% 规则影响。
|
||||||
|
|
||||||
|
## customer-archive 评分详情表控件宽度统一 + label 换行
|
||||||
|
- 需求:评分详情弹窗内输入框与选择框宽度不一致;label 宽度需固定 88px 且超长换行。
|
||||||
|
- 修改(`src/views/vehicle/customer-archive.vue`):
|
||||||
|
1. 模板 `score-detail-form` 的 `label-width="88"` → `label-width="88px"`。
|
||||||
|
2. scoped 样式 `.score-detail-form`:`.el-form-item__label` 固定 `flex: 0 0 88px !important; width: 88px !important; min-width: 88px !important;`,并保留 `white-space: normal !important; word-break: break-all; line-height: 1.4;` 实现换行。
|
||||||
|
3. 控件宽度由固定 `150px` 改为 `.el-form-item__content > .el-input/.el-select/.el-date-editor { width: 100%; }`,使输入框、下拉框、日期框在同一 content 区域内等宽撑满。
|
||||||
|
- 说明:双日期字段(临时额度期限)仍走 `.score-detail-form__range` 的 `flex:1` 布局,保持两个日期框均分并对齐。
|
||||||
|
|
||||||
|
## 评分详情表输入框/选择框宽度对齐 + label 固定 88px 换行
|
||||||
|
- 文件:`src/views/vehicle/customer-archive.vue`
|
||||||
|
- 问题:评分详情弹窗内输入框、选择框、日期框宽度不一致;长 label 需要固定 88px 并允许换行。
|
||||||
|
- 修改:
|
||||||
|
1. `label-width="88"` → `label-width="88px"`。
|
||||||
|
2. `.score-detail-form :deep(.el-form-item__label)` 强制 `flex:0 0 88px !important; width:88px !important; min-width:88px !important`,保留 `white-space:normal !important; word-break:break-all; line-height:1.4`。
|
||||||
|
3. 控件宽度由固定 150px 改为 `.el-form-item__content > .el-input/.el-select/.el-date-editor { width:100%; }`,使输入框、选择框、日期框在固定 label 后自动等宽填满 content 区。
|
||||||
|
- 保留 `.score-detail-form__range` 内双 date-editor `flex:1; min-width:0` 的收缩行为。
|
||||||
|
|
||||||
|
## vehicle/customer-archive 两处「详细地址」hover 溢出提示完整地址
|
||||||
|
- 需求:编辑客商档案页,详细地址字段(窄栏 + 长文本易截断)鼠标移上去显示完整地址;仅文本溢出才显示。
|
||||||
|
- 实现:两处 `el-input` 用 `<el-tooltip>` 包裹,`content` 绑定字段值,`placement="top"`,`:disabled="!xxxOverflow"`(溢出才显示),`style="display:block; width:100%"`(el-tooltip 根 span 默认 inline,必须 block 才能撑满栅格列)。
|
||||||
|
- 主表单(line 317,`archiveForm.registeredDetailAddress`):`ref="registeredDetailInput"` + `@input` 实时重算。
|
||||||
|
- 发票弹窗(line 1008,`invoiceForm.registeredDetailAddress`):`ref="invoiceDetailInput"`,readonly 地图回填,靠 `watch` + `openInvoice` 打开后 `nextTick` 重算(弹窗按需挂载,预填数据打开后 watch 已不触发)。
|
||||||
|
- 新增:`data` 加 `registeredDetailOverflow/invoiceDetailOverflow` 标记;`watch` 监听两字段值变化→`checkOverflow`;`methods.checkOverflow(refName,flag)` 读 `inst.$el.querySelector('input')` 的 `scrollWidth > clientWidth` 判定溢出;`openInvoice` 打开后双 nextTick 重算发票框溢出。
|
||||||
|
- 验证:`vite build --outDir /tmp/tms-check-build` 零报错(常规 build 仅因安全删除保护拦截清空 dist,与代码无关)。
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
## 合同管理独立表单页下拉框定位修复
|
||||||
|
- `src/views/business/contract-manage.vue` 全部 12 处 `el-select` 加 `:teleported="false"` + `:fit-input-width="true"`:下拉保持在输入框正下方、宽度与输入框一致(原 teleport 到 body 导致下拉漂移且按内容撑宽)。
|
||||||
|
- 若后续新增独立表单页出现同样问题,照加这两个属性即可。
|
||||||
|
- 追加:teleported=false 后 popper 因下方空间不足自动上翻,再给 12 处 el-select 加 `placement="bottom-start"` + `:popper-options="{ modifiers: [{ name: 'flip', enabled: false }] }"` 强制始终在下方展示。
|
||||||
|
|
||||||
|
## 调度弹窗司机输入框宽度不一致修复
|
||||||
|
- 现象:「编辑调度信息(精简录入)」弹窗中,司机(el-autocomplete)输入框比同行 250px 的其他输入框宽。
|
||||||
|
- 根因:`business-crud-page.vue` 约 13434 行的「表单控件统一 250px」规则漏了 `.el-autocomplete`(autocomplete 根节点是包装层,内部才是 .el-input)。
|
||||||
|
- 修复:在该规则选择器列表追加 `> .el-autocomplete` 和 `> .el-autocomplete > .el-input`,与运单独立表单页(13658 行 business-crud-page--form-page 先例)一致。
|
||||||
|
- 经验:凡「控件统一 250px」类规则,选择器必须包含 `.el-autocomplete` 包装层。
|
||||||
|
|
||||||
|
## 调度弹窗收发货地址行溢出修复
|
||||||
|
- 现象:「编辑调度信息」弹窗收发货信息行,地址输入框过宽,右侧「联系方式」列被挤出弹窗。
|
||||||
|
- 根因:`__dispatch-shipping-form __route-address-row` 网格地址列为 `minmax(280px, 1fr)`,各列最小宽度合计 1030px,弹窗不够宽时 grid 不换行直接溢出。
|
||||||
|
- 修复:地址列改 `minmax(0, 1fr)`(宽屏仍 1fr 拉伸,窄窗自动收窄,总最小宽降到 750px)。
|
||||||
|
- 备注:基础规则 `__route-address-row`(13750 行,运单独立表单页等在用,7 列含 40px 按钮列)仍是 minmax(280px,1fr),同样窄窗有溢出风险,暂未动(用户未反馈该页问题)。
|
||||||
|
|
||||||
|
## el-select 选中值 / 占位符颜色区分(全局)
|
||||||
|
- 需求:已选值 #606266,未选值保持 placeholder 灰 #909399。
|
||||||
|
- 根因:Element Plus 的选中值文本与占位符**共用同一节点** `<div class="el-select__selected-item el-select__placeholder [is-transparent]">`,仅靠 `.el-select__selected-item` 无法区分,导致未选态也被染成选中色。
|
||||||
|
- 区分标志:只有「未选值」或「已展开但无搜索输入」时才带 `.is-transparent`。
|
||||||
|
- 修复(`src/styles/element-ui.scss`):选中规则改为 `.el-select__wrapper:not(.is-disabled) .el-select__selected-item:not(.is-transparent)`;多选 tag 文字另加 `.el-select__tags-text` 规则(tag 自身 color 优先级更高,父级规则盖不到)。
|
||||||
|
- 经验:以后写任何「区分 el-select 有值/无值」的 CSS,一律用 `:not(.is-transparent)`。
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
|
||||||
|
## 付款比例设置提示改为 tooltip(contract-manage / contract-manage-change)
|
||||||
|
- 需求:将「付款比例设置」区块原有的红色文字提示改为标题右侧的 `?` 图标悬停提示。
|
||||||
|
- 改动:编辑弹窗、详情弹窗、合同变更页三处标题后添加 `el-tooltip` + `el-icon-question-filled`,content 为「非必填;配置付款比例时,合计必须等于100%。」;同时移除 contract-manage.vue 中原 `.contract-manage-form__tip` 红色提示文字及对应样式。
|
||||||
|
|
||||||
|
## 必填星号与 label 首行齐平(全局)
|
||||||
|
- 根因:`element-ui.scss` 中 `.el-form-item--label-right .el-form-item__label { align-items:center }` 使 inline-flex label 的星号 `::before`(独立 flex 子项)在多行折行时被垂直居中。
|
||||||
|
- 修复:label 改 `align-self:center`(整体仍随行垂直居中)+ `align-items:flex-start`(星号/文字首行齐平),覆盖 label-right/left/top 三种位置;旧 inline-block 注释已修正。
|
||||||
|
- 影响页面:project-apply 独立表单页等所有多行 label 场景(弹窗内 `white-space:nowrap` 不受影响)。
|
||||||
|
|
||||||
|
## 必填星号首行对齐:改用 align-items: normal(stretch 方案)
|
||||||
|
- 最终写法(`src/styles/element-ui.scss`):`.is-required .el-form-item__label { align-self:center; align-items:normal; }` + `::before { display:inline-block; height:auto; line-height:inherit; margin-right:4px }`。
|
||||||
|
- 原理:stretch 让星号盒子在「换行时」被拉伸到 label 全高、内容从顶部排布 → 贴首行;单行时盒子=一行高,视觉不变(比 flex-start 更贴合「只换行才生效」)。
|
||||||
|
- 前提:::before 高度必须 auto 且继承行高;label 不能是 固定 height + 等高 line-height,否则退化成居中,需 JS 判行数加 class。
|
||||||
|
|
||||||
|
## 合同变更页(contract-manage-change.vue)
|
||||||
|
- 「其它附件」上传按钮改为 `type="primary" plain icon="el-icon-upload"`,与「变更材料」区 vehicle-attachment-upload 内置按钮一致。
|
||||||
|
- 分区标题必填星号:`.dialog-section-title.is-required::after { content:'*'; color:#f56c6c }`(`::before` 已被蓝色竖条占用,星号走 `::after`)。
|
||||||
|
- 再次变更逻辑:`load()` 里 form 强制重置 `changeContent/changeReason/changeAttachmentsJson = ''`、`changeMaterials = []`(不带上一次内容);新增 `mergeChangeAttachments()` 把上一次的 `changeAttachmentsJson` 归集进 `attachments`(其它附件),按 url/名去重,数字 size 转格式化文本,提交时随 attachmentsJson 持久化。
|
||||||
|
|
||||||
|
## 合同变更记录对齐客商变更记录(contract-manage.vue)
|
||||||
|
- 编辑页与详情弹窗的两处「变更记录」表格新增「变更内容」列:`formatContractChangeContent(row)` 复用 `buildDetailChangeRecordRows` 生成「字段:变更前:x → 变更后:y」摘要,单行省略 + `el-tooltip` 全量悬浮(样式 `.contract-change-record-content-cell/-tooltip` 照搬客商 `change-record-content-*`)。
|
||||||
|
- 编辑页操作列由单个「流程」改为「详情 + 流程」(`openDetailChangeRecord` + `openFlow`,宽度 120→150);详情弹窗表格「查看详情」改「详情」与客商一致。
|
||||||
|
- 值格式化增强:`billingPlanJson`→方案名(默认)列表、`paymentRatioJson`→「第N笔 xx%」、`settlementRuleJson/preSettlementConfigJson/formalSettlementConfigJson`→可读摘要(自动生成开关/结算类型/周期类型/截单日/周期天数,含预结算+正式结算双段)。
|
||||||
|
- `buildDetailChangeRecordRows` 过滤条件补「前后都为空则跳过」(原来只比 JSON 字符串,'' 与 null 会漏)。
|
||||||
|
|
||||||
|
## 星号方案补丁(关键)
|
||||||
|
- EP 的 `.el-form-item__label` 写死 `height:32px; line-height:32px`(项目只覆盖了 line-height:14px),导致 stretch 方案文字顶到 32px 盒子顶部、单行不居中。
|
||||||
|
- 修复:`.is-required .el-form-item__label { height:auto; align-self:center; align-items:normal }`——height:auto 收缩到内容高 + align-self:center 行内居中 + normal(stretch) 仅换行时星号贴首行。
|
||||||
|
|
||||||
|
## 车辆认证审核弹窗重构(transportCapacity/vehicle.vue)
|
||||||
|
- 背景:用户反馈「样式不统一、内容不全」。根因是审核弹窗用 `<el-descriptions :column="3" border>`(大白表 + 居中 label),与同页编辑弹窗的 `<section-card>` 白卡分组风格不一致。
|
||||||
|
- 改造:弹窗主体改为 4 个 `section-card` 分组 —— 基础信息 / 尺寸重量 / 证件信息 / 证件图片;内部 `el-form label-position="right" label-width="auto"` + `el-row/el-col`(3 列 `:span="8"`,所属组织与备注占 `:span="24"`)。
|
||||||
|
- 内容补全:新增「证件图片」分组,6 张证件照用 `<image-upload-field readonly large class-prefix="vehicle">` 只读展示(可点击放大);证件图片尺寸 240×151 复用已有全局 `:deep(.vehicle-uploader--large)` 规则,无需新写。
|
||||||
|
- label 单位统一为中文全角括号:`外廓长度(毫米)` / `核定载质量(KG)`(原为半角 `外廓长度(mm)`)。
|
||||||
|
- 方法重构:`formatAuditValue(item)` → `auditValue(prop)`,签名更简洁;新增 longTermMap,对 drivingLicenseEndDate / roadTransportCertEndDate / annualReviewEndDate / compulsoryScrapDate 在勾选长期有效时返回「长期有效」,避免与未填写的「-」混淆(与列表页 formatEndDate 行为一致)。
|
||||||
|
- 删除已无引用的 `auditDetailItems` computed。
|
||||||
|
|
||||||
|
## 全站弹窗标题补 4px 主色竖条(全局 element-ui.scss)
|
||||||
|
- 发现:`.el-dialog__title` 全局只配了 `inline-flex + gap:8px`,**没有竖条**(竖条此前只在 `.dialog-section-title::before`),导致全站 el-dialog 标题都不带竖条,不符合 AGENTS.md 4.8。
|
||||||
|
- 修复:新增 `.el-dialog__title:not(:empty)::before { width:4px; height:16px; border-radius:2px; background:var(--el-color-primary) }`。
|
||||||
|
- 注意:必须加 `:not(:empty)`,否则无标题弹窗会出现孤立蓝条。
|
||||||
|
- customer-archive.vue 评分详情弹窗:将「临时额度申请使用期限」区间行(span=12 + 至 连接)拆分为两个 span=6 独立字段「临时申请额度有效期起 / 止」,字段名 tempCreditStartDate/End 不变,删除废弃样式 .score-detail-form__range。
|
||||||
|
- temporary-credit-limit 文案调整:option 增加 addTitle/editTitle/viewTitle=「新增/编辑/查看临时额度申请」(覆盖 crud-dialog-title mixin 按菜单名注入的「临时额度管理」);remark label 改「临时额度信息」(hide:true 无表格副作用,rules 同步);projectName 表单 label 用 `#projectName-label` 插槽改「项目额度信息」,列 label 保持「项目名称」不动表格列头/搜索。Avue 支持-{prop}-label / -header / -search 插槽分离表单/表头/搜索文案。
|
||||||
|
- temporary-credit-limit「申请有效期至」增加「必须晚于当前日期」校验:common.js 新增通用 `futureDateRule(label)`(trigger: ['change','blur'],空值/非法值放行,按 0 点比较,当天也算过期),temporary-credit-limit.js 的 validUntil rules 追加该规则。已验证 Element Plus 通用 picker 在选中日期时调用 formItem.validate('change'),故选择即刻提示;form.validate() 不带 trigger 时会跑全部规则,提交仍拦截。
|
||||||
|
- temporary-credit-limit 搜索占位文案统一:createCrudOption(withSearchPlaceholders([...])),搜索栏 placeholder 变为「请输入/请选择」不带字段名,与 waybill-manage / transport-plan / shipping-template 同一模式。
|
||||||
|
- temporary-credit-limit 弹窗表单 placeholder 统一为「请输入/请选择」不带字段名:option 各列加 placeholder(projectCode/undertakeDeptName/projectFundLimit/usedFundLimit/remainingFundLimit/remark=请输入,validUntil=请选择),视图 formslot 自定义控件(projectName select、applyLimit input)placeholder 同步改。
|
||||||
|
|
||||||
|
## 【项目管理】列表增加「项目资金使用预警」(project-apply)
|
||||||
|
- 需求:列表新增「资金使用风险」列,高风险红色、中风险黄色;列表头部增加「资金使用风险」快速筛选标签(高风险(数)/中风险(数)),点击筛选。规则 tooltip:中风险≥80%、高风险≥90%,使用率=已使用资金/项目资金使用额度×100%。
|
||||||
|
- 前端(已落地,src/views/business/project-apply.vue + src/option/business/project-apply.js + src/api/business/project-apply.js):
|
||||||
|
- option/project-apply.js 新增列 `fundUseRisk`(slot:true, hide 列表显示) + 导出 `fundUseRiskOptions`(high/medium/none) 与 `fundUseRiskThreshold`{high:90, medium:80}。
|
||||||
|
- view 新增 `#header` 插槽:`el-check-tag` 快速筛选(复用 contract-manage 到期标签交互),带数量(fundRiskStats[statKey])与 `?` tooltip 规则;新增 `#fundUseRisk` 插槽渲染彩色 `el-tag`(high→type=danger 红,medium→type=warning 黄,无→"-")。
|
||||||
|
- data 加 fundRiskScope/fundRiskStats{high,medium,none}/fundRiskTagOptions;onLoad 追加 `fundUseRisk` 筛选参数 + 成功后 refreshFundRiskStats;新增 changeFundRiskScope(再次点击取消)、refreshFundRiskStats;searchReset 清空风险筛选。
|
||||||
|
- api 新增 `getFundRiskStats` → GET /blade-transport/project-apply/fund-risk-stats。
|
||||||
|
- 后端契约(未改,待确认数据来源):ProjectApplyVO 补齐 `fundUseRisk`(high/medium/none)、`fundUseRiskName`、`fundUseRate`、`usedFundLimit`/`remainingFundLimit`;列表 selectProjectApplyPage 填充每行风险 + buildQuery 支持 `fundUseRisk` 过滤;新增 `/fund-risk-stats` 返回 {high,medium,none}。
|
||||||
|
- ⚠️ 关键阻塞:系统里项目与任何费用表(运输计划/运单/账单/客商)均无关联字段,`usedFundLimit`(已使用资金) 无真实取数来源;临时额度模块的 project-apply/list 返回里 usedFundLimit 实际为空。需业务方确认「已使用资金」口径后才能算风险等级,否则列表风险列全空、统计为 0。
|
||||||
|
|
||||||
|
## 合同文件「批量下载」按钮与标题同排(contract-manage.vue 内联 AttachmentSection)
|
||||||
|
- 需求:编辑页「合同文件」分组里「批量下载」按钮目前独立一行、靠右偏上,高度与标题区错位;要放到「合同文件」标题右侧同排、垂直居中。
|
||||||
|
- 修复:`const AttachmentSection = defineComponent(...)` 内联组件 render() —— 原结构是 `div.dialog-section-title`(标题) 与 `div.contract-manage-form__attachment-head`(批量下载按钮) 两个并列 div;改为把标题与按钮放进同一个 `contract-manage-form__attachment-head` flex 容器(标题左、按钮右)。
|
||||||
|
- 样式:`:deep(.contract-manage-form__attachment-head)` 由 `justify-content:flex-end` 改为 `space-between` + `align-items:center`,并 `.dialog-section-title { margin-bottom:0 }` 抵消标题自身 20px 下边距(2674 行 `:deep(.dialog-section-title)` 会命中子组件渲染的标题)。
|
||||||
|
- 注意:AttachmentSection 用 render 函数产出 DOM,不带当前组件 scoped data 属性,因此样式必须走 `:deep()`;scoped 的 `&__billing-head, &__attachment-head`(2723) 只作用于当前组件模板内(计费信息),对子组件 DOM 无效。
|
||||||
|
- 影响:4 处使用(编辑/详情 × 合同文件/其它附件)布局统一变为「标题 + 批量下载」同排。
|
||||||
|
- 主人二次反馈「下面的其它附件也一样」——确认 L463(编辑/其它附件 description) / L765(详情/其它附件 description+readonly) 与 L313(编辑/合同文件) / L671(详情/合同文件) 都走同一份 render+样式,无需新代码;指挥浏览器 Cmd+Shift+R 硬刷即可看到效果(render 函数组件的 :deep() 样式改 HMR 经常不传播)。
|
||||||
|
|
||||||
|
## 合同格式下拉改读业务字典 contractFormat(contract-manage.vue)
|
||||||
|
- 需求:新增/编辑合同管理页「合同格式」下拉由硬编码改为读取业务字典 `contractFormat`。
|
||||||
|
- 原实现:`option/business/contract-manage.js` 里 `contractFormat` 列 `dicData` 硬编码 `[{label:'电子合同',value:'电子合同'},{label:'纸质合同',value:'纸质合同'}]`,view 里 `contractFormatOptions` computed 直接 `columnOptions('contractFormat')` 读 column.dicData。
|
||||||
|
- 修复(4 处):
|
||||||
|
1. data 加 `contractFormatDictOptions: []`(与 settlementModeDictOptions 并列)。
|
||||||
|
2. computed `contractFormatOptions` 改为优先 `contractFormatDictOptions`,为空回退 `columnOptions('contractFormat')`(保留静态兜底)。
|
||||||
|
3. `loadSettlementDictionaries` 的 `Promise.all` 追加 `getBizDictionary({ code: 'contractFormat' })`,用 `records()` 映射 `{label:dictValue,value:dictKey}` 填充 `contractFormatDictOptions`;再**条件覆盖** column.dicData(仅当字典非空时),避免字典未配时清掉静态兜底。
|
||||||
|
4. `detailValue` 的字典映射分支加入 `prop === 'contractFormat'`,详情页「合同格式」也走 `displayStatus` 显示 label。
|
||||||
|
- 后端契约:`getBizDictionary` → `GET /blade-system/dict-biz/dictionary?code=contractFormat`,返回 `List<DictBiz>{code,dictKey,dictValue,remark}`(DictBizController#dictionary 按 code 精确匹配)。字典 code 按用户口述用驼峰 `contractFormat`(现有 settle_method 为下划线,若后台实际是 `contract_format` 需调整 code)。
|
||||||
|
- 参考模式:与 `settlementMode`(settle_method)完全一致。
|
||||||
|
|
||||||
|
## 合同类别 contractCategory 也读业务字典(contract-manage.vue,续上)
|
||||||
|
- 需求:模板里 label 实为「合同类型」(prop `contractCategory`),下拉由硬编码 `common.js#contractCategoryOptions`(客户合同/承运商合同/补充协议,value 中文)改为读业务字典 `contractCategory`。
|
||||||
|
- 修复(3 处,与 contractFormat 同模式):
|
||||||
|
1. data 加 `contractCategoryDictOptions: []`。
|
||||||
|
2. computed `contractCategoryOptions` 改为优先 `contractCategoryDictOptions`,空回退 `columnOptions('contractCategory')`(静态兜底)。
|
||||||
|
3. `loadSettlementDictionaries` 的 `Promise.all` 追加 `getBizDictionary({ code: 'contractCategory' })`,映射填充后**条件覆盖** `contractCategory` 列 dicData(仅非空时)。
|
||||||
|
- 列表页列显示/搜索下拉、详情页(detailValue 已有 `prop==='contractCategory'` 分支走 displayStatus)均因 column.dicData 覆盖而自动跟随业务字典。
|
||||||
|
- 注意:`config.defaultForm.contractCategory: '客户合同'`(option/contract-manage.js)为硬编码中文默认值,若业务字典 dictKey 非中文需同步调整。
|
||||||
|
|
||||||
|
## 车辆新增弹窗:所有组织移位 + 使用部门 + 长期有效清空(vehicle.vue + 后端)
|
||||||
|
- 需求(transportCapacity/vehicle):①所有组织移到「基础车辆信息」区末尾;②新增「使用部门」字段(后端 transport_vehicle 无该字段,需加);③车牌号三段(省/号码/颜色)包成整体输入框+减宽;④勾选「长期有效」清空对应日期且不可编辑。
|
||||||
|
- 用户确认:所有组织+使用部门放「基础车辆信息」区末尾;使用部门用部门下拉(复用 getDeptTree→organizationOptions,存部门名,仅部门名非级联);车牌号包成整体输入框+减宽;长期有效 4 组全部统一清空(强制报废/行驶证/道路运输证/年审)。
|
||||||
|
- 前端(vehicle.vue):
|
||||||
|
1. emptyForm 加 `useDepartment: ''`。
|
||||||
|
2. 删「基础车辆信息」首行的「所有组织」(原 span24),在区末新增一行:`使用部门`(span8, el-select+organizationOptions, 存 rawLabel) + `所有组织`(span8, el-cascader)。organizationOptions 由 initDeptTree 填充。
|
||||||
|
3. `.plate-group` 由 grid(112px 1fr 112px) 改为 flex 整体输入框:外层 border+radius+focus-within 变化;内部 el-select/el-input 去 box-shadow、无边框,第一/最后 select flex 0 0 30%、中间 input flex 1,子项间用 `::before` 竖线分隔。
|
||||||
|
4. 4 组 date-picker 加 `:disabled="xxxLongTerm === 1"`(行驶证/道路运输证原缺,补上),checkbox 加 `@change="handleLongTermChange(日期prop, longterm prop)"`;新增方法 handleLongTermChange(勾选时清空日期并 validateField)。
|
||||||
|
5. formRules:drivingLicenseEndDate / roadTransportCertEndDate 由 required 改为 validator(新增 validateDrivingLicenseEndDate / validateRoadTransportCertEndDate,longTerm===1 时跳过必填),annualReview 原 validator 已支持。
|
||||||
|
- 后端(tms-erp-api-ws,新增 use_department 列):
|
||||||
|
- `TransportVehicle.java` 加 `private String useDepartment`。
|
||||||
|
- `blade_transport_vehicle.sql` 建表加 `use_department varchar(50)`。
|
||||||
|
- `TransportVehicleExcel.java` 加 `使用部门` 列。
|
||||||
|
- `TransportVehicleMapper.xml` resultMap + BaseColumn + QueryCondition(useDepartment LIKE) 各加。
|
||||||
|
- `TransportVehicleServiceImpl.prepare()` trim useDepartment;validate() 加长度≤50 校验。
|
||||||
|
- 新增增量迁移 `doc/sql/transport/blade_transport_vehicle_use_department_patch_20260909.sql`(ALTER ADD use_department AFTER organization_name)。
|
||||||
|
- 注意:VO 继承 entity 无需改;Wrapper BeanUtil 同名透传。已上线库须执行 patch SQL(建表脚本是 DROP+CREATE,勿直接跑)。
|
||||||
|
- 后续补充(vehicle.vue「车辆资质图片」区输入框对齐):第一行原 span `6/8/10` 不等宽(行驶证档案编号 6 / 行驶证有效期 8 / 道路运输证号 10)导致输入框宽度不齐,统一改为 `8/8/8`,与下方各行 span=8 一致。仅改 2 处 span,无逻辑改动。
|
||||||
|
|
||||||
|
## 车辆车牌号:改为单一 input 输入框(vehicle.vue,3 段 → 1 段)
|
||||||
|
- 需求:基础车辆信息区「车牌号」原本是「省简称 select + 号码 input + 车牌颜色 select」三段拼装(plateProvince/plateNoBody/plateColor),改为**单个 el-input** 直接输入完整车牌号(如 京A12345)。
|
||||||
|
- 改动(vehicle.vue):
|
||||||
|
1. 模板:`plate-group` 三段 → `<el-input v-model="vehicleForm.plateNo" maxlength="8" show-word-limit placeholder="如:京A12345" @input="handlePlateNoInput" @blur="validatePlateFormat">`。删除 `.plate-group` 样式(含 focus-within、竖线分隔、child flex 占比等整段)。
|
||||||
|
2. emptyForm:删 `plateProvince`、`plateNoBody`,保留 `plateNo`、`plateColor`(后端字段继续存历史车牌颜色)。
|
||||||
|
3. data:删 `plateProvinceOptions`、`plateColorOptions` 暴露;常量 `plateProvinceOptions`(31 省简称数组)整段删除;`plateColorOptions` 常量删除。
|
||||||
|
4. validatePlateNo 重写:直接校验 `value`(完整车牌号),正则 `/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼港澳]{1}[A-Z][A-Z0-9挂学警港澳]{5,6}$/`,错误提示改为「请输入标准车牌号(如:京A12345)」。
|
||||||
|
5. syncPlateNo / splitPlateNo 重写:splitPlateNo 改为 noop(保留以兼容 OCR 后回调链路);syncPlateNo 改为 trim+upperCase 单一字段。
|
||||||
|
6. handlePlateBodyInput 改名为 handlePlateNoInput,新增 validatePlateFormat 失焦校验。
|
||||||
|
7. 车辆认证审核弹窗只读详情同步删「车牌颜色」展示(与录入表单一致)。
|
||||||
|
- 后端不动:`plate_no` / `plate_color` 字段保留(数据库兼容历史车牌颜色),Service 无强制校验。
|
||||||
|
- 受影响:OCR 自动识别(行驶证/机动车销售统一发票)后续调用链路无需改,因 syncPlateNo 仍对 plateNo 做归一化。
|
||||||
|
|
||||||
|
## 车辆新增/编辑弹窗:输入框宽度统一(vehicle.vue,前后两区)
|
||||||
|
- 需求(transportCapacity/vehicle):新增/编辑弹窗「基础车辆信息」+「车辆资质图片」两个 section 各输入框宽度要一致;截图红框重点为车牌号、外廓尺寸、所有组织/使用部门。
|
||||||
|
- 根因:基础区原本列宽混乱(车牌号 span10 / 车辆类型 span6 / 外廓尺寸 span6,其余 span6),行内 3 列与 2 列混排;图片区文本字段全 span6,与基础区不一致。
|
||||||
|
- 改动(vehicle.vue 仅模板 el-col span):
|
||||||
|
1. 「基础车辆信息」区所有字段统一为 **span=8(3 列布局)**:车牌号、车辆类型、外廓尺寸、核定载质量、准牵引总质量、业务关系、能源类型、强制报废日期、海关备案号、所有组织、使用部门。
|
||||||
|
2. 「车辆资质图片」区文本/日期字段(行驶证档案编号、行驶证有效期、道路运输证号、道路运输证有效期、道路运输年审有效期、机动车登记编号、机动车登记日期)统一为 **span=8**。
|
||||||
|
- 不改:只读「车辆认证审核」弹窗(L140 起的 span6 只读详情 + large 图片上传)与新增弹窗内「证件图片」上传控件(固定尺寸)保持 span6。外廓尺寸为复合控件(长/宽/高三 input),口径同 span8。
|
||||||
|
- ⚠️ 注意:操作中曾误删核定载质量 el-col 标签(453-460 行开始时少 el-col、尾部多一个 `</el-col>`),已修复为完整 `<el-col :span="8">...`。
|
||||||
|
- 后续纠偏(2026-09-09 傍晚):主人反馈「为什么不是按4列显示」——上一步把"基础车辆信息/车辆资质图片"两区误改成 3 列(span8)。同项目惯例 driver.vue/customer-archive.vue 全为 **4 列(span6,仅复合长字段 span12)**,故统一重排为 4 列:
|
||||||
|
- 基础车辆信息:`车牌号(6)+车辆类型(6)+外廓尺寸(12)` → `核定载质量/准牵引总质量/业务关系/能源类型(4×6)` → `强制报废日期/海关备案号/所有组织/使用部门(4×6)`
|
||||||
|
- 车辆资质图片:`行驶证有效期/道路运输证号/道路运输证有效期/道路运输年审有效期(4×6)` → `机动车登记编号/机动车登记日期/行驶证档案编号(3×6)`,图片上传控件仍 span6
|
||||||
|
- 该区 span8 已全部移除,编辑表单区仅剩外廓尺寸 span12(复合控件长/宽/高)+ 图片上传 span6。
|
||||||
|
- 宽度微调(2026-09-09 傍晚2):主人圈出「所有组织」字段宽度与同行不一致。根因:`.vehicle-form` 里的 `:deep(...){width:100%}` 选择器只列了 `el-select / el-input / el-input-number / el-date-editor`,**漏了 `el-cascader`**(所有组织用的就是级联选择器,默认不撑满整列)。已补 `:deep(.el-cascader)` 进该规则,全表单仅此一处用到 el-cascader,故所有组织与同行等宽。
|
||||||
|
|
||||||
|
## 司机弹窗:补 placeholder + 每行统一 3 列(transportCapacity/driver.vue)
|
||||||
|
- 需求:新增/编辑司机弹窗 ①输入框无提示文案;②基础身份信息 3 列,其它 4 列 / 6 列行不统一。
|
||||||
|
- 改动(仅 1819 行 driver.vue 的 126-540 行 `<driver-dialog>` 模板,script/style 不动):
|
||||||
|
1. **基础身份信息**:行结构不动,7 个 el-form-item 控件补 placeholder(司机姓名/身份证号/出生年月/性别/民族/学历/详细地址)。住址已有「请选择省市区」、驾驶车辆已有「请输入或选择车辆」,不动。
|
||||||
|
2. **驾驶证信息**:第一行 4 列(:span=6)拆为两行 — 行 1(准驾车型 / 档案编号 / 有效期 date-range,:span=8)+ 行 2(长期有效,:span=8)。起/止 placeholder 保留简短「起」「止」,符合主人 1.B 偏好(保留原 date-range 二联结构,不改 daterange)。
|
||||||
|
3. **从业资格证信息**:第一行 4 列拆为两行 — 行 1(从业资格认证类型 / 资格证号 / 有效期,:span=8)+ 行 2(长期有效,:span=8)。资格证号 placeholder 保留「默认身份证号」不动。
|
||||||
|
4. **司机联系信息**:一行 6 列拆为两行 — 行 1(司机类型 / 手机号 / 紧急联系人姓名,:span=8)+ 行 2(紧急联系人手机号 / 与联系人关系 / 所属组织,:span=8)。所属组织已有 placeholder,不动。
|
||||||
|
- 决策点(主人确认):1.B(保留原 date-range 二联 + 第三列留空后拆行的方案);2 保留;3.A(驾驶证主页/副页、封面页/内容页两组上传图仍 :span=12,与占整行宽的原貌一致)。
|
||||||
|
- 校验:node 脚本统计 4 个 section-card / 13 个 el-row / 31 个 el-col / 24 个 el-form-item,全平衡。改动只在 driver.vue 一文件,~25 处 Edit 调用。
|
||||||
|
|
||||||
|
## 新增/修改司机弹窗改为 4 列布局(driver.vue,16:xx)
|
||||||
|
- 主人要求 /transportCapacity/driver 新增司机弹窗改 4 列显示。原文件各分组已是 span=6(4 列栅格)但每行只放 3 个字段,第 4 列空置。
|
||||||
|
- 重排结果(仅删行断 + 移动字段顺序,字段/校验/逻辑零改动):
|
||||||
|
1. 基础身份信息:行1 司机姓名/身份证号/出生年月/性别;行2 民族/学历/住址/详细地址;行3 驾驶车辆/岗位;行4 三张证件照(不变)。
|
||||||
|
2. 驾驶证信息:行1 准驾车型/档案编号/有效期/长期有效;上传主页/副页 span 12→6。
|
||||||
|
3. 从业资格证信息:行1 类型/资格证号/有效期/长期有效;上传封面页/内容页 span 12→6。
|
||||||
|
4. 司机联系信息:行1 司机类型/手机号/紧急联系人姓名/紧急联系人手机号;行2 与联系人关系/所属组织。
|
||||||
|
- 注意:此前日志记录的「3 列 span=8」方案已被后续改动覆盖为 span=6,本次以当前文件实际状态为准。
|
||||||
|
|
||||||
|
## 客商档案:客商类型→客商分类 + 新增客商类型(customer-archive.vue)
|
||||||
|
- ⚠️ 补救(17:34):data 里的 `customerKindOptions`/`customerKindMap` 定义一度丢失(模板 v-for 空 → 下拉显示「无数据」,主人截图反馈),已补回 data(customerTypeOptions 之后)。其余 8 处改动经 grep 确认完好。
|
||||||
|
- 需求:原「客商类型」(prop `customerType`,字典 customer_type:客户/承运商,多选)改名「客商分类」;另新增「客商类型」字段,取值为 外部客商 / 内部组织。
|
||||||
|
- 约定:prop `customerType` 不动(其它页面 loading-manage / dept / business-crud-page 都以 `customerType:'承运商'` 作为后端查询参数),只改中文标签。
|
||||||
|
- 新增字段 prop = **`customerKind`**(前端自定义命名,后端尚无对应字段,需后端在客商表加列并透传),值 `external`(外部客商) / `internal`(内部组织),单选下拉,默认 `external`。
|
||||||
|
- 改动点(8 处,全在 `src/views/vehicle/customer-archive.vue`):
|
||||||
|
1. 表单:227 行 label 改「客商分类」;其后新增一个 `el-col :span="6"` 表单项「客商类型」prop `customerKind`(el-select + `customerKindOptions`)。
|
||||||
|
2. data 新增 `customerKindOptions` / `customerKindMap`(external/internal ↔ 中文)。
|
||||||
|
3. formRules:`customerType` 提示文案改「请选择客商分类」。
|
||||||
|
4. 列表列:「客户类型」label 改「客商分类」;新增「客商类型」列(prop customerKind,type select,dicData 内外部,minWidth 120,**未加 search**)。
|
||||||
|
5. `emptyArchive()` 加 `customerKind: 'external'`。
|
||||||
|
6. `normalizeDetail()` 加 `customerKind: detail.customerKind || 'external'`。
|
||||||
|
7. 变更记录 `getChangeFieldLabel`:`customerType:'客商分类'`,新增 `customerKind:'客商类型'`。
|
||||||
|
8. `formatChangeFieldValue`:别名映射加 `客商分类/客户类型 → customerType`、`客商类型 → customerKind`;valueMap 加 `customerKind:{external:'外部客商',internal:'内部组织'}`。
|
||||||
|
- 校验:用 @vue/compiler-sfc 编译模板,0 错误。
|
||||||
|
|
||||||
|
## 上传证件项 label 贴顶修复(全局 archive-form 垂直居中提升)
|
||||||
|
- 症状:driver.vue 司机弹窗「驾驶证主页/副页」「封面页/内容页」等证件上传项(el-form-item 内容高 151px)label 文字贴顶,普通 32px 控件行看不出来。
|
||||||
|
- 根因:`archive-form` class 被 driver / vehicle / ship / credit-score-quantification / loading-manage / waybill-import-dialog 等 8+ 页面复用,但「label 与控件垂直居中」规则 `.archive-form :deep(.el-form-item){align-items:center}` 锁在 customer-archive.vue 的 scoped 样式里,其它页面拿不到;flex 容器默认 stretch,label(32px) 在高控件行内贴顶。
|
||||||
|
- 修复:规则提升到 `src/styles/element-ui.scss` 全局(紧跟 is-required 星号配套规则之后):`.archive-form .el-form-item { align-items:center }`。全站 archive-form 表单一并生效,label 与控件垂直居中。SCSS 编译通过。
|
||||||
|
|
||||||
|
## 司机弹窗「驾驶证信息」:有效期必填、长期有效不必填(transportCapacity/driver.vue)
|
||||||
|
- 需求:驾驶证信息区「有效期」(起/止,prop `drivingLicenseEndDate`)要必填(显示红*),「长期有效」复选框(prop `drivingLicenseLongTerm`)不必填(去掉红*)。
|
||||||
|
- 改动(formRules 两处):
|
||||||
|
1. `drivingLicenseEndDate` 规则加 `required: true`(`{ required: true, validator: this.validateDrivingLicenseEndDate, trigger: 'change' }`)——仅用于渲染红*,实际校验仍走自定义 validator。
|
||||||
|
2. 删除 `drivingLicenseLongTerm` 的 `{ required: true, message: '请选择驾驶证是否长期有效' }` 规则块。
|
||||||
|
- 关键机制(Element Plus 2.14 + async-validator 4.2.5 已核实):
|
||||||
|
- 红* 由 `form-item` 的 `isRequired = normalizedRules.some(rule => rule.required)` 决定,只要规则对象带 `required` 就显示。
|
||||||
|
- async-validator 的 `rule.validate` 中,若存在 `rule.validator`,直接走自定义 validator 并 return,**不会**再执行 required 空值检查 → 勾「长期有效」时止日被清空/disabled 也不会误报必填。
|
||||||
|
- 校验逻辑保持原样:起始日期恒必填;`longTerm !== 1` 时止日必填。
|
||||||
|
- 未同步:「从业资格证信息」区仍是「有效期无红* + 长期有效有红*」的旧形态(待主人确认是否一并统一)。
|
||||||
|
|
||||||
|
## 司机弹窗 placeholder 统一为「请输入/请选择」(transportCapacity/driver.vue)
|
||||||
|
- 需求:新增/修改司机弹窗输入框 placeholder 去掉后面的字段名称,只保留「请输入」「请选择」。
|
||||||
|
- 改动:18 处 placeholder 简化(7 处 → `请输入`,10 处 → `请选择`,1 处动态 cascader `driverForm.addressRegion || '请选择省市区'` → `|| '请选择'`)。
|
||||||
|
- **保留 4 处**(主人明确确认):
|
||||||
|
1. 驾驶车辆 `请输入或选择车辆` —— 复合操作(既可输入又可点按钮选车),去掉会丢语义。
|
||||||
|
2. 日期范围 `起` / `止` —— 起止标记,非字段提示。
|
||||||
|
3. 资格证号 `默认身份证号` —— 默认值说明,非「请输入」格式。
|
||||||
|
- 未动:表单**校验错误提示 message 仍带字段名**(如「请选择准驾车型」),因报错需指明具体字段,与 placeholder 定位不同;主人未要求,暂保持。
|
||||||
|
- 做法:Python 脚本按完整 placeholder 字符串精确替换(18 规则全命中,未匹配即 exit 1 中断),比逐个 Edit 高效且不会误伤。
|
||||||
|
- 校验:`@vue/compiler-sfc` parse + compileTemplate + compileScript 三项全通过。注意脚本必须放在**项目根目录**下运行(放 /tmp 解析不到 node_modules 里的 @vue/compiler-sfc)。
|
||||||
|
- ⚠️ git diff 显示的 `drivingLicenseLongTerm` 校验规则变动是**本次之前工作区已有的未提交改动**(见上一条日志),非本次引入,勿混淆。
|
||||||
|
|
||||||
|
## 「自动生成结算单」开关加 ? 悬浮提示(3 处)
|
||||||
|
- 需求:开关「开启」后面加问号图标,hover 提示「开启时,系统根据配置规则归集运单,定时生成结算单」。
|
||||||
|
- 改动文件(同一文案,为保持全站一致):
|
||||||
|
1. `src/views/business/components/business-crud-page.vue`(1884 行附近,`.business-crud-page__settlement-switch`)
|
||||||
|
2. `src/views/business/contract-manage.vue`(357 行附近,`.contract-manage-form__settlement-switch`)
|
||||||
|
3. `src/views/business/contract-manage-change.vue`(55 行,`.settlement-switch`,原为单行长标签已拆行)
|
||||||
|
- 写法:`<el-radio :label="1">开启</el-radio>` 后插 `<el-tooltip content="..." placement="top"><el-icon class="settlement-switch-tip"><QuestionFilled /></el-icon></el-tooltip>`,再接「关闭」radio。
|
||||||
|
- 样式:`.settlement-switch-tip { margin:0 4px; color:#a8abb2; cursor:help; font-size:14px }`,各文件 scoped 样式内一份(contract-manage 写在 `&__settlement-switch` 内)。
|
||||||
|
- 注意:图标走 main.js 全局注册(@element-plus/icons-vue 全量注册),无需 import;写法与 `project-apply.vue` 的 `__label-tip` 一致。
|
||||||
|
- 这三个大文件本身已存在 prettier 格式问题(改动前 git HEAD 版本同样 fail),非本次引入。
|
||||||
|
|
||||||
|
## 车辆认证审核弹窗排版对齐新增弹窗(vehicle.vue)
|
||||||
|
- 诊断 `/project-apply/fund-risk-stats` No endpoint:前端已定义 getFundRiskStats(api/business/project-apply.js:13),后端未实现;方案已给(后端补 R<{high,medium}> / 前端 catch 降级),待主人选。
|
||||||
|
- vehicle.vue 认证审核弹窗重排对齐新增弹窗:取消独立「尺寸重量」卡片,外廓尺寸改长/宽/高组合(span12,dimension-group);基础信息 3 行×4 列(车牌号/车辆类型/外廓尺寸;载质量/牵引质量/业务关系/能源类型;强制报废日期/海关备案号/所有组织/使用部门);证件信息按新增「车辆资质图片」行分布(止+运输证号+止+年审 / 登记编号+登记日期+档案编号+行驶证起 / 运输证起 / 备注整行);label「所属组织」改「所有组织」对齐新增;dimension-group 样式复制进 .certification-audit 作用域。@vue/compiler-sfc 编译校验通过。
|
||||||
|
- vehicle.vue 行驶证有效期必填星号:原规则是纯 validator(Element Plus 不渲染星号),改为 data 内 baseRules + computed formRules,drivingLicenseEndDate 动态返回 required 规则(长期有效=1 时返回 [],星号与校验同步消失);删除 validateDrivingLicenseEndDate。经验:Element Plus 星号只在规则含 required:true 时渲染,且 el-form-item 的 :required 属性会被注入为 {required} 规则并产生英文默认提示,不宜用它控制星号。
|
||||||
|
- 备注:车辆保存的「车牌颜色不能为空」「行驶证有效期:必填」均为后端校验(前端无对应规则),需后端改。
|
||||||
|
- driver.vue 从业资格证有效期对齐驾驶证:单日期改 date-range 起/止(列宽 6+6+6+6 → 6+6+8+4),新增 qualificationStartDate 字段(emptyForm + handleSubmit normalize);qualificationEndDate 规则加 required:true(渲染星号,validator 豁免长期);删除 qualificationLongTerm 的 required 规则(长期有效不必填);勾长期后起止均 disabled(比驾驶证更严,驾驶证起仍可编辑)。⚠️ 后端需新增 qualification_start_date 字段否则起日期无法存取。
|
||||||
|
- driver.vue 补充:驾驶证「有效期起」加 :disabled(长期=1 时禁用);驾驶证与从业资格证两个 validator 统一在「长期有效=1」时直接 callback() 放行(起止都不校验、不提示)。禁用不清空旧值,提交时保留原日期(后端以 longTerm 为准)。
|
||||||
|
- contract-manage-change.vue「合同文件」批量下载按钮上移:标题与按钮合并进 .section-head(flex space-between 同排),与「其它附件/计费信息」分区一致;删除无引用的 .attachment-head 样式。
|
||||||
|
- contract-manage-change.vue「自动生成结算单」问号提示图标位置:原 icon 夹在开启/关闭 radio 之间,因 .el-radio 默认 margin-right:32px 导致离开启远、贴关闭;修复 = 开启 radio margin-right 清零 + icon margin 改 0 32px 0 8px。
|
||||||
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
## 设计约定(已确认)
|
## 设计约定(已确认)
|
||||||
|
|
||||||
|
### 表单 placeholder 统一为「请输入 / 请选择」(不带字段名)
|
||||||
|
- **规范**:输入框 placeholder 只写动作词 `请输入` / `请选择`,**后面不跟字段名称**(label 已说明是什么字段,重复无意义)。
|
||||||
|
- 已生效页面:waybill-manage、transport-plan、shipping-template、temporary-credit-limit(搜索栏 + 弹窗表单)、**driver.vue**(2026-09-09 补齐 18 处)。
|
||||||
|
- **3 类例外必须保留,不要误简化**:
|
||||||
|
1. 复合操作:如 `请输入或选择车辆`(既可键盘输入又可点按钮弹窗选车),丢掉会损失语义。
|
||||||
|
2. 日期区间的 `起` / `止`:是起止标记,不是字段提示。
|
||||||
|
3. 默认值说明:如资格证号 `默认身份证号`、车牌号 `如:京A12345`,属于示例/默认值提示,非「请输入」格式。
|
||||||
|
- **校验错误提示 message 保持带字段名**(如「请选择准驾车型」「请输入档案编号」),与 placeholder 定位不同——报错必须指明具体哪个字段,不能只说「请选择」。
|
||||||
|
- Avue 配置化表单:在 option 各列加 `placeholder`;搜索栏可用通用工具 `withSearchPlaceholders([...])`(见 temporary-credit-limit.js)。
|
||||||
|
- 批量改法:Python 脚本按**完整 placeholder 字符串**做精确替换,每规则校验 `count > 0` 否则中断,避免误伤;改完用 `@vue/compiler-sfc` 的 parse + compileTemplate + compileScript 验证(脚本须放项目根目录下跑,放 /tmp 解析不到 node_modules)。
|
||||||
|
|
||||||
### 弹窗分组:统一用全局 `<section-card>`
|
### 弹窗分组:统一用全局 `<section-card>`
|
||||||
- 弹窗 body 背景由 `src/styles/element-ui.scss` 全局 `.el-dialog__body { background:#f5f6fa; padding:16px }` 统一灰底。
|
- 弹窗 body 背景由 `src/styles/element-ui.scss` 全局 `.el-dialog__body { background:#f5f6fa; padding:16px }` 统一灰底。
|
||||||
- 每组 = 白底卡片:4px 主色竖条 + 8px 间距 + 圆角 6px + 淡阴影。组件全局注册 `<section-card title="...">`,支持 `#title` / `#extra` slot。
|
- 每组 = 白底卡片:4px 主色竖条 + 8px 间距 + 圆角 6px + 淡阴影。组件全局注册 `<section-card title="...">`,支持 `#title` / `#extra` slot。
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import request from '@/axios';
|
||||||
|
|
||||||
|
export const getList = (current, size, params) => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-system/measurement-unit/list',
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
current,
|
||||||
|
size,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDetail = id => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-system/measurement-unit/detail',
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const remove = ids => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-system/measurement-unit/remove',
|
||||||
|
method: 'post',
|
||||||
|
params: {
|
||||||
|
ids,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const submit = row => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-system/measurement-unit/submit',
|
||||||
|
method: 'post',
|
||||||
|
data: row,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const changeStatus = (id, status) => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-system/measurement-unit/status',
|
||||||
|
method: 'post',
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -92,13 +92,12 @@ export const createCrudApi = baseUrl => ({
|
|||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
reassign(id) {
|
reassign(data) {
|
||||||
|
const payload = data && typeof data === 'object' ? data : { id: data };
|
||||||
return request({
|
return request({
|
||||||
url: `${baseUrl}/reassign`,
|
url: `${baseUrl}/reassign`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
params: {
|
data: payload,
|
||||||
id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
batchComplete(ids) {
|
batchComplete(ids) {
|
||||||
|
|||||||
@@ -59,14 +59,14 @@ export const withdraw = id =>
|
|||||||
params: { id },
|
params: { id },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const startChange = (id, reason) =>
|
export const startChange = (id, changeContent, changeReason = changeContent) =>
|
||||||
request({
|
request({
|
||||||
url: `${baseUrl}/start-change`,
|
url: `${baseUrl}/start-change`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
params: {
|
params: {
|
||||||
id,
|
id,
|
||||||
changeContent: reason,
|
changeContent: changeContent || '',
|
||||||
changeReason: reason,
|
changeReason,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,14 @@ export const getDetail = (id, waybillId) =>
|
|||||||
...(waybillId === undefined ? {} : { waybillId }),
|
...(waybillId === undefined ? {} : { waybillId }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
export const getVoucherImages = waybillId =>
|
export const getVoucherImages = (waybillId, params = {}) =>
|
||||||
request({
|
request({
|
||||||
url: '/blade-transport/process-config/voucher-images',
|
url: '/blade-transport/process-config/voucher-images',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: { waybillId },
|
params: {
|
||||||
|
waybillId,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
export const submit = api.submit;
|
export const submit = api.submit;
|
||||||
export const remove = api.remove;
|
export const remove = api.remove;
|
||||||
|
|||||||
@@ -6,9 +6,23 @@ const api = createCrudApi(baseUrl);
|
|||||||
|
|
||||||
export const getList = api.getList;
|
export const getList = api.getList;
|
||||||
export const getDetail = api.getDetail;
|
export const getDetail = api.getDetail;
|
||||||
|
export const getChangeRecordDetail = (id, recordIndex) =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/change-record/detail`,
|
||||||
|
method: 'get',
|
||||||
|
params: { id, recordIndex },
|
||||||
|
});
|
||||||
export const submit = api.submit;
|
export const submit = api.submit;
|
||||||
export const remove = api.remove;
|
export const remove = api.remove;
|
||||||
|
|
||||||
|
// 项目资金使用风险统计(高风险 / 中风险数量),供列表头部快速筛选标签使用
|
||||||
|
export const getFundRiskStats = params =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/fund-risk-stats`,
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
export const saveDraft = row =>
|
export const saveDraft = row =>
|
||||||
request({
|
request({
|
||||||
url: `${baseUrl}/save-draft`,
|
url: `${baseUrl}/save-draft`,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export const submit = api.submit;
|
|||||||
export const dispatch = api.dispatch;
|
export const dispatch = api.dispatch;
|
||||||
export const remove = api.remove;
|
export const remove = api.remove;
|
||||||
export const copy = api.copy;
|
export const copy = api.copy;
|
||||||
export const cancel = api.cancel;
|
|
||||||
export const complete = api.complete;
|
export const complete = api.complete;
|
||||||
|
|
||||||
export const importTransportPlan = ({
|
export const importTransportPlan = ({
|
||||||
@@ -36,3 +35,27 @@ export const importTransportPlan = ({
|
|||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const validateTransportPlan = ({
|
||||||
|
file,
|
||||||
|
projectId,
|
||||||
|
projectName,
|
||||||
|
customerName,
|
||||||
|
contractId,
|
||||||
|
contractName,
|
||||||
|
}) => {
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('file', file);
|
||||||
|
data.append('projectId', projectId || '');
|
||||||
|
data.append('projectName', projectName || '');
|
||||||
|
data.append('customerName', customerName || '');
|
||||||
|
data.append('contractId', contractId || '');
|
||||||
|
data.append('contractName', contractName || '');
|
||||||
|
return request({
|
||||||
|
url: `${baseUrl}/validate-transport-plan`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
responseType: 'blob',
|
||||||
|
timeout: 60000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,7 +5,29 @@ const baseUrl = '/blade-transport/voucher-manage';
|
|||||||
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 getFolderPage = (voucherId, current, size, params) =>
|
||||||
|
request({ url: `${baseUrl}/folder-page`, method: 'get', params: { voucherId, current, size, ...params } });
|
||||||
|
export const getFolderDetail = (voucherId, plateNo) =>
|
||||||
|
request({ url: `${baseUrl}/folder-detail`, method: 'get', params: { voucherId, plateNo } });
|
||||||
|
export const replaceFolder = (voucherId, plateNo, file) => {
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('voucherId', voucherId);
|
||||||
|
data.append('plateNo', plateNo);
|
||||||
|
data.append('file', file);
|
||||||
|
return request({ url: `${baseUrl}/folder-replace`, method: 'post', data });
|
||||||
|
};
|
||||||
|
export const replaceFolderByObject = (voucherId, plateNo, fileInfo) =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/folder-replace-object`,
|
||||||
|
method: 'post',
|
||||||
|
params: { voucherId, plateNo, ...fileInfo },
|
||||||
|
timeout: 120000,
|
||||||
|
});
|
||||||
|
export const removeFolder = (voucherId, plateNo) =>
|
||||||
|
request({ url: `${baseUrl}/folder-remove`, method: 'post', params: { voucherId, plateNo } });
|
||||||
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
||||||
|
export const changeWaybillBatch = data =>
|
||||||
|
request({ url: `${baseUrl}/change-waybill-batch`, method: 'post', data });
|
||||||
export const createUploadDraft = data =>
|
export const createUploadDraft = data =>
|
||||||
request({ url: `${baseUrl}/upload-draft`, method: 'post', data });
|
request({ url: `${baseUrl}/upload-draft`, method: 'post', data });
|
||||||
export const completeUploadFile = data =>
|
export const completeUploadFile = data =>
|
||||||
@@ -17,6 +39,9 @@ export const getWaybillBatches = (current, size, params) =>
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
params: { current, size, ...params },
|
params: { current, size, ...params },
|
||||||
});
|
});
|
||||||
|
export const auditPass = id => request({ url: `${baseUrl}/audit-pass`, method: 'post', params: { id } });
|
||||||
|
export const auditReject = (id, rejectReason) =>
|
||||||
|
request({ url: `${baseUrl}/audit-reject`, method: 'post', params: { id, rejectReason } });
|
||||||
|
|
||||||
const fileTaskBaseUrl = '/blade-file/fileTask';
|
const fileTaskBaseUrl = '/blade-file/fileTask';
|
||||||
export const queryFileTask = md5 =>
|
export const queryFileTask = md5 =>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const api = createCrudApi(baseUrl);
|
|||||||
export const getList = api.getList;
|
export const getList = api.getList;
|
||||||
export const getDetail = api.getDetail;
|
export const getDetail = api.getDetail;
|
||||||
export const submit = api.submit;
|
export const submit = api.submit;
|
||||||
|
export const saveDraft = data => request({ url: `${baseUrl}/save-draft`, method: 'post', data });
|
||||||
export const remove = api.remove;
|
export const remove = api.remove;
|
||||||
export const copy = api.copy;
|
export const copy = api.copy;
|
||||||
export const cancel = api.cancel;
|
export const cancel = api.cancel;
|
||||||
@@ -25,6 +26,13 @@ export const maintainMileage = data =>
|
|||||||
method: 'post',
|
method: 'post',
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
/** 运单打卡记录 + 司机上传凭证图 */
|
||||||
|
export const getPunchRecords = waybillId =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/punch-records`,
|
||||||
|
method: 'get',
|
||||||
|
params: { waybillId },
|
||||||
|
});
|
||||||
export const roadLoading = ids =>
|
export const roadLoading = ids =>
|
||||||
request({
|
request({
|
||||||
url: `${baseUrl}/road-loading`,
|
url: `${baseUrl}/road-loading`,
|
||||||
@@ -35,9 +43,11 @@ export const roadLoading = ids =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getImportBatches = params => request({ url: `${baseUrl}/import-batch/list`, method: 'get', params });
|
export const getImportBatches = params => request({ url: `${baseUrl}/import-batch/list`, method: 'get', params });
|
||||||
|
export const getImportBatchNextCode = () => request({ url: `${baseUrl}/import-batch/next-code`, method: 'get' });
|
||||||
export const getImportDetails = params => request({ url: `${baseUrl}/import-batch/details`, method: 'get', params });
|
export const getImportDetails = params => request({ url: `${baseUrl}/import-batch/details`, method: 'get', params });
|
||||||
export const removeImportBatches = ids => request({ url: `${baseUrl}/import-batch/remove`, method: 'post', params: { ids } });
|
export const removeImportBatches = ids => request({ url: `${baseUrl}/import-batch/remove`, method: 'post', params: { ids } });
|
||||||
export const getImportOptions = () => request({ url: `${baseUrl}/import-batch/options`, method: 'get' });
|
export const getImportOptions = () => request({ url: `${baseUrl}/import-batch/options`, method: 'get' });
|
||||||
export const exportImportTemplate = () => request({ url: `${baseUrl}/import-batch/export-template`, method: 'get', responseType: 'blob' });
|
export const exportImportTemplate = () => request({ url: `${baseUrl}/import-batch/export-template`, method: 'get', responseType: 'blob' });
|
||||||
export const saveImportDraft = data => request({ url: `${baseUrl}/import-batch/draft`, method: 'post', data });
|
export const saveImportDraft = data => request({ url: `${baseUrl}/import-batch/draft`, method: 'post', data });
|
||||||
|
export const validateImport = data => request({ url: `${baseUrl}/import-batch/validate`, method: 'post', data, responseType: 'blob' });
|
||||||
export const confirmImport = data => request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data });
|
export const confirmImport = data => request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data });
|
||||||
|
|||||||
+3
-2
@@ -34,7 +34,8 @@ export const exportBlob = (url, params, options = {}) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const importBlob = (url, file) => {
|
export const importBlob = (url, file, options = {}) => {
|
||||||
|
const { timeout = 60000 } = options;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
return request({
|
return request({
|
||||||
@@ -42,6 +43,6 @@ export const importBlob = (url, file) => {
|
|||||||
method: 'post',
|
method: 'post',
|
||||||
data: formData,
|
data: formData,
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
timeout: 60000,
|
timeout,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ export const getAvailableOptions = (keyword, deptId, selectedId) =>
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
params: { keyword, deptId, selectedId },
|
params: { keyword, deptId, selectedId },
|
||||||
});
|
});
|
||||||
|
export const getAvailablePage = (current, size, keyword, deptId, selectedId) =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/available-page`,
|
||||||
|
method: 'get',
|
||||||
|
params: { current, size, keyword, deptId, selectedId },
|
||||||
|
});
|
||||||
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
export const submit = data => request({ url: `${baseUrl}/submit`, 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 } });
|
||||||
|
|
||||||
|
|||||||
@@ -18,17 +18,16 @@ export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'p
|
|||||||
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', params: { id } });
|
||||||
|
export const syncKingdeeBatch = ids =>
|
||||||
|
request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', data: ids });
|
||||||
|
export const syncKingdeeResult = () =>
|
||||||
|
request({ url: `${baseUrl}/sync-kingdee-result`, method: 'post' });
|
||||||
|
|
||||||
export const paymentTypeOptions = [
|
export const paymentTypeOptions = [
|
||||||
{ label: '项目预付', value: 'project_advance' },
|
{ label: '项目预付', value: 'project_advance' },
|
||||||
{ label: '进度预付', value: 'progress_advance' },
|
{ label: '进度预付', value: 'progress_advance' },
|
||||||
{ label: '结算付款', value: 'settlement_payment' },
|
{ label: '结算付款', value: 'settlement_payment' },
|
||||||
];
|
];
|
||||||
export const paymentMethodOptions = [
|
|
||||||
{ label: '银行转账', value: 'bank_transfer' },
|
|
||||||
{ label: '银行承兑汇票', value: 'bank_draft' },
|
|
||||||
{ label: '商业承兑汇票', value: 'commercial_draft' },
|
|
||||||
];
|
|
||||||
export const approvalStatusOptions = [
|
export const approvalStatusOptions = [
|
||||||
{ label: '草稿', value: 'draft' },
|
{ label: '草稿', value: 'draft' },
|
||||||
{ label: '审批中', value: 'reviewing' },
|
{ label: '审批中', value: 'reviewing' },
|
||||||
@@ -39,5 +38,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' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ const baseUrl = '/blade-transport/formal-settlement';
|
|||||||
|
|
||||||
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 exportList = params =>
|
||||||
|
request({ url: `${baseUrl}/export`, method: 'get', params, responseType: 'blob' });
|
||||||
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 getCandidates = (current, size, params) =>
|
export const getCandidates = (current, size, params) =>
|
||||||
request({
|
request({
|
||||||
@@ -44,3 +46,7 @@ export const applyPayment = data =>
|
|||||||
request({ url: `${baseUrl}/apply-payment`, method: 'post', data });
|
request({ url: `${baseUrl}/apply-payment`, method: 'post', data });
|
||||||
export const applyPayments = data =>
|
export const applyPayments = data =>
|
||||||
request({ url: `${baseUrl}/apply-payments`, method: 'post', data });
|
request({ url: `${baseUrl}/apply-payments`, method: 'post', data });
|
||||||
|
export const claimInvoices = data =>
|
||||||
|
request({ url: `${baseUrl}/claim-invoices`, method: 'post', data });
|
||||||
|
export const queryInvoicePool = params =>
|
||||||
|
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params });
|
||||||
|
|||||||
@@ -10,6 +10,18 @@ export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get'
|
|||||||
export const getContractOptions = keyword =>
|
export const getContractOptions = keyword =>
|
||||||
request({ url: `${baseUrl}/contract-options`, method: 'get', params: { keyword } });
|
request({ url: `${baseUrl}/contract-options`, method: 'get', params: { keyword } });
|
||||||
|
|
||||||
|
export const getContractList = (current, size, params) =>
|
||||||
|
request({
|
||||||
|
url: '/blade-transport/contract-manage/list',
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
current,
|
||||||
|
size,
|
||||||
|
approvalStatuses: 'approved,change_approved',
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const getFeeOptions = () => request({ url: `${baseUrl}/fee-options`, method: 'get' });
|
export const getFeeOptions = () => request({ url: `${baseUrl}/fee-options`, method: 'get' });
|
||||||
|
|
||||||
export const getCandidateDetails = (current, size, params) =>
|
export const getCandidateDetails = (current, size, params) =>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ const baseUrl = '/blade-transport/transport-reconciliation';
|
|||||||
|
|
||||||
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 exportList = params =>
|
||||||
|
request({ url: `${baseUrl}/export`, method: 'get', params, responseType: 'blob' });
|
||||||
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 getFormalOptions = (current, size, params) =>
|
export const getFormalOptions = (current, size, params) =>
|
||||||
request({
|
request({
|
||||||
@@ -14,17 +16,21 @@ export const getFormalOptions = (current, size, 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 match = id => request({ url: `${baseUrl}/match`, method: 'post', params: { id } });
|
export const match = id => request({ url: `${baseUrl}/match`, method: 'post', params: { id } });
|
||||||
|
export const matchPreview = data =>
|
||||||
|
request({ url: `${baseUrl}/match-preview`, method: 'post', data });
|
||||||
export const manualMatch = data =>
|
export const manualMatch = data =>
|
||||||
request({ url: `${baseUrl}/manual-match`, method: 'post', data });
|
request({ url: `${baseUrl}/manual-match`, method: 'post', data });
|
||||||
export const unmatch = internalId =>
|
export const unmatch = internalId =>
|
||||||
request({ url: `${baseUrl}/unmatch`, method: 'post', params: { internalId } });
|
request({ url: `${baseUrl}/unmatch`, method: 'post', params: { internalId } });
|
||||||
export const adjust = data => request({ url: `${baseUrl}/adjust`, method: 'post', data });
|
export const adjust = data => request({ url: `${baseUrl}/adjust`, method: 'post', data });
|
||||||
export const updateByMatch = id =>
|
export const updateByMatch = (id, data) =>
|
||||||
request({ url: `${baseUrl}/update-by-match`, method: 'post', params: { id } });
|
request({ url: `${baseUrl}/update-by-match`, method: 'post', params: { id }, data });
|
||||||
export const complete = id =>
|
export const complete = id =>
|
||||||
request({ url: `${baseUrl}/complete`, method: 'post', params: { id } });
|
request({ url: `${baseUrl}/complete`, method: 'post', params: { id } });
|
||||||
export const template = mode =>
|
export const completeWithData = data =>
|
||||||
request({ url: `${baseUrl}/template`, method: 'get', params: { mode }, responseType: 'blob' });
|
request({ url: `${baseUrl}/complete-with-data`, method: 'post', data });
|
||||||
|
export const template = (mode, params) =>
|
||||||
|
request({ url: `${baseUrl}/template`, method: 'get', params: { mode, ...params }, responseType: 'blob' });
|
||||||
|
|
||||||
const importFile = (url, id, file) => {
|
const importFile = (url, id, file) => {
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
|
|||||||
@@ -78,3 +78,10 @@ export const getDeptLazyTree = parentId => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getPlatformCompanySelect = () => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-system/dept/platform-company-select',
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -86,3 +86,10 @@ export const getDictionary = params => {
|
|||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDictionaryAll = () => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-system/dict-biz/select-all',
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import request from '@/axios';
|
||||||
|
|
||||||
|
const baseUrl = '/blade-transport/vehicle-dispatch';
|
||||||
|
|
||||||
|
export const getList = (current, size, params) =>
|
||||||
|
request({ url: `${baseUrl}/list`, method: 'get', params: { ...params, current, size } });
|
||||||
|
|
||||||
|
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
|
||||||
|
|
||||||
|
export const submit = row => request({ url: `${baseUrl}/submit`, method: 'post', data: row });
|
||||||
|
|
||||||
|
export const remove = ids => request({ url: `${baseUrl}/remove`, method: 'post', params: { ids } });
|
||||||
|
|
||||||
|
export const submitApproval = id =>
|
||||||
|
request({ url: `${baseUrl}/submit-approval`, method: 'post', params: { id } });
|
||||||
|
|
||||||
|
export const approve = id => request({ url: `${baseUrl}/approve`, method: 'post', params: { id } });
|
||||||
|
|
||||||
|
export const exportVehicleDispatch = params =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/export-vehicle-dispatch`,
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
@@ -47,3 +47,11 @@ export const update = row => {
|
|||||||
data: row,
|
data: row,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getExpiryStat = params => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-transport/annual-inspection-record/expiry-stat',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -113,6 +113,26 @@ export const getScoreTemplate = quantificationId => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const queryKingdeeCustomer = creditCode => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-transport/bfi/customer/query-by-credit-code',
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
creditCode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const syncKingdee = id => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-transport/customer-archive/sync-kingdee',
|
||||||
|
method: 'post',
|
||||||
|
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',
|
||||||
|
|||||||
@@ -223,6 +223,14 @@ 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));
|
return Promise.reject(new Error(error));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog v-model="visible" title="地图选择地址" width="920px" append-to-body @opened="initMap">
|
||||||
v-model="visible"
|
|
||||||
title="地图选择地址"
|
|
||||||
width="920px"
|
|
||||||
append-to-body
|
|
||||||
@opened="initMap"
|
|
||||||
>
|
|
||||||
<div class="address-map-picker__toolbar">
|
<div class="address-map-picker__toolbar">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="keyword"
|
v-model="keyword"
|
||||||
@@ -15,7 +9,14 @@
|
|||||||
/>
|
/>
|
||||||
<el-button type="primary" :loading="searching" @click="searchKeyword">搜索</el-button>
|
<el-button type="primary" :loading="searching" @click="searchKeyword">搜索</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div ref="map" class="address-map-picker__map"></div>
|
<div class="address-map-picker__content">
|
||||||
|
<map-search-results
|
||||||
|
v-if="searchResults.length"
|
||||||
|
:results="searchResults"
|
||||||
|
@select="selectSearchResult"
|
||||||
|
/>
|
||||||
|
<div ref="map" class="address-map-picker__map"></div>
|
||||||
|
</div>
|
||||||
<div class="address-map-picker__info">{{ status }}</div>
|
<div class="address-map-picker__info">{{ status }}</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="visible = false">取消</el-button>
|
<el-button @click="visible = false">取消</el-button>
|
||||||
@@ -54,6 +55,8 @@ export default {
|
|||||||
amap: null,
|
amap: null,
|
||||||
marker: null,
|
marker: null,
|
||||||
geocoder: null,
|
geocoder: null,
|
||||||
|
searchResults: [],
|
||||||
|
mapInitializing: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -65,6 +68,7 @@ export default {
|
|||||||
this.keyword = this.address || '';
|
this.keyword = this.address || '';
|
||||||
this.selected = {};
|
this.selected = {};
|
||||||
this.status = '可搜索地址或点击地图选点';
|
this.status = '可搜索地址或点击地图选点';
|
||||||
|
this.searchResults = [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -96,25 +100,39 @@ export default {
|
|||||||
return amapLoader;
|
return amapLoader;
|
||||||
},
|
},
|
||||||
initMap() {
|
initMap() {
|
||||||
this.loadAmap()
|
if (this.mapInitializing) return this.mapInitializing;
|
||||||
|
|
||||||
|
this.mapInitializing = this.loadAmap()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$nextTick(() => {
|
if (!window.AMap?.Map) {
|
||||||
if (!this.amap) {
|
throw new Error('高德地图组件未就绪');
|
||||||
this.amap = new window.AMap.Map(this.$refs.map, {
|
}
|
||||||
center: [116.40769, 39.89945],
|
return new Promise(resolve => {
|
||||||
zoom: 11,
|
this.$nextTick(() => {
|
||||||
});
|
if (!this.$refs.map) return resolve(null);
|
||||||
this.amap.on('click', event => this.pickPoint(event.lnglat));
|
if (!this.amap) {
|
||||||
} else {
|
this.amap = new window.AMap.Map(this.$refs.map, {
|
||||||
this.amap.resize();
|
center: [116.40769, 39.89945],
|
||||||
this.clearMarker();
|
zoom: 11,
|
||||||
}
|
});
|
||||||
|
this.amap.on('click', event => this.pickPoint(event.lnglat));
|
||||||
|
} else {
|
||||||
|
this.amap.resize();
|
||||||
|
this.clearMarker();
|
||||||
|
}
|
||||||
|
resolve(this.amap);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.$message.error('高德地图组件加载失败,请稍后重试');
|
this.$message.error('高德地图组件加载失败,请稍后重试');
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.mapInitializing = null;
|
||||||
});
|
});
|
||||||
|
return this.mapInitializing;
|
||||||
},
|
},
|
||||||
ensureGeocoder() {
|
ensureGeocoder() {
|
||||||
if (this.geocoder) return Promise.resolve(this.geocoder);
|
if (this.geocoder) return Promise.resolve(this.geocoder);
|
||||||
@@ -154,15 +172,28 @@ export default {
|
|||||||
this.$message.warning('请输入地址关键词');
|
this.$message.warning('请输入地址关键词');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.searching) return;
|
||||||
|
this.searchResults = [];
|
||||||
this.searching = true;
|
this.searching = true;
|
||||||
this.loadAmap()
|
this.initMap()
|
||||||
.then(() => this.runGeocode('location', keyword))
|
.then(map => (map ? this.runGeocode('location', keyword) : null))
|
||||||
.then(result => {
|
.then(result => {
|
||||||
const point = result.geocodes?.[0]?.location || result.location;
|
if (!result) return;
|
||||||
|
const geocodes = result.geocodes || (result.location ? [result] : []);
|
||||||
|
this.searchResults = geocodes.map((item, index) => ({
|
||||||
|
id: item.id || index,
|
||||||
|
name: item.formattedAddress || item.address || keyword,
|
||||||
|
address: item.formattedAddress || item.address || keyword,
|
||||||
|
location: item.location,
|
||||||
|
}));
|
||||||
|
const point = geocodes[0]?.location || result.location;
|
||||||
if (!point) {
|
if (!point) {
|
||||||
throw new Error('未找到匹配地址');
|
throw new Error('未找到匹配地址');
|
||||||
}
|
}
|
||||||
this.pickPoint(point, keyword);
|
return this.$nextTick().then(() => {
|
||||||
|
this.amap?.resize();
|
||||||
|
this.pickPoint(point, geocodes[0]?.formattedAddress || keyword);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.status = '未找到匹配地址';
|
this.status = '未找到匹配地址';
|
||||||
@@ -172,6 +203,11 @@ export default {
|
|||||||
this.searching = false;
|
this.searching = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
selectSearchResult(item) {
|
||||||
|
if (item && item.location) {
|
||||||
|
this.pickPoint(item.location, item.address || item.name || '');
|
||||||
|
}
|
||||||
|
},
|
||||||
pickPoint(point, fallbackAddress = '') {
|
pickPoint(point, fallbackAddress = '') {
|
||||||
const longitude = typeof point.getLng === 'function' ? point.getLng() : point.lng;
|
const longitude = typeof point.getLng === 'function' ? point.getLng() : point.lng;
|
||||||
const latitude = typeof point.getLat === 'function' ? point.getLat() : point.lat;
|
const latitude = typeof point.getLat === 'function' ? point.getLat() : point.lat;
|
||||||
@@ -181,7 +217,11 @@ export default {
|
|||||||
}
|
}
|
||||||
const lnglat = new window.AMap.LngLat(Number(longitude), Number(latitude));
|
const lnglat = new window.AMap.LngLat(Number(longitude), Number(latitude));
|
||||||
this.renderMarker(lnglat);
|
this.renderMarker(lnglat);
|
||||||
this.selected = { longitude: Number(longitude), latitude: Number(latitude), address: fallbackAddress };
|
this.selected = {
|
||||||
|
longitude: Number(longitude),
|
||||||
|
latitude: Number(latitude),
|
||||||
|
address: fallbackAddress,
|
||||||
|
};
|
||||||
this.status = '正在解析地址...';
|
this.status = '正在解析地址...';
|
||||||
this.resolving = true;
|
this.resolving = true;
|
||||||
this.runGeocode('address', lnglat)
|
this.runGeocode('address', lnglat)
|
||||||
@@ -238,11 +278,19 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__map {
|
&__map {
|
||||||
|
flex: 1 1 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
height: 420px;
|
height: 420px;
|
||||||
border: 1px solid #eff1f7;
|
border: 1px solid #eff1f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="results.length" class="map-search-results">
|
||||||
|
<div class="map-search-results__title">搜索结果</div>
|
||||||
|
<el-empty v-if="!results.length" description="暂无搜索结果" :image-size="60" />
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in results"
|
||||||
|
:key="item.id || index"
|
||||||
|
class="map-search-results__item"
|
||||||
|
@click="$emit('select', item)"
|
||||||
|
>
|
||||||
|
<div class="map-search-results__name">{{ item.name || item.address || '未命名地址' }}</div>
|
||||||
|
<div class="map-search-results__address">{{ item.address || item.name || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
results: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['select'],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.map-search-results {
|
||||||
|
width: 260px;
|
||||||
|
height: 420px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #eff1f7;
|
||||||
|
background: #fff;
|
||||||
|
flex: 0 0 260px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid #eff1f7;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item:hover {
|
||||||
|
background: #f5f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
color: #303133;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__address {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -15,10 +15,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import VuePdfEmbed from 'vue-pdf-embed';
|
import { defineAsyncComponent } from 'vue';
|
||||||
import 'vue-pdf-embed/dist/styles/annotationLayer.css';
|
import 'vue-pdf-embed/dist/styles/annotationLayer.css';
|
||||||
import 'vue-pdf-embed/dist/styles/textLayer.css';
|
import 'vue-pdf-embed/dist/styles/textLayer.css';
|
||||||
|
|
||||||
|
const VuePdfEmbed = defineAsyncComponent(() => import('vue-pdf-embed'));
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PdfPreview',
|
name: 'PdfPreview',
|
||||||
components: { VuePdfEmbed },
|
components: { VuePdfEmbed },
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
|||||||
import { baseUrl } from '@/config/env';
|
import { baseUrl } from '@/config/env';
|
||||||
import { getUploadHeaders } from '@/utils/upload';
|
import { getUploadHeaders } from '@/utils/upload';
|
||||||
|
|
||||||
const UNIFIED_ATTACHMENT_TIP =
|
const UNIFIED_ATTACHMENT_TIP_PREFIX =
|
||||||
'支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过500M';
|
'支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过';
|
||||||
|
|
||||||
const MIME_MAP = {
|
const MIME_MAP = {
|
||||||
jpg: 'image/jpeg',
|
jpg: 'image/jpeg',
|
||||||
@@ -215,7 +215,7 @@ export default {
|
|||||||
return this.acceptedList.join(',');
|
return this.acceptedList.join(',');
|
||||||
},
|
},
|
||||||
tipText() {
|
tipText() {
|
||||||
return this.tip || UNIFIED_ATTACHMENT_TIP;
|
return this.tip || `${UNIFIED_ATTACHMENT_TIP_PREFIX}${this.maxSize}M`;
|
||||||
},
|
},
|
||||||
uploadingCount() {
|
uploadingCount() {
|
||||||
return this.fileList.filter(file => ['ready', 'uploading'].includes(file.status)).length;
|
return this.fileList.filter(file => ['ready', 'uploading'].includes(file.status)).length;
|
||||||
@@ -335,7 +335,12 @@ export default {
|
|||||||
const data = (file.response && file.response.data) || file.data || file;
|
const data = (file.response && file.response.data) || file.data || file;
|
||||||
const url = data.link || data.url || data.domain || file.url || '';
|
const url = data.link || data.url || data.domain || file.url || '';
|
||||||
const originalName =
|
const originalName =
|
||||||
data.originalName || file.originalName || data.name || file.name || this.getFileName(url) || '附件';
|
data.originalName ||
|
||||||
|
file.originalName ||
|
||||||
|
file.name ||
|
||||||
|
data.name ||
|
||||||
|
this.getFileName(url) ||
|
||||||
|
'附件';
|
||||||
const extension = this.getExtension({ name: originalName, url });
|
const extension = this.getExtension({ name: originalName, url });
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
|
|||||||
+3
-1
@@ -43,6 +43,7 @@ import tenantPackage from './views/system/tenantpackage.vue';
|
|||||||
import tenantDatasource from './views/system/tenantdatasource.vue';
|
import tenantDatasource from './views/system/tenantdatasource.vue';
|
||||||
// 通用弹窗分组白卡(灰底弹窗内使用,标题在卡内)
|
// 通用弹窗分组白卡(灰底弹窗内使用,标题在卡内)
|
||||||
import sectionCard from './components/section-card/main.vue';
|
import sectionCard from './components/section-card/main.vue';
|
||||||
|
import mapSearchResults from './components/map-search-results/main.vue';
|
||||||
|
|
||||||
window.$crudCommon = crudCommon;
|
window.$crudCommon = crudCommon;
|
||||||
debug();
|
debug();
|
||||||
@@ -66,6 +67,7 @@ app.component('formSetting', formSetting);
|
|||||||
app.component('tenantPackage', tenantPackage);
|
app.component('tenantPackage', tenantPackage);
|
||||||
app.component('tenantDatasource', tenantDatasource);
|
app.component('tenantDatasource', tenantDatasource);
|
||||||
app.component('sectionCard', sectionCard);
|
app.component('sectionCard', sectionCard);
|
||||||
|
app.component('mapSearchResults', mapSearchResults);
|
||||||
app.config.globalProperties.$app = app;
|
app.config.globalProperties.$app = app;
|
||||||
app.config.globalProperties.$dayjs = dayjs;
|
app.config.globalProperties.$dayjs = dayjs;
|
||||||
app.config.globalProperties.website = website;
|
app.config.globalProperties.website = website;
|
||||||
@@ -81,7 +83,7 @@ app.use(ElementPlus, {
|
|||||||
});
|
});
|
||||||
app.use(Avue, {
|
app.use(Avue, {
|
||||||
axios,
|
axios,
|
||||||
calcHeight: 10,
|
calcHeight: -26,
|
||||||
crudOption: {
|
crudOption: {
|
||||||
searchIcon: true,
|
searchIcon: true,
|
||||||
searchIndex: 4,
|
searchIndex: 4,
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const getCargoTypeOption = ctx => ({
|
|||||||
height: 'auto',
|
height: 'auto',
|
||||||
calcHeight: 22,
|
calcHeight: 22,
|
||||||
dialogWidth: 760,
|
dialogWidth: 760,
|
||||||
|
// 货物类型弹窗单独使用样式,避免全局 Avue footer 上移遮挡底部备注字段
|
||||||
|
dialogCustomClass: 'cargo-type-dialog',
|
||||||
labelPosition: 'right',
|
labelPosition: 'right',
|
||||||
labelWidth: 'auto',
|
labelWidth: 'auto',
|
||||||
tip: false,
|
tip: false,
|
||||||
@@ -172,6 +174,7 @@ export const getCargoTypeOption = ctx => ({
|
|||||||
label: '备注',
|
label: '备注',
|
||||||
prop: 'remark',
|
prop: 'remark',
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
|
formslot: true,
|
||||||
minRows: 2,
|
minRows: 2,
|
||||||
span: 24,
|
span: 24,
|
||||||
minWidth: 180,
|
minWidth: 180,
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
const dimensionOptions = [
|
||||||
|
{ label: '重量', value: '重量' },
|
||||||
|
{ label: '体积', value: '体积' },
|
||||||
|
{ label: '数量', value: '数量' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: '启用', value: 1 },
|
||||||
|
{ label: '停用', value: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const createOption = () => ({
|
||||||
|
height: 'auto',
|
||||||
|
calcHeight: 32,
|
||||||
|
dialogWidth: 680,
|
||||||
|
labelPosition: 'right',
|
||||||
|
labelWidth: 'auto',
|
||||||
|
tip: false,
|
||||||
|
searchBtnText: '查询',
|
||||||
|
emptyBtnText: '重置',
|
||||||
|
saveBtnText: '提交',
|
||||||
|
updateBtnText: '提交',
|
||||||
|
searchShow: true,
|
||||||
|
searchMenuSpan: 24,
|
||||||
|
searchIcon: true,
|
||||||
|
searchIndex: 4,
|
||||||
|
searchMenuPosition: 'right',
|
||||||
|
border: true,
|
||||||
|
index: true,
|
||||||
|
indexLabel: '序号',
|
||||||
|
indexWidth: 70,
|
||||||
|
addBtn: false,
|
||||||
|
viewBtn: false,
|
||||||
|
editBtn: false,
|
||||||
|
delBtn: false,
|
||||||
|
selection: true,
|
||||||
|
dialogClickModal: false,
|
||||||
|
menuWidth: 240,
|
||||||
|
menuFixed: 'right',
|
||||||
|
column: [
|
||||||
|
{
|
||||||
|
label: '计量单位',
|
||||||
|
prop: 'unitName',
|
||||||
|
minWidth: 150,
|
||||||
|
search: true,
|
||||||
|
searchOrder: 3,
|
||||||
|
searchSpan: 6,
|
||||||
|
maxlength: 50,
|
||||||
|
showWordLimit: true,
|
||||||
|
rules: [
|
||||||
|
{ required: true, message: '请输入计量单位', trigger: 'blur' },
|
||||||
|
{ max: 50, message: '计量单位不能超过50个字', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '计量维度',
|
||||||
|
prop: 'dimension',
|
||||||
|
type: 'select',
|
||||||
|
minWidth: 130,
|
||||||
|
search: true,
|
||||||
|
searchOrder: 2,
|
||||||
|
searchSpan: 6,
|
||||||
|
dicData: dimensionOptions,
|
||||||
|
rules: [{ required: true, message: '请选择计量维度', trigger: 'change' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '备注',
|
||||||
|
prop: 'remark',
|
||||||
|
type: 'textarea',
|
||||||
|
minRows: 2,
|
||||||
|
span: 24,
|
||||||
|
minWidth: 200,
|
||||||
|
maxlength: 200,
|
||||||
|
showWordLimit: true,
|
||||||
|
overHidden: true,
|
||||||
|
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '状态',
|
||||||
|
prop: 'status',
|
||||||
|
type: 'select',
|
||||||
|
search: true,
|
||||||
|
searchOrder: 1,
|
||||||
|
searchSpan: 6,
|
||||||
|
slot: true,
|
||||||
|
dataType: 'number',
|
||||||
|
dicData: statusOptions,
|
||||||
|
clearable: true,
|
||||||
|
value: 1,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
viewDisplay: false,
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '更新人',
|
||||||
|
prop: 'updateUserName',
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
viewDisplay: false,
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创建时间',
|
||||||
|
prop: 'createTime',
|
||||||
|
type: 'datetime',
|
||||||
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
sortable: true,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '更新时间',
|
||||||
|
prop: 'updateTime',
|
||||||
|
type: 'datetime',
|
||||||
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
sortable: true,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -2,7 +2,6 @@ import {
|
|||||||
auditColumns,
|
auditColumns,
|
||||||
createCrudOption,
|
createCrudOption,
|
||||||
packageOptions,
|
packageOptions,
|
||||||
priceUnitOptions,
|
|
||||||
textRule,
|
textRule,
|
||||||
} from './common';
|
} from './common';
|
||||||
|
|
||||||
@@ -25,10 +24,42 @@ export const config = {
|
|||||||
enableAllDept: false,
|
enableAllDept: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 单价统一按两位小数展示;格式非法时保留原值,交由表单校验提示。
|
||||||
|
export const formatCargoValue = value => {
|
||||||
|
if (value === undefined || value === null) return '';
|
||||||
|
const raw = String(value).trim();
|
||||||
|
if (!raw || raw === '-1') return '';
|
||||||
|
if (!/^\d+(?:\.\d+)?$/.test(raw)) return raw;
|
||||||
|
const number = Number(raw);
|
||||||
|
return Number.isFinite(number) ? number.toFixed(2) : raw;
|
||||||
|
};
|
||||||
|
|
||||||
export const option = createCrudOption([
|
export const option = createCrudOption([
|
||||||
|
{
|
||||||
|
label: '一级货物类型',
|
||||||
|
prop: 'firstCargoTypeName',
|
||||||
|
formslot: true,
|
||||||
|
order: 60,
|
||||||
|
span: 12,
|
||||||
|
minWidth: 150,
|
||||||
|
editDisabled: true,
|
||||||
|
rules: [{ required: true, message: '请选择一级货物类型', trigger: 'change' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '二级货物类型',
|
||||||
|
prop: 'secondCargoTypeName',
|
||||||
|
formslot: true,
|
||||||
|
order: 59,
|
||||||
|
span: 12,
|
||||||
|
minWidth: 150,
|
||||||
|
editDisabled: true,
|
||||||
|
rules: [{ required: true, message: '请选择二级货物类型', trigger: 'change' }],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '货物名称',
|
label: '货物名称',
|
||||||
prop: 'cargoName',
|
prop: 'cargoName',
|
||||||
|
order: 50,
|
||||||
|
span: 12,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
placeholder: '请输入',
|
placeholder: '请输入',
|
||||||
rules: textRule('货物名称', 100, true),
|
rules: textRule('货物名称', 100, true),
|
||||||
@@ -37,6 +68,8 @@ export const option = createCrudOption([
|
|||||||
label: '货物编号',
|
label: '货物编号',
|
||||||
prop: 'cargoCode',
|
prop: 'cargoCode',
|
||||||
formslot: true,
|
formslot: true,
|
||||||
|
order: 49,
|
||||||
|
span: 12,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
maxlength: 20,
|
maxlength: 20,
|
||||||
showWordLimit: true,
|
showWordLimit: true,
|
||||||
@@ -45,14 +78,6 @@ export const option = createCrudOption([
|
|||||||
{ max: 20, message: '货物编号不能超过20个字符', trigger: 'blur' },
|
{ max: 20, message: '货物编号不能超过20个字符', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: '一级货物类型',
|
|
||||||
prop: 'firstCargoTypeName',
|
|
||||||
formslot: true,
|
|
||||||
minWidth: 150,
|
|
||||||
editDisabled: true,
|
|
||||||
rules: [{ required: true, message: '请选择一级货物类型', trigger: 'change' }],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: '一级货物类型编码',
|
label: '一级货物类型编码',
|
||||||
prop: 'firstCargoTypeCode',
|
prop: 'firstCargoTypeCode',
|
||||||
@@ -61,14 +86,6 @@ export const option = createCrudOption([
|
|||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisabled: true,
|
editDisabled: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: '二级货物类型',
|
|
||||||
prop: 'secondCargoTypeName',
|
|
||||||
formslot: true,
|
|
||||||
minWidth: 150,
|
|
||||||
editDisabled: true,
|
|
||||||
rules: [{ required: true, message: '请选择二级货物类型', trigger: 'change' }],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: '二级货物类型编码',
|
label: '二级货物类型编码',
|
||||||
prop: 'secondCargoTypeCode',
|
prop: 'secondCargoTypeCode',
|
||||||
@@ -174,12 +191,16 @@ export const option = createCrudOption([
|
|||||||
prop: 'packageType',
|
prop: 'packageType',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
dicData: packageOptions,
|
dicData: packageOptions,
|
||||||
|
order: 39,
|
||||||
|
span: 12,
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '品牌',
|
label: '品牌',
|
||||||
prop: 'brand',
|
prop: 'brand',
|
||||||
search: false,
|
search: false,
|
||||||
|
order: 40,
|
||||||
|
span: 12,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
placeholder: '请输入',
|
placeholder: '请输入',
|
||||||
rules: textRule('品牌', 50),
|
rules: textRule('品牌', 50),
|
||||||
@@ -187,22 +208,18 @@ export const option = createCrudOption([
|
|||||||
{
|
{
|
||||||
label: '规格',
|
label: '规格',
|
||||||
prop: 'specification',
|
prop: 'specification',
|
||||||
|
order: 29,
|
||||||
|
span: 12,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
placeholder: '请输入',
|
placeholder: '请输入',
|
||||||
rules: textRule('规格', 50),
|
rules: textRule('规格', 50),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '型号',
|
label: '货值',
|
||||||
prop: 'model',
|
|
||||||
minWidth: 120,
|
|
||||||
placeholder: '请输入',
|
|
||||||
rules: textRule('型号', 50),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '单价',
|
|
||||||
prop: 'cargoValue',
|
prop: 'cargoValue',
|
||||||
formslot: true,
|
formslot: true,
|
||||||
formatter: row => (String(row.cargoValue) === '-1' ? '' : row.cargoValue),
|
order: 30,
|
||||||
|
formatter: row => formatCargoValue(row.cargoValue),
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
@@ -225,10 +242,19 @@ export const option = createCrudOption([
|
|||||||
label: '计价单位',
|
label: '计价单位',
|
||||||
prop: 'priceUnit',
|
prop: 'priceUnit',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
dicData: priceUnitOptions,
|
dicData: [],
|
||||||
display: false,
|
display: false,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '型号',
|
||||||
|
prop: 'model',
|
||||||
|
order: 19,
|
||||||
|
span: 12,
|
||||||
|
minWidth: 120,
|
||||||
|
placeholder: '请输入',
|
||||||
|
rules: textRule('型号', 50, true),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '尺寸',
|
label: '尺寸',
|
||||||
prop: 'sizeText',
|
prop: 'sizeText',
|
||||||
@@ -238,23 +264,19 @@ export const option = createCrudOption([
|
|||||||
rules: textRule('尺寸', 100),
|
rules: textRule('尺寸', 100),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '其他说明1',
|
label: '说明',
|
||||||
prop: 'descriptionOne',
|
prop: 'descriptionOne',
|
||||||
|
order: 20,
|
||||||
|
span: 12,
|
||||||
minWidth: 160,
|
minWidth: 160,
|
||||||
placeholder: '请输入',
|
placeholder: '请输入',
|
||||||
rules: textRule('其他说明1', 100),
|
rules: textRule('说明', 100),
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '其他说明2',
|
|
||||||
prop: 'descriptionTwo',
|
|
||||||
minWidth: 160,
|
|
||||||
placeholder: '请输入',
|
|
||||||
rules: textRule('其他说明2', 100),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '备注',
|
label: '备注',
|
||||||
prop: 'remark',
|
prop: 'remark',
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
|
order: 10,
|
||||||
minRows: 2,
|
minRows: 2,
|
||||||
span: 24,
|
span: 24,
|
||||||
maxlength: 200,
|
maxlength: 200,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const planStatusOptions = [
|
|||||||
export const waybillStatusOptions = [
|
export const waybillStatusOptions = [
|
||||||
{ label: '草稿', value: 'draft' },
|
{ label: '草稿', value: 'draft' },
|
||||||
{ label: '待执行', value: 'pending' },
|
{ label: '待执行', value: 'pending' },
|
||||||
{ label: '进行中', value: 'processing' },
|
{ label: '进行中', value: 'running' },
|
||||||
{ label: '已完成', value: 'completed' },
|
{ label: '已完成', value: 'completed' },
|
||||||
{ label: '已取消', value: 'cancelled' },
|
{ label: '已取消', value: 'cancelled' },
|
||||||
];
|
];
|
||||||
@@ -49,7 +49,6 @@ export const waybillStatusOptions = [
|
|||||||
export const dataSourceOptions = [
|
export const dataSourceOptions = [
|
||||||
{ label: '手工创建', value: '手工创建' },
|
{ label: '手工创建', value: '手工创建' },
|
||||||
{ label: '批量导入', value: '批量导入' },
|
{ label: '批量导入', value: '批量导入' },
|
||||||
{ label: '模板生成', value: '模板生成' },
|
|
||||||
{ label: '计划调度', value: '计划调度' },
|
{ label: '计划调度', value: '计划调度' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -98,6 +97,11 @@ export const businessTypeOptions = [
|
|||||||
{ label: '普通业务', value: '普通业务' },
|
{ label: '普通业务', value: '普通业务' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const businessModeOptions = [
|
||||||
|
{ label: '国内运输', value: '国内运输' },
|
||||||
|
{ label: '跨境运输', value: '跨境运输' },
|
||||||
|
];
|
||||||
|
|
||||||
export const settlementModeOptions = [
|
export const settlementModeOptions = [
|
||||||
{ label: '先票后款', value: '先票后款' },
|
{ label: '先票后款', value: '先票后款' },
|
||||||
{ label: '先款后票', value: '先款后票' },
|
{ label: '先款后票', value: '先款后票' },
|
||||||
@@ -126,12 +130,18 @@ export const contractApprovalStatusOptions = [
|
|||||||
{ label: '草稿', value: 'draft' },
|
{ label: '草稿', value: 'draft' },
|
||||||
{ label: '审批中', value: 'reviewing' },
|
{ label: '审批中', value: 'reviewing' },
|
||||||
{ label: '已驳回', value: 'rejected' },
|
{ label: '已驳回', value: 'rejected' },
|
||||||
|
{ label: '已撤回', value: 'withdrawn' },
|
||||||
{ label: '审批通过', value: 'approved' },
|
{ label: '审批通过', value: 'approved' },
|
||||||
{ label: '变更审批中', value: 'change_reviewing' },
|
{ label: '变更审批中', value: 'change_reviewing' },
|
||||||
{ label: '变更驳回', value: 'change_rejected' },
|
{ label: '变更驳回', value: 'change_rejected' },
|
||||||
{ label: '变更审批通过', value: 'change_approved' },
|
{ label: '变更审批通过', value: 'change_approved' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const contractArchiveStatusOptions = [
|
||||||
|
{ label: '未归档', value: '未归档' },
|
||||||
|
{ label: '已归档', value: '已归档' },
|
||||||
|
];
|
||||||
|
|
||||||
export const nodeOptions = [
|
export const nodeOptions = [
|
||||||
{ label: '接单', value: '接单' },
|
{ label: '接单', value: '接单' },
|
||||||
{ label: '到场', value: '到场' },
|
{ label: '到场', value: '到场' },
|
||||||
@@ -170,6 +180,32 @@ export const nonNegativeRule = label => ({
|
|||||||
trigger: 'blur',
|
trigger: 'blur',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 日期必须晚于当前日期(当天不可选):选择后立即校验,trigger 固定为 change
|
||||||
|
// 取值支持 YYYY-MM-DD / YYYY-MM-DD HH:mm:ss / 时间戳
|
||||||
|
export const futureDateRule = label => ({
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const raw = String(value).trim().replace(/-/g, '/');
|
||||||
|
const selected = new Date(raw);
|
||||||
|
if (Number.isNaN(selected.getTime())) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
selected.setHours(0, 0, 0, 0);
|
||||||
|
if (selected.getTime() <= today.getTime()) {
|
||||||
|
callback(new Error(`${label}必须晚于当前日期`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
});
|
||||||
|
|
||||||
export const phoneRule = label => ({
|
export const phoneRule = label => ({
|
||||||
pattern: /^1[3-9]\d{9}$/,
|
pattern: /^1[3-9]\d{9}$/,
|
||||||
message: `${label}格式不正确`,
|
message: `${label}格式不正确`,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
auditColumns,
|
auditColumns,
|
||||||
contractApprovalStatusOptions,
|
contractApprovalStatusOptions,
|
||||||
|
contractArchiveStatusOptions,
|
||||||
contractCategoryOptions,
|
contractCategoryOptions,
|
||||||
contractStageOptions,
|
contractStageOptions,
|
||||||
createCrudOption,
|
createCrudOption,
|
||||||
@@ -27,6 +28,12 @@ const listDicFormatter = res => {
|
|||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const projectNameDicFormatter = res => {
|
||||||
|
const list = listDicFormatter(res);
|
||||||
|
// 过滤掉 projectName 为空的记录
|
||||||
|
return list.filter(item => item && item.projectName && String(item.projectName).trim());
|
||||||
|
};
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
title: '合同管理',
|
title: '合同管理',
|
||||||
permission: 'contract_manage',
|
permission: 'contract_manage',
|
||||||
@@ -42,7 +49,7 @@ export const config = {
|
|||||||
batchDelete: false,
|
batchDelete: false,
|
||||||
enableProjectSelect: true,
|
enableProjectSelect: true,
|
||||||
projectQueryParams: {
|
projectQueryParams: {
|
||||||
approvalStatuses: 'approved,change_approved',
|
contractSelectable: true,
|
||||||
},
|
},
|
||||||
actions: ['copy'],
|
actions: ['copy'],
|
||||||
statusProp: 'approvalStatus',
|
statusProp: 'approvalStatus',
|
||||||
@@ -65,6 +72,10 @@ export const config = {
|
|||||||
['legalSealFlag', '是否需要加盖法人章'],
|
['legalSealFlag', '是否需要加盖法人章'],
|
||||||
['copyCount', '一式(份)'],
|
['copyCount', '一式(份)'],
|
||||||
['paymentDays', '回款账期(天)'],
|
['paymentDays', '回款账期(天)'],
|
||||||
|
['contractAmount', '合同金额'],
|
||||||
|
['templateFlag', '是否范本'],
|
||||||
|
['originalContractNo', '原件合同编号'],
|
||||||
|
['electronicSealFlag', '是否电子章'],
|
||||||
['remark', '备注', 2],
|
['remark', '备注', 2],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -78,6 +89,7 @@ export const config = {
|
|||||||
['effectiveType', '生效类型'],
|
['effectiveType', '生效类型'],
|
||||||
['contractStage', '合同阶段'],
|
['contractStage', '合同阶段'],
|
||||||
['approvalStatus', '审核状态'],
|
['approvalStatus', '审核状态'],
|
||||||
|
['archiveStatus', '归档状态'],
|
||||||
['currentNode', '当前节点'],
|
['currentNode', '当前节点'],
|
||||||
['currentProcessor', '当前处理人'],
|
['currentProcessor', '当前处理人'],
|
||||||
],
|
],
|
||||||
@@ -98,7 +110,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
deleteStatus: ['draft'],
|
deleteStatus: ['draft'],
|
||||||
deleteStage: ['draft'],
|
deleteStage: ['draft'],
|
||||||
editStatus: ['draft', 'rejected', 'change_rejected'],
|
editStatus: ['draft', 'withdrawn', 'rejected', 'change_rejected'],
|
||||||
operations: [
|
operations: [
|
||||||
{
|
{
|
||||||
action: 'flow',
|
action: 'flow',
|
||||||
@@ -265,8 +277,8 @@ export const option = createCrudOption([
|
|||||||
formslot: true,
|
formslot: true,
|
||||||
order: 165,
|
order: 165,
|
||||||
dicUrl:
|
dicUrl:
|
||||||
'/blade-transport/project-apply/list?current=1&size=9999&approvalStatuses=approved,change_approved',
|
'/blade-transport/project-apply/list?current=1&size=9999&contractSelectable=true',
|
||||||
dicFormatter: listDicFormatter,
|
dicFormatter: projectNameDicFormatter,
|
||||||
props: {
|
props: {
|
||||||
label: 'projectName',
|
label: 'projectName',
|
||||||
value: 'projectName',
|
value: 'projectName',
|
||||||
@@ -479,6 +491,47 @@ export const option = createCrudOption([
|
|||||||
order: 50,
|
order: 50,
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '合同金额',
|
||||||
|
prop: 'contractAmount',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
precision: 2,
|
||||||
|
hide: true,
|
||||||
|
order: 45,
|
||||||
|
minWidth: 130,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '是否范本',
|
||||||
|
prop: 'templateFlag',
|
||||||
|
type: 'select',
|
||||||
|
dicData: [
|
||||||
|
{ label: '否', value: 0 },
|
||||||
|
{ label: '是', value: 1 },
|
||||||
|
],
|
||||||
|
hide: true,
|
||||||
|
order: 40,
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '原件合同编号',
|
||||||
|
prop: 'originalContractNo',
|
||||||
|
hide: true,
|
||||||
|
order: 35,
|
||||||
|
minWidth: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '是否电子章',
|
||||||
|
prop: 'electronicSealFlag',
|
||||||
|
type: 'select',
|
||||||
|
dicData: [
|
||||||
|
{ label: '否', value: 0 },
|
||||||
|
{ label: '是', value: 1 },
|
||||||
|
],
|
||||||
|
hide: true,
|
||||||
|
order: 30,
|
||||||
|
minWidth: 110,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
prop: 'createTime',
|
prop: 'createTime',
|
||||||
@@ -516,6 +569,19 @@ export const option = createCrudOption([
|
|||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '归档状态',
|
||||||
|
prop: 'archiveStatus',
|
||||||
|
type: 'select',
|
||||||
|
search: true,
|
||||||
|
searchOrder: 5,
|
||||||
|
searchPlaceholder: '请选择',
|
||||||
|
slot: true,
|
||||||
|
dicData: contractArchiveStatusOptions,
|
||||||
|
minWidth: 110,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '当前节点',
|
label: '当前节点',
|
||||||
prop: 'currentNode',
|
prop: 'currentNode',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const transportTypeDict = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const carrierTypeOptions = ['承运商', '自运', '网货平台'];
|
export const carrierTypeOptions = ['承运商', '自运'];
|
||||||
|
|
||||||
export const loadingStatusOptions = [
|
export const loadingStatusOptions = [
|
||||||
{ label: '草稿', value: 'draft' },
|
{ label: '草稿', value: 'draft' },
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ export const option = {
|
|||||||
searchPlaceholder: '请选择',
|
searchPlaceholder: '请选择',
|
||||||
dicData: processStatusOptions,
|
dicData: processStatusOptions,
|
||||||
slot: true,
|
slot: true,
|
||||||
hide: true,
|
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
auditColumns,
|
auditColumns,
|
||||||
|
businessModeOptions,
|
||||||
businessTypeOptions,
|
businessTypeOptions,
|
||||||
createCrudOption,
|
createCrudOption,
|
||||||
nonNegativeRule,
|
nonNegativeRule,
|
||||||
@@ -16,6 +17,18 @@ const projectEffectiveTypeOptions = [
|
|||||||
{ label: '正式', value: 'formal' },
|
{ label: '正式', value: 'formal' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const fundUseRiskOptions = [
|
||||||
|
{ label: '高风险', value: 'high' },
|
||||||
|
{ label: '中风险', value: 'medium' },
|
||||||
|
{ label: '正常', value: 'none' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 项目资金额度使用率阈值:中风险 ≥ 80%,高风险 ≥ 90%
|
||||||
|
export const fundUseRiskThreshold = {
|
||||||
|
high: 90,
|
||||||
|
medium: 80,
|
||||||
|
};
|
||||||
|
|
||||||
const amountRules = label => [
|
const amountRules = label => [
|
||||||
{ required: true, message: `请输入${label}`, trigger: 'blur' },
|
{ required: true, message: `请输入${label}`, trigger: 'blur' },
|
||||||
nonNegativeRule(label),
|
nonNegativeRule(label),
|
||||||
@@ -117,7 +130,7 @@ export const option = createCrudOption([
|
|||||||
hide: true,
|
hide: true,
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
maxlength: 20,
|
maxlength: 20,
|
||||||
rules: textRule('项目简称', 20, true),
|
rules: textRule('项目简称', 20),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '项目类型',
|
label: '项目类型',
|
||||||
@@ -156,16 +169,16 @@ export const option = createCrudOption([
|
|||||||
rules: textRule('业务部门', 50, true),
|
rules: textRule('业务部门', 50, true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '承办部门',
|
label: '平台公司',
|
||||||
prop: 'undertakeDeptName',
|
prop: 'undertakeDeptName',
|
||||||
search: true,
|
search: true,
|
||||||
searchPlaceholder: '请输入',
|
searchPlaceholder: '请输入',
|
||||||
searchOrder: 8,
|
searchOrder: 8,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
rules: textRule('承办部门', 50, true),
|
rules: textRule('平台公司', 50, true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '承办部门ID',
|
label: '平台公司ID',
|
||||||
prop: 'undertakeDeptId',
|
prop: 'undertakeDeptId',
|
||||||
hide: true,
|
hide: true,
|
||||||
},
|
},
|
||||||
@@ -179,6 +192,15 @@ export const option = createCrudOption([
|
|||||||
formatter: row => formatUserRealName(row, 'principal'),
|
formatter: row => formatUserRealName(row, 'principal'),
|
||||||
rules: textRule('项目负责人', 50, true),
|
rules: textRule('项目负责人', 50, true),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '资金使用风险',
|
||||||
|
prop: 'fundUseRisk',
|
||||||
|
slot: true,
|
||||||
|
minWidth: 150,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
hide: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '项目负责人ID',
|
label: '项目负责人ID',
|
||||||
prop: 'principalUserId',
|
prop: 'principalUserId',
|
||||||
@@ -293,6 +315,15 @@ export const option = createCrudOption([
|
|||||||
hide: true,
|
hide: true,
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '业务模式',
|
||||||
|
prop: 'businessMode',
|
||||||
|
type: 'select',
|
||||||
|
dicData: businessModeOptions,
|
||||||
|
hide: true,
|
||||||
|
minWidth: 120,
|
||||||
|
rules: [selectRule('业务模式')],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '项目规模(万元)',
|
label: '项目规模(万元)',
|
||||||
prop: 'projectScale',
|
prop: 'projectScale',
|
||||||
@@ -311,6 +342,15 @@ export const option = createCrudOption([
|
|||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
rules: [nonNegativeRule('预计利润')],
|
rules: [nonNegativeRule('预计利润')],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '利润率(%)',
|
||||||
|
prop: 'profitRate',
|
||||||
|
type: 'number',
|
||||||
|
precision: 2,
|
||||||
|
hide: true,
|
||||||
|
minWidth: 120,
|
||||||
|
rules: [nonNegativeRule('利润率')],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '资金需求(万元)',
|
label: '资金需求(万元)',
|
||||||
prop: 'fundDemand',
|
prop: 'fundDemand',
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ export const option = {
|
|||||||
order: 270,
|
order: 270,
|
||||||
hide: true,
|
hide: true,
|
||||||
labelWidth: '0px',
|
labelWidth: '0px',
|
||||||
className: 'business-crud-page__full-form-item',
|
className: 'shipping-template-page__full-form-item',
|
||||||
},
|
},
|
||||||
{ label: '运费信息', prop: 'freightJson', hide: true, display: false },
|
{ label: '运费信息', prop: 'freightJson', hide: true, display: false },
|
||||||
{
|
{
|
||||||
@@ -290,7 +290,7 @@ export const option = {
|
|||||||
order: 250,
|
order: 250,
|
||||||
hide: true,
|
hide: true,
|
||||||
labelWidth: '0px',
|
labelWidth: '0px',
|
||||||
className: 'business-crud-page__full-form-item',
|
className: 'shipping-template-page__full-form-item',
|
||||||
},
|
},
|
||||||
{ label: '项目ID', prop: 'projectId', hide: true, display: false },
|
{ label: '项目ID', prop: 'projectId', hide: true, display: false },
|
||||||
{ label: '客户合同ID', prop: 'contractId', hide: true, display: false },
|
{ label: '客户合同ID', prop: 'contractId', hide: true, display: false },
|
||||||
@@ -300,7 +300,14 @@ export const option = {
|
|||||||
{ label: '收货地', prop: 'arrivalName', hide: true, display: false },
|
{ label: '收货地', prop: 'arrivalName', hide: true, display: false },
|
||||||
{ label: '收货联系人', prop: 'arrivalContact', hide: true, display: false },
|
{ label: '收货联系人', prop: 'arrivalContact', hide: true, display: false },
|
||||||
{ label: '收货联系方式', prop: 'arrivalPhone', hide: true, display: false },
|
{ label: '收货联系方式', prop: 'arrivalPhone', hide: true, display: false },
|
||||||
{ label: '所属组织', prop: 'deptName', minWidth: 150, addDisplay: false, editDisplay: false, display: false },
|
{
|
||||||
|
label: '所属组织',
|
||||||
|
prop: 'deptName',
|
||||||
|
minWidth: 150,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
...auditColumns.map(column => ({
|
...auditColumns.map(column => ({
|
||||||
...column,
|
...column,
|
||||||
display: false,
|
display: false,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
createCrudOption,
|
createCrudOption,
|
||||||
nonNegativeRule,
|
nonNegativeRule,
|
||||||
textRule,
|
textRule,
|
||||||
|
withSearchPlaceholders,
|
||||||
} from './common';
|
} from './common';
|
||||||
|
|
||||||
const listDicFormatter = res => {
|
const listDicFormatter = res => {
|
||||||
@@ -15,6 +16,12 @@ const listDicFormatter = res => {
|
|||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const projectNameDicFormatter = res => {
|
||||||
|
const list = listDicFormatter(res);
|
||||||
|
// 过滤掉 projectName 为空的记录
|
||||||
|
return list.filter(item => item && item.projectName && String(item.projectName).trim());
|
||||||
|
};
|
||||||
|
|
||||||
const userDicFormatter = res =>
|
const userDicFormatter = res =>
|
||||||
listDicFormatter(res).map(user => ({
|
listDicFormatter(res).map(user => ({
|
||||||
...user,
|
...user,
|
||||||
@@ -50,30 +57,25 @@ export const config = {
|
|||||||
detailAlignCenter: true,
|
detailAlignCenter: true,
|
||||||
detailSections: [
|
detailSections: [
|
||||||
{
|
{
|
||||||
title: '申请信息',
|
title: '项目额度信息',
|
||||||
fields: [
|
fields: [
|
||||||
['applicationNo', '申请单号', 1],
|
['projectName', '项目名称', 1],
|
||||||
['projectName', '项目', 1],
|
|
||||||
['projectCode', '项目编号', 1],
|
['projectCode', '项目编号', 1],
|
||||||
['undertakeDeptName', '承办部门', 1],
|
['undertakeDeptName', '承办部门', 1],
|
||||||
['applyDeptName', '申请部门', 1],
|
|
||||||
['applicantName', '申请人', 1],
|
|
||||||
['approvalStatus', '审批状态', 1],
|
|
||||||
['currentNode', '当前节点', 1],
|
|
||||||
['currentProcessor', '当前处理人', 1],
|
|
||||||
['validUntil', '申请有效期至', 1],
|
|
||||||
['remark', '备注', 1],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '额度信息',
|
|
||||||
fields: [
|
|
||||||
['projectFundLimit', '项目资金使用额度(万元)', 1],
|
['projectFundLimit', '项目资金使用额度(万元)', 1],
|
||||||
['usedFundLimit', '已使用项目资金使用额度(万元)', 1],
|
['usedFundLimit', '已使用项目资金使用额度(万元)', 1],
|
||||||
['remainingFundLimit', '剩余项目资金使用额度(万元)', 1],
|
['remainingFundLimit', '剩余项目资金使用额度(万元)', 1],
|
||||||
['applyLimit', '申请临时额度(万元)', 1],
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '临时额度信息',
|
||||||
|
fields: [
|
||||||
|
['applyLimit', '申请临时额度(万元)', 1],
|
||||||
|
['validUntil', '申请有效期至', 1],
|
||||||
|
['remark', '备注', 4],
|
||||||
|
],
|
||||||
|
showAttachment: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
operations: [
|
operations: [
|
||||||
{
|
{
|
||||||
@@ -118,7 +120,10 @@ export const config = {
|
|||||||
|
|
||||||
export const option = {
|
export const option = {
|
||||||
dialogCustomClass: 'temporary-credit-limit-dialog',
|
dialogCustomClass: 'temporary-credit-limit-dialog',
|
||||||
...createCrudOption([
|
addTitle: '新增临时额度申请',
|
||||||
|
editTitle: '编辑临时额度申请',
|
||||||
|
viewTitle: '查看临时额度申请',
|
||||||
|
...createCrudOption(withSearchPlaceholders([
|
||||||
{
|
{
|
||||||
label: '申请单号',
|
label: '申请单号',
|
||||||
prop: 'applicationNo',
|
prop: 'applicationNo',
|
||||||
@@ -129,6 +134,14 @@ export const option = {
|
|||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
editDisabled: true,
|
editDisabled: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
prop: 'projectQuotaInfoTitle',
|
||||||
|
formslot: true,
|
||||||
|
span: 24,
|
||||||
|
hide: true,
|
||||||
|
labelWidth: 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '项目名称',
|
label: '项目名称',
|
||||||
prop: 'projectName',
|
prop: 'projectName',
|
||||||
@@ -137,7 +150,7 @@ export const option = {
|
|||||||
searchType: 'select',
|
searchType: 'select',
|
||||||
formslot: true,
|
formslot: true,
|
||||||
dicUrl: '/blade-transport/project-apply/list?current=1&size=9999',
|
dicUrl: '/blade-transport/project-apply/list?current=1&size=9999',
|
||||||
dicFormatter: listDicFormatter,
|
dicFormatter: projectNameDicFormatter,
|
||||||
props: {
|
props: {
|
||||||
label: 'projectName',
|
label: 'projectName',
|
||||||
value: 'projectName',
|
value: 'projectName',
|
||||||
@@ -158,6 +171,7 @@ export const option = {
|
|||||||
prop: 'projectCode',
|
prop: 'projectCode',
|
||||||
hide: true,
|
hide: true,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
|
placeholder: '请输入',
|
||||||
span: 6,
|
span: 6,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
rules: textRule('项目编号', 50),
|
rules: textRule('项目编号', 50),
|
||||||
@@ -167,6 +181,7 @@ export const option = {
|
|||||||
prop: 'undertakeDeptName',
|
prop: 'undertakeDeptName',
|
||||||
hide: true,
|
hide: true,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
|
placeholder: '请输入',
|
||||||
span: 6,
|
span: 6,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
rules: textRule('承办部门', 50, true),
|
rules: textRule('承办部门', 50, true),
|
||||||
@@ -176,6 +191,7 @@ export const option = {
|
|||||||
prop: 'projectFundLimit',
|
prop: 'projectFundLimit',
|
||||||
hide: true,
|
hide: true,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
|
placeholder: '请输入',
|
||||||
span: 6,
|
span: 6,
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
rules: [nonNegativeRule('项目资金使用额度')],
|
rules: [nonNegativeRule('项目资金使用额度')],
|
||||||
@@ -185,6 +201,7 @@ export const option = {
|
|||||||
prop: 'usedFundLimit',
|
prop: 'usedFundLimit',
|
||||||
hide: true,
|
hide: true,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
|
placeholder: '请输入',
|
||||||
span: 6,
|
span: 6,
|
||||||
minWidth: 220,
|
minWidth: 220,
|
||||||
value: 0,
|
value: 0,
|
||||||
@@ -195,11 +212,20 @@ export const option = {
|
|||||||
prop: 'remainingFundLimit',
|
prop: 'remainingFundLimit',
|
||||||
hide: true,
|
hide: true,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
|
placeholder: '请输入',
|
||||||
span: 6,
|
span: 6,
|
||||||
minWidth: 230,
|
minWidth: 230,
|
||||||
value: 0,
|
value: 0,
|
||||||
rules: [nonNegativeRule('剩余项目资金使用额度')],
|
rules: [nonNegativeRule('剩余项目资金使用额度')],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
prop: 'temporaryCreditInfoTitle',
|
||||||
|
formslot: true,
|
||||||
|
span: 24,
|
||||||
|
hide: true,
|
||||||
|
labelWidth: 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '申请临时额度(万元)',
|
label: '申请临时额度(万元)',
|
||||||
prop: 'applyLimit',
|
prop: 'applyLimit',
|
||||||
@@ -217,9 +243,31 @@ export const option = {
|
|||||||
type: 'date',
|
type: 'date',
|
||||||
format: 'YYYY-MM-DD',
|
format: 'YYYY-MM-DD',
|
||||||
valueFormat: 'YYYY-MM-DD',
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
placeholder: '请选择',
|
||||||
span: 6,
|
span: 6,
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
rules: [{ required: true, message: '请选择申请有效期至', trigger: 'change' }],
|
rules: [
|
||||||
|
{ required: true, message: '请选择申请有效期至', trigger: 'change' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '备注',
|
||||||
|
prop: 'remark',
|
||||||
|
type: 'textarea',
|
||||||
|
span: 24,
|
||||||
|
minRows: 2,
|
||||||
|
hide: true,
|
||||||
|
placeholder: '请输入',
|
||||||
|
maxlength: 200,
|
||||||
|
showWordLimit: true,
|
||||||
|
rules: textRule('备注', 200),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '附件',
|
||||||
|
prop: 'attachmentsJson',
|
||||||
|
formslot: true,
|
||||||
|
span: 24,
|
||||||
|
hide: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '申请部门',
|
label: '申请部门',
|
||||||
@@ -284,26 +332,8 @@ export const option = {
|
|||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: '备注',
|
|
||||||
prop: 'remark',
|
|
||||||
type: 'textarea',
|
|
||||||
span: 24,
|
|
||||||
minRows: 2,
|
|
||||||
hide: true,
|
|
||||||
maxlength: 200,
|
|
||||||
showWordLimit: true,
|
|
||||||
rules: textRule('备注', 200),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '附件',
|
|
||||||
prop: 'attachmentsJson',
|
|
||||||
formslot: true,
|
|
||||||
span: 24,
|
|
||||||
hide: true,
|
|
||||||
},
|
|
||||||
...auditColumns.map(column => ({ ...column, hide: true })),
|
...auditColumns.map(column => ({ ...column, hide: true })),
|
||||||
]),
|
])),
|
||||||
dialogWidth: '96%',
|
dialogWidth: '96%',
|
||||||
menuFixed: 'right',
|
menuFixed: 'right',
|
||||||
menuWidth: 320,
|
menuWidth: 320,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
auditColumns,
|
auditColumns,
|
||||||
createCrudOption,
|
createCrudOption,
|
||||||
dataSourceOptions,
|
|
||||||
phoneRule,
|
phoneRule,
|
||||||
planStatusOptions,
|
planStatusOptions,
|
||||||
selectRule,
|
selectRule,
|
||||||
@@ -9,6 +8,12 @@ import {
|
|||||||
withSearchPlaceholders,
|
withSearchPlaceholders,
|
||||||
} from './common';
|
} from './common';
|
||||||
|
|
||||||
|
const transportPlanDataSourceOptions = [
|
||||||
|
{ label: '批量导入', value: '批量导入' },
|
||||||
|
{ label: '手工创建', value: '手工创建' },
|
||||||
|
{ label: '外部系统', value: '外部系统' },
|
||||||
|
];
|
||||||
|
|
||||||
const listDicFormatter = res => {
|
const listDicFormatter = res => {
|
||||||
const data = res?.data || res;
|
const data = res?.data || res;
|
||||||
if (Array.isArray(data)) return data;
|
if (Array.isArray(data)) return data;
|
||||||
@@ -18,6 +23,12 @@ const listDicFormatter = res => {
|
|||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const projectNameDicFormatter = res => {
|
||||||
|
const list = listDicFormatter(res);
|
||||||
|
// 过滤掉 projectName 为空的记录
|
||||||
|
return list.filter(item => item && item.projectName && String(item.projectName).trim());
|
||||||
|
};
|
||||||
|
|
||||||
const parseGoodsRows = value => {
|
const parseGoodsRows = value => {
|
||||||
if (!value) return [];
|
if (!value) return [];
|
||||||
if (Array.isArray(value)) return value;
|
if (Array.isArray(value)) return value;
|
||||||
@@ -61,7 +72,11 @@ const getSecondCargoTypeName = item =>
|
|||||||
'货物';
|
'货物';
|
||||||
|
|
||||||
const formatGoodsInfo = row => {
|
const formatGoodsInfo = row => {
|
||||||
const rows = parseGoodsRows(row.goodsJson);
|
const rows =
|
||||||
|
[row.goodsJson, row.goodsList, row.goodsRows, row.cargoList, row.cargoRows, row.goods]
|
||||||
|
.map(source => parseGoodsRows(source))
|
||||||
|
.filter(source => source.length)
|
||||||
|
.sort((left, right) => right.length - left.length)[0] || [];
|
||||||
if (!rows.length) return row.goodsInfo || '';
|
if (!rows.length) return row.goodsInfo || '';
|
||||||
|
|
||||||
const groups = rows.reduce((result, item = {}) => {
|
const groups = rows.reduce((result, item = {}) => {
|
||||||
@@ -78,11 +93,22 @@ const formatGoodsInfo = row => {
|
|||||||
item.goodsQuantity,
|
item.goodsQuantity,
|
||||||
item.quantityUnit,
|
item.quantityUnit,
|
||||||
item.goodsQuantityUnit,
|
item.goodsQuantityUnit,
|
||||||
|
item.cargoUnit,
|
||||||
item.unit,
|
item.unit,
|
||||||
].some(Boolean);
|
].some(Boolean);
|
||||||
if (!hasGoodsInfo) return result;
|
if (!hasGoodsInfo) return result;
|
||||||
|
|
||||||
const unit = item.quantityUnit || item.goodsQuantityUnit || item.unit || '';
|
const unit = String(
|
||||||
|
item.quantityUnit ||
|
||||||
|
item.goodsQuantityUnit ||
|
||||||
|
item.cargoUnit ||
|
||||||
|
item.unit ||
|
||||||
|
row.quantityUnit ||
|
||||||
|
row.goodsQuantityUnit ||
|
||||||
|
row.cargoUnit ||
|
||||||
|
row.unit ||
|
||||||
|
''
|
||||||
|
).trim();
|
||||||
if (!result[unit]) {
|
if (!result[unit]) {
|
||||||
result[unit] = {
|
result[unit] = {
|
||||||
typeName: getSecondCargoTypeName(item),
|
typeName: getSecondCargoTypeName(item),
|
||||||
@@ -150,9 +176,12 @@ const formatDispatchProgress = row => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const lines = Array.from(summary.entries()).map(
|
const lines = Array.from(summary.entries()).map(
|
||||||
([unit, item]) => `${formatDispatchQuantity(item.assigned)}/${formatDispatchQuantity(item.total)}${unit}`
|
([unit, item]) =>
|
||||||
|
`${formatDispatchQuantity(item.assigned)}/${formatDispatchQuantity(item.total)}${unit}`
|
||||||
);
|
);
|
||||||
return lines.length ? lines.join('\n') : row.dispatchProgressName || row.dispatchProgress || row.progress || '';
|
return lines.length
|
||||||
|
? lines.join('\n')
|
||||||
|
: row.dispatchProgressName || row.dispatchProgress || row.progress || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const auditColumn = prop => ({ ...auditColumns.find(item => item.prop === prop) });
|
const auditColumn = prop => ({ ...auditColumns.find(item => item.prop === prop) });
|
||||||
@@ -162,7 +191,6 @@ export const config = {
|
|||||||
permission: 'transport_plan',
|
permission: 'transport_plan',
|
||||||
exportUrl: '/blade-transport/transport-plan/export-transport-plan',
|
exportUrl: '/blade-transport/transport-plan/export-transport-plan',
|
||||||
exportName: '运输计划',
|
exportName: '运输计划',
|
||||||
enableAllDept: false,
|
|
||||||
enableProjectSelect: true,
|
enableProjectSelect: true,
|
||||||
projectQueryParams: {
|
projectQueryParams: {
|
||||||
approvalStatuses: 'approved,change_approved',
|
approvalStatuses: 'approved,change_approved',
|
||||||
@@ -176,7 +204,7 @@ export const config = {
|
|||||||
enableTransportPlanCreateActions: true,
|
enableTransportPlanCreateActions: true,
|
||||||
enableTransportPlanImport: true,
|
enableTransportPlanImport: true,
|
||||||
fixedTransportAddressType: true,
|
fixedTransportAddressType: true,
|
||||||
transportPlanImportTitle: '导入发货计划',
|
transportPlanImportTitle: '导入运输计划',
|
||||||
transportPlanTemplateUrl: '/blade-transport/transport-plan/export-template',
|
transportPlanTemplateUrl: '/blade-transport/transport-plan/export-template',
|
||||||
attachmentTitle: '附件',
|
attachmentTitle: '附件',
|
||||||
searchRangeMap: {
|
searchRangeMap: {
|
||||||
@@ -187,7 +215,7 @@ export const config = {
|
|||||||
statusProp: 'businessStatus',
|
statusProp: 'businessStatus',
|
||||||
statusTextProp: 'businessStatusName',
|
statusTextProp: 'businessStatusName',
|
||||||
deleteStatus: ['draft', 'waiting_dispatch'],
|
deleteStatus: ['draft', 'waiting_dispatch'],
|
||||||
editStatus: ['draft', 'waiting_dispatch'],
|
editStatus: ['draft', 'waiting_dispatch', 'dispatching'],
|
||||||
detailButton: true,
|
detailButton: true,
|
||||||
detailSections: [
|
detailSections: [
|
||||||
{
|
{
|
||||||
@@ -293,7 +321,6 @@ export const option = {
|
|||||||
prop: 'goodsInfo',
|
prop: 'goodsInfo',
|
||||||
formatter: row => formatGoodsInfo(row),
|
formatter: row => formatGoodsInfo(row),
|
||||||
minWidth: 320,
|
minWidth: 320,
|
||||||
className: 'business-crud-page__goods-info-cell',
|
|
||||||
overHidden: false,
|
overHidden: false,
|
||||||
showOverflowTooltip: false,
|
showOverflowTooltip: false,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
@@ -305,7 +332,6 @@ export const option = {
|
|||||||
prop: 'dispatchProgress',
|
prop: 'dispatchProgress',
|
||||||
formatter: row => formatDispatchProgress(row),
|
formatter: row => formatDispatchProgress(row),
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
className: 'business-crud-page__dispatch-progress-cell',
|
|
||||||
overHidden: false,
|
overHidden: false,
|
||||||
showOverflowTooltip: false,
|
showOverflowTooltip: false,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
@@ -344,7 +370,7 @@ export const option = {
|
|||||||
span: 6,
|
span: 6,
|
||||||
order: 400,
|
order: 400,
|
||||||
dicUrl: '/blade-transport/project-apply/list?current=1&size=9999',
|
dicUrl: '/blade-transport/project-apply/list?current=1&size=9999',
|
||||||
dicFormatter: listDicFormatter,
|
dicFormatter: projectNameDicFormatter,
|
||||||
props: {
|
props: {
|
||||||
label: 'projectName',
|
label: 'projectName',
|
||||||
value: 'projectName',
|
value: 'projectName',
|
||||||
@@ -371,6 +397,7 @@ export const option = {
|
|||||||
span: 6,
|
span: 6,
|
||||||
order: 350,
|
order: 350,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
|
rules: [{ required: true, message: '请选择计划开始日期', trigger: 'change' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '计划结束日期',
|
label: '计划结束日期',
|
||||||
@@ -381,6 +408,7 @@ export const option = {
|
|||||||
span: 6,
|
span: 6,
|
||||||
order: 340,
|
order: 340,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
|
rules: [{ required: true, message: '请选择计划结束日期', trigger: 'change' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '数据来源',
|
label: '数据来源',
|
||||||
@@ -388,7 +416,7 @@ export const option = {
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
search: true,
|
search: true,
|
||||||
searchOrder: 1,
|
searchOrder: 1,
|
||||||
dicData: dataSourceOptions,
|
dicData: transportPlanDataSourceOptions,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
@@ -580,7 +608,7 @@ export const option = {
|
|||||||
order: 270,
|
order: 270,
|
||||||
hide: true,
|
hide: true,
|
||||||
labelWidth: '0px',
|
labelWidth: '0px',
|
||||||
className: 'business-crud-page__full-form-item',
|
className: 'transport-plan-page__full-form-item',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '',
|
label: '',
|
||||||
@@ -601,7 +629,7 @@ export const option = {
|
|||||||
order: 260,
|
order: 260,
|
||||||
hide: true,
|
hide: true,
|
||||||
labelWidth: '0px',
|
labelWidth: '0px',
|
||||||
className: 'business-crud-page__full-form-item',
|
className: 'transport-plan-page__full-form-item',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ const transportTypeDict = {
|
|||||||
|
|
||||||
const isEmpty = value => value === undefined || value === null || value === '';
|
const isEmpty = value => value === undefined || value === null || value === '';
|
||||||
const normalizeNumericDisplayValue = value => (Number(value) === -1 ? '' : value);
|
const normalizeNumericDisplayValue = value => (Number(value) === -1 ? '' : value);
|
||||||
|
const formatMoneyDisplay = value => {
|
||||||
|
if (isEmpty(value) || Number(value) === -1) return '';
|
||||||
|
const number = Number(value);
|
||||||
|
if (!Number.isFinite(number)) return '';
|
||||||
|
return number.toFixed(2);
|
||||||
|
};
|
||||||
|
|
||||||
const parseJsonArray = value => {
|
const parseJsonArray = value => {
|
||||||
if (Array.isArray(value)) return value;
|
if (Array.isArray(value)) return value;
|
||||||
@@ -92,14 +98,32 @@ const getProcessConfigNodes = row => {
|
|||||||
|
|
||||||
const isAcceptProcessNode = node => node.key === 'accept' || node.name === '接单';
|
const isAcceptProcessNode = node => node.key === 'accept' || node.name === '接单';
|
||||||
|
|
||||||
|
const isTruthyFlag = value => {
|
||||||
|
if (value === true || value === 1) return true;
|
||||||
|
if (value === false || value === 0 || value === null || value === undefined || value === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ['true', '1', 'yes'].includes(String(value).trim().toLowerCase());
|
||||||
|
};
|
||||||
|
|
||||||
const requiresDriverAcceptConfirmation = row =>
|
const requiresDriverAcceptConfirmation = row =>
|
||||||
getProcessConfigNodes(row).some(
|
getProcessConfigNodes(row).some(node => {
|
||||||
node =>
|
if (!isAcceptProcessNode(node) || node.enabled === false) return false;
|
||||||
isAcceptProcessNode(node) &&
|
if (node.confirmMode === 'no_confirm_accept') return false;
|
||||||
node.enabled !== false &&
|
// 与后端一致:是否确认=是 即需接单(不强制依赖 confirmDriver)
|
||||||
node.confirmMode === 'yes' &&
|
return node.confirmMode === 'yes' || !node.confirmMode;
|
||||||
node.confirmDriver === true
|
});
|
||||||
);
|
|
||||||
|
const getDriverRejectReason = row => {
|
||||||
|
if (!row) return '';
|
||||||
|
const reason =
|
||||||
|
row.driverRejectReason ||
|
||||||
|
row.driverRefuseReason ||
|
||||||
|
row.acceptRejectReason ||
|
||||||
|
row.rejectReason ||
|
||||||
|
'';
|
||||||
|
return String(reason || '').trim();
|
||||||
|
};
|
||||||
|
|
||||||
const isDriverRejectedStatus = value => {
|
const isDriverRejectedStatus = value => {
|
||||||
if (value === null || value === undefined || typeof value === 'boolean') return false;
|
if (value === null || value === undefined || typeof value === 'boolean') return false;
|
||||||
@@ -122,6 +146,7 @@ const isDriverRejectedStatus = value => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hasDriverRejectRecord = row => {
|
const hasDriverRejectRecord = row => {
|
||||||
|
if (String(row.driverAcceptStatus || '').toLowerCase() === 'rejected') return true;
|
||||||
const rejectRecordValues = [
|
const rejectRecordValues = [
|
||||||
row.driverRejectTime,
|
row.driverRejectTime,
|
||||||
row.driverRejectedTime,
|
row.driverRejectedTime,
|
||||||
@@ -161,9 +186,16 @@ const hasDriverRejectRecord = row => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isDriverRejectedWaybill = row =>
|
const isDriverRejectedWaybill = row =>
|
||||||
requiresDriverAcceptConfirmation(row) && hasDriverRejectRecord(row);
|
(row.requireAccept === true ||
|
||||||
|
row.requireAccept === 1 ||
|
||||||
|
requiresDriverAcceptConfirmation(row)) &&
|
||||||
|
hasDriverRejectRecord(row);
|
||||||
|
|
||||||
const hasDriverAcceptRecord = row => {
|
const hasDriverAcceptRecord = row => {
|
||||||
|
if (String(row.driverAcceptStatus || '').toLowerCase() === 'accepted') return true;
|
||||||
|
if (['rejected', 'pending'].includes(String(row.driverAcceptStatus || '').toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const recordValues = [
|
const recordValues = [
|
||||||
row.driverAcceptTime,
|
row.driverAcceptTime,
|
||||||
row.driverAcceptedTime,
|
row.driverAcceptedTime,
|
||||||
@@ -172,6 +204,7 @@ const hasDriverAcceptRecord = row => {
|
|||||||
row.driverAcceptedBy,
|
row.driverAcceptedBy,
|
||||||
row.acceptUserId,
|
row.acceptUserId,
|
||||||
row.acceptUserName,
|
row.acceptUserName,
|
||||||
|
row.driverAcceptDriverId,
|
||||||
];
|
];
|
||||||
if (recordValues.some(value => value !== null && value !== undefined && value !== '')) {
|
if (recordValues.some(value => value !== null && value !== undefined && value !== '')) {
|
||||||
return true;
|
return true;
|
||||||
@@ -207,6 +240,26 @@ const getFirstValue = (row, props) => {
|
|||||||
|
|
||||||
const getGoodsRows = row => parseJsonArray(row.goodsList || row.goodsRows || row.goodsJson);
|
const getGoodsRows = row => parseJsonArray(row.goodsList || row.goodsRows || row.goodsJson);
|
||||||
|
|
||||||
|
const getDetailedGoodsRows = row => {
|
||||||
|
const goodsRows = [
|
||||||
|
row.goodsJson,
|
||||||
|
row.goodsList,
|
||||||
|
row.goodsRows,
|
||||||
|
row.cargoList,
|
||||||
|
row.cargoRows,
|
||||||
|
row.goods,
|
||||||
|
]
|
||||||
|
.map(source => {
|
||||||
|
if (source === undefined || source === null || source === '') return [];
|
||||||
|
if (Array.isArray(source)) return source;
|
||||||
|
if (typeof source === 'object') return [source];
|
||||||
|
return parseJsonArray(source);
|
||||||
|
})
|
||||||
|
.filter(source => source.length)
|
||||||
|
.sort((left, right) => right.length - left.length);
|
||||||
|
return goodsRows[0] || [];
|
||||||
|
};
|
||||||
|
|
||||||
const getBillingRows = row => {
|
const getBillingRows = row => {
|
||||||
const freightRows = parseJsonArray(row.freightList || row.freightRows || row.freightJson);
|
const freightRows = parseJsonArray(row.freightList || row.freightRows || row.freightJson);
|
||||||
if (freightRows.length) return freightRows;
|
if (freightRows.length) return freightRows;
|
||||||
@@ -218,9 +271,25 @@ const getBillingRows = row => {
|
|||||||
const joinText = list => list.filter(item => !isEmpty(item)).join('/');
|
const joinText = list => list.filter(item => !isEmpty(item)).join('/');
|
||||||
|
|
||||||
const formatGoodsInfo = row => {
|
const formatGoodsInfo = row => {
|
||||||
|
const goodsRows = [
|
||||||
|
row.goodsJson,
|
||||||
|
row.goodsList,
|
||||||
|
row.goodsRows,
|
||||||
|
row.cargoList,
|
||||||
|
row.cargoRows,
|
||||||
|
row.goods,
|
||||||
|
]
|
||||||
|
.map(source => {
|
||||||
|
if (source === undefined || source === null || source === '') return [];
|
||||||
|
if (Array.isArray(source)) return source;
|
||||||
|
if (typeof source === 'object') return [source];
|
||||||
|
return parseJsonArray(source);
|
||||||
|
})
|
||||||
|
.filter(source => source.length)
|
||||||
|
.sort((left, right) => right.length - left.length)[0] || [];
|
||||||
const text = getFirstValue(row, ['goodsInfo', 'cargoInfo']);
|
const text = getFirstValue(row, ['goodsInfo', 'cargoInfo']);
|
||||||
if (text) return text;
|
if (!goodsRows.length) return text || '';
|
||||||
return getGoodsRows(row)
|
return goodsRows
|
||||||
.map(item => {
|
.map(item => {
|
||||||
const name = getFirstValue(item, ['cargoName', 'goodsName', 'name']);
|
const name = getFirstValue(item, ['cargoName', 'goodsName', 'name']);
|
||||||
const type = getFirstValue(item, ['cargoType', 'goodsType', 'typeName']);
|
const type = getFirstValue(item, ['cargoType', 'goodsType', 'typeName']);
|
||||||
@@ -228,12 +297,13 @@ const formatGoodsInfo = row => {
|
|||||||
normalizeNumericDisplayValue(
|
normalizeNumericDisplayValue(
|
||||||
getFirstValue(item, ['quantity', 'cargoQuantity', 'goodsQuantity'])
|
getFirstValue(item, ['quantity', 'cargoQuantity', 'goodsQuantity'])
|
||||||
),
|
),
|
||||||
getFirstValue(item, ['quantityUnit', 'cargoUnit', 'unit']),
|
getFirstValue(item, ['quantityUnit', 'goodsQuantityUnit', 'cargoUnit', 'unit']) ||
|
||||||
|
getFirstValue(row, ['quantityUnit', 'goodsQuantityUnit', 'cargoUnit', 'unit']),
|
||||||
]);
|
]);
|
||||||
return joinText([name, type, quantity]);
|
return joinText([name, type, quantity]);
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('; ');
|
.join('; ') || text || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatGoodsField = (row, props) => {
|
const formatGoodsField = (row, props) => {
|
||||||
@@ -246,6 +316,28 @@ const formatGoodsField = (row, props) => {
|
|||||||
const formatUnitPrice = row => {
|
const formatUnitPrice = row => {
|
||||||
const value = normalizeNumericDisplayValue(getFirstValue(row, ['unitPrice', 'price']));
|
const value = normalizeNumericDisplayValue(getFirstValue(row, ['unitPrice', 'price']));
|
||||||
const unit = getFirstValue(row, ['priceUnit', 'billingUnit', 'unit']);
|
const unit = getFirstValue(row, ['priceUnit', 'billingUnit', 'unit']);
|
||||||
|
|
||||||
|
const goodsRows = getDetailedGoodsRows(row);
|
||||||
|
if (goodsRows.length > 1) {
|
||||||
|
const billingRows = getBillingRows(row);
|
||||||
|
const goodsPrices = goodsRows
|
||||||
|
.map((item, index) => {
|
||||||
|
const itemPrice = normalizeNumericDisplayValue(
|
||||||
|
getFirstValue(item, ['unitPrice', 'price'])
|
||||||
|
);
|
||||||
|
if (!isEmpty(itemPrice)) return itemPrice;
|
||||||
|
return normalizeNumericDisplayValue(
|
||||||
|
getFirstValue(billingRows[index] || {}, ['unitPrice', 'price'])
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.filter(itemPrice => !isEmpty(itemPrice));
|
||||||
|
const priceKeys = goodsPrices.map(itemPrice => {
|
||||||
|
const numericPrice = Number(itemPrice);
|
||||||
|
return Number.isFinite(numericPrice) ? String(numericPrice) : String(itemPrice).trim();
|
||||||
|
});
|
||||||
|
if (new Set(priceKeys).size > 1) return '-';
|
||||||
|
}
|
||||||
|
|
||||||
if (!isEmpty(value)) return unit ? `${value}(${unit})` : value;
|
if (!isEmpty(value)) return unit ? `${value}(${unit})` : value;
|
||||||
const billing = getBillingRows(row).find(
|
const billing = getBillingRows(row).find(
|
||||||
item => !isEmpty(normalizeNumericDisplayValue(item.unitPrice))
|
item => !isEmpty(normalizeNumericDisplayValue(item.unitPrice))
|
||||||
@@ -256,10 +348,10 @@ const formatUnitPrice = row => {
|
|||||||
return billingUnit ? `${billingPrice}(${billingUnit})` : billingPrice;
|
return billingUnit ? `${billingPrice}(${billingUnit})` : billingPrice;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatAmount = (row, props) => getFirstValue(row, props);
|
const formatAmount = (row, props) => formatMoneyDisplay(getFirstValue(row, props));
|
||||||
|
|
||||||
const sumAmount = list =>
|
const sumAmount = (rows = []) =>
|
||||||
list.reduce((sum, item) => {
|
rows.reduce((sum, item) => {
|
||||||
const value = getFirstValue(item, ['freightAmount', 'amount', 'totalAmount']);
|
const value = getFirstValue(item, ['freightAmount', 'amount', 'totalAmount']);
|
||||||
return isEmpty(value) ? sum : sum + Number(value || 0);
|
return isEmpty(value) ? sum : sum + Number(value || 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -278,7 +370,7 @@ const calculateFreightTotal = rows => {
|
|||||||
return sum + Number(item.unitPrice || 0) * Number(item.quantity || 0);
|
return sum + Number(item.unitPrice || 0) * Number(item.quantity || 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
if (!hasAmountField) return '';
|
if (!hasAmountField) return '';
|
||||||
return Number.isFinite(total) ? Number(total.toFixed(2)).toString() : '';
|
return Number.isFinite(total) ? total.toFixed(2) : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatFreight = row => {
|
const formatFreight = row => {
|
||||||
@@ -286,9 +378,9 @@ const formatFreight = row => {
|
|||||||
if (!isEmpty(value)) return value;
|
if (!isEmpty(value)) return value;
|
||||||
const freight = parseJsonObject(row.freightJson);
|
const freight = parseJsonObject(row.freightJson);
|
||||||
const freightValue = getFirstValue(freight, ['freightAmount', 'transportFee']);
|
const freightValue = getFirstValue(freight, ['freightAmount', 'transportFee']);
|
||||||
if (!isEmpty(freightValue)) return freightValue;
|
if (!isEmpty(freightValue)) return formatMoneyDisplay(freightValue);
|
||||||
const total = sumAmount(Array.isArray(freight.freightItems) ? freight.freightItems : []);
|
const total = sumAmount(Array.isArray(freight.freightItems) ? freight.freightItems : []);
|
||||||
return total ? total : '';
|
return total ? formatMoneyDisplay(total) : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatOtherFeeTotal = row => {
|
const formatOtherFeeTotal = row => {
|
||||||
@@ -305,10 +397,10 @@ const formatFreightTotal = row => {
|
|||||||
const otherFeeTotal = !isEmpty(otherFee) ? otherFee : formatOtherFeeTotal(row);
|
const otherFeeTotal = !isEmpty(otherFee) ? otherFee : formatOtherFeeTotal(row);
|
||||||
if (!isEmpty(calculated)) {
|
if (!isEmpty(calculated)) {
|
||||||
const total = Number(calculated || 0) + Number(otherFeeTotal || 0);
|
const total = Number(calculated || 0) + Number(otherFeeTotal || 0);
|
||||||
return Number.isFinite(total) ? Number(total.toFixed(2)).toString() : calculated;
|
return Number.isFinite(total) ? total.toFixed(2) : calculated;
|
||||||
}
|
}
|
||||||
const value = getFirstValue(freight, ['totalFreightAmount', 'totalFreight', 'totalAmount']);
|
const value = getFirstValue(freight, ['totalFreightAmount', 'totalFreight', 'totalAmount']);
|
||||||
if (!isEmpty(value)) return value;
|
if (!isEmpty(value)) return formatMoneyDisplay(value);
|
||||||
return formatAmount(row, ['freightTotal', 'totalFreight', 'totalAmount']);
|
return formatAmount(row, ['freightTotal', 'totalFreight', 'totalAmount']);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -318,6 +410,8 @@ export const config = {
|
|||||||
title: '运单管理',
|
title: '运单管理',
|
||||||
permission: 'waybill_manage',
|
permission: 'waybill_manage',
|
||||||
importUrl: '/blade-transport/waybill-manage/import-waybill-manage',
|
importUrl: '/blade-transport/waybill-manage/import-waybill-manage',
|
||||||
|
exportUrl: '/blade-transport/waybill-manage/export-waybill-manage',
|
||||||
|
exportName: '运单管理',
|
||||||
defaultForm: {
|
defaultForm: {
|
||||||
carrierType: '承运商',
|
carrierType: '承运商',
|
||||||
transportType: 'road',
|
transportType: 'road',
|
||||||
@@ -364,29 +458,37 @@ export const config = {
|
|||||||
statusProp: 'businessStatus',
|
statusProp: 'businessStatus',
|
||||||
statusTextProp: 'businessStatusName',
|
statusTextProp: 'businessStatusName',
|
||||||
formatStatus(row, prop, defaultText) {
|
formatStatus(row, prop, defaultText) {
|
||||||
if (
|
if (prop !== 'businessStatus') return defaultText;
|
||||||
prop === 'businessStatus' &&
|
const status = String(row.businessStatus || '');
|
||||||
requiresDriverAcceptConfirmation(row) &&
|
const terminalStatuses = ['draft', 'completed', 'cancelled', 'waiting_dispatch', 'dispatching'];
|
||||||
isInProgressStatus(row, defaultText) &&
|
const needAccept =
|
||||||
!hasDriverAcceptRecord(row)
|
row.requireAccept === true ||
|
||||||
) {
|
row.requireAccept === 1 ||
|
||||||
|
requiresDriverAcceptConfirmation(row);
|
||||||
|
// 需司机接单且尚未接单 → 待执行(含库中仍为 running 的历史数据展示校正)
|
||||||
|
if (needAccept && !hasDriverAcceptRecord(row) && !terminalStatuses.includes(status)) {
|
||||||
return '待执行';
|
return '待执行';
|
||||||
}
|
}
|
||||||
if (
|
if (status === 'pending') return '待执行';
|
||||||
prop === 'businessStatus' &&
|
if (status === 'running' || status === 'processing') return '进行中';
|
||||||
row.businessStatus === 'pending' &&
|
|
||||||
!requiresDriverAcceptConfirmation(row)
|
|
||||||
) {
|
|
||||||
return '进行中';
|
|
||||||
}
|
|
||||||
return defaultText;
|
return defaultText;
|
||||||
},
|
},
|
||||||
canReassign(row) {
|
canReassign(row) {
|
||||||
return isDriverRejectedWaybill(row);
|
return isDriverRejectedWaybill(row);
|
||||||
},
|
},
|
||||||
|
getDriverRejectReason,
|
||||||
|
isDriverRejectedWaybill,
|
||||||
canEdit(row) {
|
canEdit(row) {
|
||||||
|
// 进行中的运单不允许编辑
|
||||||
|
if (isInProgressStatus(row, row.businessStatusName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return !isDriverRejectedWaybill(row);
|
return !isDriverRejectedWaybill(row);
|
||||||
},
|
},
|
||||||
|
canCancel(row) {
|
||||||
|
// 进行中的运单不允许取消
|
||||||
|
return !isInProgressStatus(row, row.businessStatusName);
|
||||||
|
},
|
||||||
deleteStatus: ['draft'],
|
deleteStatus: ['draft'],
|
||||||
editStatus: ['draft', 'pending'],
|
editStatus: ['draft', 'pending'],
|
||||||
};
|
};
|
||||||
@@ -578,7 +680,7 @@ export const option = {
|
|||||||
viewDisplay: false,
|
viewDisplay: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '单价(计价单位)',
|
label: '单价',
|
||||||
prop: 'unitPrice',
|
prop: 'unitPrice',
|
||||||
formatter: row => formatUnitPrice(row),
|
formatter: row => formatUnitPrice(row),
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
@@ -808,7 +910,7 @@ export const option = {
|
|||||||
order: 760,
|
order: 760,
|
||||||
hide: true,
|
hide: true,
|
||||||
labelWidth: '0px',
|
labelWidth: '0px',
|
||||||
className: 'business-crud-page__full-form-item',
|
className: 'waybill-manage-page__full-form-item',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '实际发货时间',
|
label: '实际发货时间',
|
||||||
@@ -920,7 +1022,7 @@ export const option = {
|
|||||||
order: 740,
|
order: 740,
|
||||||
hide: true,
|
hide: true,
|
||||||
labelWidth: '0px',
|
labelWidth: '0px',
|
||||||
className: 'business-crud-page__full-form-item',
|
className: 'waybill-manage-page__full-form-item',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '所属组织',
|
label: '所属组织',
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ export const invoiceApplicationDetailColumns = [
|
|||||||
{ prop: 'vehicleNo', label: '车号', minWidth: 120 },
|
{ prop: 'vehicleNo', label: '车号', minWidth: 120 },
|
||||||
{ prop: 'departureAddress', label: '发货地址', minWidth: 180 },
|
{ prop: 'departureAddress', label: '发货地址', minWidth: 180 },
|
||||||
{ prop: 'arrivalAddress', label: '到货地址', minWidth: 180 },
|
{ prop: 'arrivalAddress', label: '到货地址', minWidth: 180 },
|
||||||
{ prop: 'actualDepartureTime', label: '实际发货时间', minWidth: 170 },
|
{ prop: 'actualDepartureTime', label: '实际发货时间', minWidth: 170, dateTime: true },
|
||||||
{ prop: 'actualCompletionTime', label: '实际完成时间', minWidth: 170 },
|
{ prop: 'actualCompletionTime', label: '实际完成时间', minWidth: 170, dateTime: true },
|
||||||
{ prop: 'transportType', label: '运输类型', minWidth: 120 },
|
{ prop: 'transportType', label: '运输类型', minWidth: 120 },
|
||||||
{ prop: 'cargoName', label: '货物名称', minWidth: 140 },
|
{ prop: 'cargoName', label: '货物名称', minWidth: 140 },
|
||||||
{ prop: 'cargoType', label: '货物类型', minWidth: 130 },
|
{ prop: 'cargoType', label: '货物类型', minWidth: 130 },
|
||||||
|
|||||||
@@ -67,9 +67,7 @@ export const feeDetailBaseColumns = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const feeDetailTailColumns = [
|
export const feeDetailTailColumns = [
|
||||||
{ label: '原总金额', prop: 'originalAmountText', minWidth: 120 },
|
{ label: '结算金额', prop: 'originalAmountText', minWidth: 120 },
|
||||||
{ label: '调整金额', prop: 'adjustAmountText', minWidth: 120 },
|
|
||||||
{ label: '调整后总金额', prop: 'afterAmountText', minWidth: 140 },
|
|
||||||
{ label: '备注', prop: 'remark', minWidth: 160 },
|
{ label: '备注', prop: 'remark', minWidth: 160 },
|
||||||
{ label: '最后录入人', prop: 'updateUserName', minWidth: 120 },
|
{ label: '最后录入人', prop: 'updateUserName', minWidth: 120 },
|
||||||
{ label: '最后录入时间', prop: 'updateTime', minWidth: 170 },
|
{ label: '最后录入时间', prop: 'updateTime', minWidth: 170 },
|
||||||
@@ -138,4 +136,11 @@ export const generatePreviewColumns = [
|
|||||||
{ label: '货物名称', prop: 'cargoName', minWidth: 140 },
|
{ label: '货物名称', prop: 'cargoName', minWidth: 140 },
|
||||||
{ label: '货物类型', prop: 'cargoType', minWidth: 140 },
|
{ label: '货物类型', prop: 'cargoType', minWidth: 140 },
|
||||||
{ label: '规格', prop: 'specification', minWidth: 140 },
|
{ label: '规格', prop: 'specification', minWidth: 140 },
|
||||||
|
{ label: '型号', prop: 'model', minWidth: 120 },
|
||||||
|
{ label: '计费要素', prop: 'billingFactor', minWidth: 130 },
|
||||||
|
{ label: '计费类型', prop: 'billingType', minWidth: 130 },
|
||||||
|
{ label: '运输量', prop: 'transportQuantityText', minWidth: 120 },
|
||||||
|
{ label: '运费计算单位', prop: 'priceUnit', minWidth: 130 },
|
||||||
|
{ label: '运输单价', prop: 'unitPrice', minWidth: 120 },
|
||||||
|
{ label: '里程(KM)', prop: 'mileage', minWidth: 120 },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ export const transportReconciliationFormFields = [
|
|||||||
{ prop: 'reconciliationNo', label: '对账单号', readonly: true },
|
{ prop: 'reconciliationNo', label: '对账单号', readonly: true },
|
||||||
{ prop: 'reconciliationMode', label: '对账模式', type: 'mode' },
|
{ prop: 'reconciliationMode', label: '对账模式', type: 'mode' },
|
||||||
{ prop: 'formalSettlementNo', label: '正式结算单', type: 'formal' },
|
{ prop: 'formalSettlementNo', label: '正式结算单', type: 'formal' },
|
||||||
{ prop: 'payerName', label: '付款方', readonly: true },
|
{ prop: 'customerName', label: '客户/承运商', readonly: true },
|
||||||
{ prop: 'payeeName', label: '收款方', readonly: true },
|
|
||||||
{ prop: 'projectName', label: '项目', readonly: true },
|
{ prop: 'projectName', label: '项目', readonly: true },
|
||||||
{ prop: 'deptName', label: '所属组织', readonly: true },
|
|
||||||
{ prop: 'contractName', label: '合同名称', readonly: true },
|
{ prop: 'contractName', label: '合同名称', readonly: true },
|
||||||
{ prop: 'paidAmount', label: '已付合计', money: true, readonly: true },
|
{ prop: 'paidAmount', label: '已付合计', money: true, readonly: true },
|
||||||
{ prop: 'reconcilerName', label: '对账人', readonly: true },
|
{ prop: 'reconcilerName', label: '对账人', readonly: true },
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
export const reconciliationMatchStatusOptions = [
|
export const reconciliationMatchStatusOptions = [
|
||||||
{ label: '全部', value: '' },
|
{ label: '全部', value: 'all' },
|
||||||
{ label: '未匹配', value: 'unmatched' },
|
{ label: '未匹配', value: 'unmatched' },
|
||||||
{ label: '部分匹配', value: 'partial' },
|
{ label: '部分匹配', value: 'partial' },
|
||||||
{ label: '已匹配', value: 'matched' },
|
{ label: '已匹配', value: 'matched' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const reconciliationStatusOptions = [
|
export const reconciliationStatusOptions = [
|
||||||
{ label: '全部', value: '' },
|
{ label: '全部', value: 'all' },
|
||||||
{ label: '未完成', value: 'unfinished' },
|
{ label: '未完成', value: 'unfinished' },
|
||||||
{ label: '已完成', value: 'completed' },
|
{ label: '已完成', value: 'completed' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const transportReconciliationTableColumns = [
|
|||||||
export const internalColumns = [
|
export const internalColumns = [
|
||||||
{ prop: 'documentNo', label: '单据号', minWidth: 150 },
|
{ prop: 'documentNo', label: '单据号', minWidth: 150 },
|
||||||
{ prop: 'waybillNo', label: '运单号', minWidth: 120 },
|
{ prop: 'waybillNo', label: '运单号', minWidth: 120 },
|
||||||
{ prop: 'matchedExternalLineNo', label: '匹配外部账单行号', minWidth: 140 },
|
{ prop: 'matchedExternalLineNo', label: '匹配外部账单行号', minWidth: 180 },
|
||||||
{ prop: 'vehicleNo', label: '车号', minWidth: 100 },
|
{ prop: 'vehicleNo', label: '车号', minWidth: 100 },
|
||||||
{ prop: 'departureAddress', label: '发货地址', minWidth: 180 },
|
{ prop: 'departureAddress', label: '发货地址', minWidth: 180 },
|
||||||
{ prop: 'arrivalAddress', label: '到货地址', minWidth: 180 },
|
{ prop: 'arrivalAddress', label: '到货地址', minWidth: 180 },
|
||||||
@@ -27,13 +27,10 @@ export const internalColumns = [
|
|||||||
{ prop: 'transportType', label: '运输类型', minWidth: 120 },
|
{ prop: 'transportType', label: '运输类型', minWidth: 120 },
|
||||||
{ prop: 'cargoName', label: '货物名称', minWidth: 120 },
|
{ prop: 'cargoName', label: '货物名称', minWidth: 120 },
|
||||||
{ prop: 'cargoType', label: '货物类型', minWidth: 120 },
|
{ prop: 'cargoType', label: '货物类型', minWidth: 120 },
|
||||||
{ prop: 'specification', label: '规格', minWidth: 100 },
|
|
||||||
{ prop: 'model', label: '型号', minWidth: 100 },
|
|
||||||
{ prop: 'transportQuantity', label: '运输总量', minWidth: 100, number: true },
|
{ prop: 'transportQuantity', label: '运输总量', minWidth: 100, number: true },
|
||||||
{ prop: 'mileage', label: '里程(KM)', minWidth: 100, number: true },
|
{ prop: 'mileage', label: '里程(KM)', minWidth: 120, number: true },
|
||||||
{ prop: 'batchNo', label: '批次号', minWidth: 100 },
|
{ prop: 'batchNo', label: '批次号', minWidth: 100 },
|
||||||
{ prop: 'unitPrice', label: '运输单价', minWidth: 110, money: true },
|
{ prop: 'unitPrice', label: '运输单价', minWidth: 110, money: true },
|
||||||
{ prop: 'freightAmount', label: '运输费', minWidth: 110, money: true },
|
|
||||||
{ prop: 'settlementAmount', label: '结算金额', minWidth: 120, money: true },
|
{ prop: 'settlementAmount', label: '结算金额', minWidth: 120, money: true },
|
||||||
{ prop: 'matchResult', label: '匹配结果', minWidth: 100 },
|
{ prop: 'matchResult', label: '匹配结果', minWidth: 100 },
|
||||||
{ prop: 'updateResult', label: '账单更新结果', minWidth: 130 },
|
{ prop: 'updateResult', label: '账单更新结果', minWidth: 130 },
|
||||||
@@ -50,7 +47,7 @@ export const externalVehicleColumns = [
|
|||||||
{ prop: 'cargoName', label: '货物名称', minWidth: 120 },
|
{ prop: 'cargoName', label: '货物名称', minWidth: 120 },
|
||||||
{ prop: 'cargoType', label: '货物类型', minWidth: 120 },
|
{ prop: 'cargoType', label: '货物类型', minWidth: 120 },
|
||||||
{ prop: 'transportQuantity', label: '运输总量', minWidth: 100, number: true },
|
{ prop: 'transportQuantity', label: '运输总量', minWidth: 100, number: true },
|
||||||
{ prop: 'mileage', label: '里程(KM)', minWidth: 100, number: true },
|
{ prop: 'mileage', label: '里程(KM)', minWidth: 120, number: true },
|
||||||
{ prop: 'batchNo', label: '批次号', minWidth: 100 },
|
{ prop: 'batchNo', label: '批次号', minWidth: 100 },
|
||||||
{ prop: 'unitPrice', label: '运输单价', minWidth: 110, money: true },
|
{ prop: 'unitPrice', label: '运输单价', minWidth: 110, money: true },
|
||||||
{ prop: 'freightAmount', label: '运费', minWidth: 110, money: true },
|
{ prop: 'freightAmount', label: '运费', minWidth: 110, money: true },
|
||||||
@@ -69,9 +66,9 @@ export const externalCargoColumns = [
|
|||||||
{ prop: 'cargoType', label: '货物类型', minWidth: 120 },
|
{ prop: 'cargoType', label: '货物类型', minWidth: 120 },
|
||||||
{ prop: 'specification', label: '规格', minWidth: 100 },
|
{ prop: 'specification', label: '规格', minWidth: 100 },
|
||||||
{ prop: 'model', label: '型号', minWidth: 100 },
|
{ prop: 'model', label: '型号', minWidth: 100 },
|
||||||
{ prop: 'transportQuantity', label: '运输量', minWidth: 100, number: true },
|
{ prop: 'transportQuantity', label: '运输总量', minWidth: 100, number: true },
|
||||||
{ prop: 'unitPrice', label: '运输单价', minWidth: 110, money: true },
|
{ prop: 'unitPrice', label: '运输单价', minWidth: 110, money: true },
|
||||||
{ prop: 'mileage', label: '里程(KM)', minWidth: 100, number: true },
|
{ prop: 'mileage', label: '里程(KM)', minWidth: 120, number: true },
|
||||||
{ prop: 'freightAmount', label: '运输费', minWidth: 110, money: true },
|
{ prop: 'freightAmount', label: '运输费', minWidth: 110, money: true },
|
||||||
{ prop: 'settlementAmount', label: '结算金额', minWidth: 120, money: true },
|
{ prop: 'settlementAmount', label: '结算费用合计', minWidth: 130, money: true },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ export const option = {
|
|||||||
prop: 'posts',
|
prop: 'posts',
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '驾驶车辆',
|
||||||
|
prop: 'drivingVehicle',
|
||||||
|
minWidth: 130,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '所属组织',
|
label: '所属组织',
|
||||||
prop: 'organizationName',
|
prop: 'organizationName',
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const option = {
|
|||||||
label: '车牌号',
|
label: '车牌号',
|
||||||
prop: 'plateNo',
|
prop: 'plateNo',
|
||||||
search: true,
|
search: true,
|
||||||
searchOrder: 6,
|
searchOrder: 7,
|
||||||
slot: true,
|
slot: true,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
},
|
},
|
||||||
@@ -29,7 +29,7 @@ export const option = {
|
|||||||
label: '业务关系',
|
label: '业务关系',
|
||||||
prop: 'businessRelation',
|
prop: 'businessRelation',
|
||||||
search: true,
|
search: true,
|
||||||
searchOrder: 5,
|
searchOrder: 6,
|
||||||
searchValue: '',
|
searchValue: '',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
dicData: [
|
dicData: [
|
||||||
@@ -50,11 +50,18 @@ export const option = {
|
|||||||
slot: true,
|
slot: true,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '使用部门',
|
||||||
|
prop: 'useDepartment',
|
||||||
|
search: true,
|
||||||
|
searchOrder: 3,
|
||||||
|
minWidth: 140,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '车辆类型',
|
label: '车辆类型',
|
||||||
prop: 'vehicleType',
|
prop: 'vehicleType',
|
||||||
search: true,
|
search: true,
|
||||||
searchOrder: 4,
|
searchOrder: 5,
|
||||||
type: 'select',
|
type: 'select',
|
||||||
dicData: [
|
dicData: [
|
||||||
{ label: '全部', value: '' },
|
{ label: '全部', value: '' },
|
||||||
@@ -108,7 +115,7 @@ export const option = {
|
|||||||
label: '车辆状态',
|
label: '车辆状态',
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
search: true,
|
search: true,
|
||||||
searchOrder: 3,
|
searchOrder: 4,
|
||||||
type: 'select',
|
type: 'select',
|
||||||
slot: true,
|
slot: true,
|
||||||
dicData: [
|
dicData: [
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
const approvalStatusOptions = [
|
||||||
|
{ label: '草稿', value: 'draft' },
|
||||||
|
{ label: '审批中', value: 'reviewing' },
|
||||||
|
{ label: '已驳回', value: 'rejected' },
|
||||||
|
{ label: '审批通过', value: 'approved' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const statusName = status =>
|
||||||
|
approvalStatusOptions.find(item => item.value === status)?.label || status || '-';
|
||||||
|
|
||||||
|
export const option = {
|
||||||
|
height: 'auto',
|
||||||
|
calcHeight: 32,
|
||||||
|
dialogWidth: 1200,
|
||||||
|
labelPosition: 'right',
|
||||||
|
labelWidth: 'auto',
|
||||||
|
tip: false,
|
||||||
|
searchBtnText: '查询',
|
||||||
|
emptyBtnText: '重置',
|
||||||
|
saveBtnText: '提交',
|
||||||
|
updateBtnText: '提交',
|
||||||
|
searchShow: true,
|
||||||
|
searchMenuSpan: 24,
|
||||||
|
searchIndex: 4,
|
||||||
|
searchMenuPosition: 'right',
|
||||||
|
border: true,
|
||||||
|
index: true,
|
||||||
|
indexLabel: '序号',
|
||||||
|
indexWidth: 70,
|
||||||
|
addBtn: false,
|
||||||
|
editBtn: false,
|
||||||
|
delBtn: false,
|
||||||
|
viewBtn: false,
|
||||||
|
selection: false,
|
||||||
|
dialogClickModal: false,
|
||||||
|
menuFixed: 'right',
|
||||||
|
menuWidth: 320,
|
||||||
|
column: [
|
||||||
|
// ========== 表格列(顺序固定)==========
|
||||||
|
{
|
||||||
|
label: '申请单号',
|
||||||
|
prop: 'applicationNo',
|
||||||
|
search: true,
|
||||||
|
searchOrder: 5,
|
||||||
|
searchPlaceholder: '请输入',
|
||||||
|
minWidth: 180,
|
||||||
|
slot: true,
|
||||||
|
addDisplay: true,
|
||||||
|
editDisabled: true,
|
||||||
|
disabled: true,
|
||||||
|
placeholder: '保存后自动生成',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '车牌号',
|
||||||
|
prop: 'plateNo',
|
||||||
|
search: true,
|
||||||
|
searchOrder: 4,
|
||||||
|
searchPlaceholder: '请输入',
|
||||||
|
minWidth: 120,
|
||||||
|
formslot: true,
|
||||||
|
rules: [{ required: true, message: '请选择车牌号', trigger: 'blur' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '所属组织',
|
||||||
|
prop: 'organizationName',
|
||||||
|
search: true,
|
||||||
|
searchOrder: 3,
|
||||||
|
searchPlaceholder: '请输入',
|
||||||
|
minWidth: 140,
|
||||||
|
disabled: true,
|
||||||
|
placeholder: '选择车牌号后自动填充',
|
||||||
|
rules: [{ required: true, message: '请输入所属组织', trigger: 'blur' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '使用部门',
|
||||||
|
prop: 'useDepartment',
|
||||||
|
search: true,
|
||||||
|
searchOrder: 2,
|
||||||
|
searchPlaceholder: '请输入',
|
||||||
|
minWidth: 140,
|
||||||
|
formslot: true,
|
||||||
|
span: 12,
|
||||||
|
rules: [{ required: true, message: '请选择使用部门', trigger: 'blur' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '车辆类型',
|
||||||
|
prop: 'vehicleType',
|
||||||
|
minWidth: 110,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
viewDisplay: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '审批状态',
|
||||||
|
prop: 'approvalStatus',
|
||||||
|
search: true,
|
||||||
|
searchOrder: 1,
|
||||||
|
type: 'select',
|
||||||
|
dicData: approvalStatusOptions,
|
||||||
|
slot: true,
|
||||||
|
minWidth: 120,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '当前节点',
|
||||||
|
prop: 'currentNode',
|
||||||
|
minWidth: 120,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '当前处理人',
|
||||||
|
prop: 'currentProcessor',
|
||||||
|
minWidth: 130,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
minWidth: 120,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创建时间',
|
||||||
|
prop: 'createTime',
|
||||||
|
type: 'datetime',
|
||||||
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
minWidth: 170,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
},
|
||||||
|
// ========== 新增/编辑表单专用(不进表格)==========
|
||||||
|
{
|
||||||
|
label: '申请人',
|
||||||
|
prop: 'applicantName',
|
||||||
|
hide: true,
|
||||||
|
display: true,
|
||||||
|
addDisplay: true,
|
||||||
|
editDisplay: true,
|
||||||
|
viewDisplay: true,
|
||||||
|
disabled: true,
|
||||||
|
span: 12,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '申请时间',
|
||||||
|
prop: 'applyTime',
|
||||||
|
type: 'datetime',
|
||||||
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
hide: true,
|
||||||
|
display: true,
|
||||||
|
addDisplay: true,
|
||||||
|
editDisplay: true,
|
||||||
|
viewDisplay: true,
|
||||||
|
disabled: true,
|
||||||
|
span: 12,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '备注',
|
||||||
|
prop: 'remark',
|
||||||
|
type: 'textarea',
|
||||||
|
hide: true,
|
||||||
|
span: 24,
|
||||||
|
maxlength: 200,
|
||||||
|
showWordLimit: true,
|
||||||
|
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '附件',
|
||||||
|
prop: 'attachments',
|
||||||
|
hide: true,
|
||||||
|
span: 24,
|
||||||
|
minWidth: 100,
|
||||||
|
formslot: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -70,14 +70,16 @@ export const option = {
|
|||||||
{
|
{
|
||||||
label: '车牌号/船号',
|
label: '车牌号/船号',
|
||||||
prop: 'vehicleNo',
|
prop: 'vehicleNo',
|
||||||
|
type: 'select',
|
||||||
slot: true,
|
slot: true,
|
||||||
formslot: true,
|
formslot: true,
|
||||||
search: true,
|
search: true,
|
||||||
|
searchType: 'input',
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
span: 12,
|
span: 12,
|
||||||
placeholder: '输入车牌号/船号查询选择',
|
placeholder: '请选择车牌号/船号',
|
||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: '请输入车牌号/船号', trigger: 'blur' },
|
{ required: true, message: '请选择车牌号/船号', trigger: 'change' },
|
||||||
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
|
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -268,6 +270,19 @@ export const option = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const exportColumns = [
|
||||||
|
{ prop: 'vehicleType', label: '车船类型' },
|
||||||
|
{ prop: 'vehicleNo', label: '车牌号/船号' },
|
||||||
|
{ prop: 'accidentDate', label: '事故发生日期' },
|
||||||
|
{ prop: 'accidentLocation', label: '事故发生地点' },
|
||||||
|
{ prop: 'accidentNature', label: '事故性质' },
|
||||||
|
{ prop: 'accidentResponsibility', label: '事故责任' },
|
||||||
|
{ prop: 'directEconomicLoss', label: '直接经济损失' },
|
||||||
|
{ prop: 'insuranceClaimAmount', label: '保险理赔金额' },
|
||||||
|
{ prop: 'accidentReasonDamage', label: '事故原因及损坏情况' },
|
||||||
|
{ prop: 'remark', label: '备注' },
|
||||||
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
labelPosition: 'right',
|
labelPosition: 'right',
|
||||||
submitBtn: false,
|
submitBtn: false,
|
||||||
|
|||||||
@@ -75,14 +75,16 @@ export const option = {
|
|||||||
{
|
{
|
||||||
label: '车牌号/船号',
|
label: '车牌号/船号',
|
||||||
prop: 'vehicleNo',
|
prop: 'vehicleNo',
|
||||||
|
type: 'select',
|
||||||
slot: true,
|
slot: true,
|
||||||
formslot: true,
|
formslot: true,
|
||||||
search: true,
|
search: true,
|
||||||
|
searchType: 'input',
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
span: 12,
|
span: 12,
|
||||||
placeholder: '输入车牌号/船号查询选择',
|
placeholder: '请选择车牌号/船号',
|
||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: '请输入车牌号/船号', trigger: 'blur' },
|
{ required: true, message: '请选择车牌号/船号', trigger: 'change' },
|
||||||
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
|
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -105,6 +107,7 @@ export const option = {
|
|||||||
type: 'date',
|
type: 'date',
|
||||||
format: 'YYYY-MM-DD',
|
format: 'YYYY-MM-DD',
|
||||||
valueFormat: 'YYYY-MM-DD',
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
slot: true,
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
span: 12,
|
span: 12,
|
||||||
placeholder: '请输入',
|
placeholder: '请输入',
|
||||||
@@ -191,23 +194,12 @@ export const option = {
|
|||||||
formslot: true,
|
formslot: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '检测评定开始日期',
|
label: '检测评定日期',
|
||||||
prop: 'inspectionAssessmentDateStart',
|
prop: 'inspectionAssessmentDateRange',
|
||||||
type: 'date',
|
|
||||||
format: 'YYYY-MM-DD',
|
|
||||||
valueFormat: 'YYYY-MM-DD',
|
|
||||||
search: true,
|
|
||||||
hide: true,
|
|
||||||
addDisplay: false,
|
|
||||||
editDisplay: false,
|
|
||||||
viewDisplay: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '检测评定结束日期',
|
|
||||||
prop: 'inspectionAssessmentDateEnd',
|
|
||||||
type: 'date',
|
type: 'date',
|
||||||
format: 'YYYY-MM-DD',
|
format: 'YYYY-MM-DD',
|
||||||
valueFormat: 'YYYY-MM-DD',
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
searchRange: true,
|
||||||
search: true,
|
search: true,
|
||||||
hide: true,
|
hide: true,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
@@ -278,6 +270,20 @@ export const option = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const exportColumns = [
|
||||||
|
{ prop: 'vehicleType', label: '车船类型' },
|
||||||
|
{ prop: 'vehicleNo', label: '车牌号/船号' },
|
||||||
|
{ prop: 'inspectionAssessmentDate', label: '检测评定日期' },
|
||||||
|
{ prop: 'validUntilDate', label: '有效期截止日' },
|
||||||
|
{ prop: 'vehicleTechnicalLevel', label: '车辆技术等级' },
|
||||||
|
{ prop: 'shipInspectionType', label: '船舶检验类型' },
|
||||||
|
{ prop: 'passengerTypeLevel', label: '客车类型及等级' },
|
||||||
|
{ prop: 'inspectionUnit', label: '检测评定单位' },
|
||||||
|
{ prop: 'fee', label: '费用(元)' },
|
||||||
|
{ prop: 'assessmentUnit', label: '评定(复核)单位' },
|
||||||
|
{ prop: 'remark', label: '备注' },
|
||||||
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
labelPosition: 'right',
|
labelPosition: 'right',
|
||||||
submitBtn: false,
|
submitBtn: false,
|
||||||
|
|||||||
@@ -54,9 +54,11 @@ export const option = {
|
|||||||
{
|
{
|
||||||
label: '车牌号/船号',
|
label: '车牌号/船号',
|
||||||
prop: 'vehicleNo',
|
prop: 'vehicleNo',
|
||||||
|
type: 'select',
|
||||||
slot: true,
|
slot: true,
|
||||||
formslot: true,
|
formslot: true,
|
||||||
search: true,
|
search: true,
|
||||||
|
searchType: 'input',
|
||||||
searchOrder: 4,
|
searchOrder: 4,
|
||||||
span: 12,
|
span: 12,
|
||||||
order: 10,
|
order: 10,
|
||||||
@@ -211,6 +213,19 @@ export const option = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const exportColumns = [
|
||||||
|
{ prop: 'vehicleType', label: '车船类型' },
|
||||||
|
{ prop: 'vehicleNo', label: '车牌号/船号' },
|
||||||
|
{ prop: 'equipmentCode', label: '设备号' },
|
||||||
|
{ prop: 'equipmentName', label: '设备名称' },
|
||||||
|
{ prop: 'factoryDate', label: '出厂日期' },
|
||||||
|
{ prop: 'equipmentBrand', label: '设备品牌' },
|
||||||
|
{ prop: 'equipmentType', label: '设备类型' },
|
||||||
|
{ prop: 'specificationModel', label: '规格型号' },
|
||||||
|
{ prop: 'remark', label: '备注' },
|
||||||
|
{ prop: 'originalEquipmentNo', label: '原厂设备号' },
|
||||||
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
labelPosition: 'right',
|
labelPosition: 'right',
|
||||||
submitBtn: false,
|
submitBtn: false,
|
||||||
|
|||||||
@@ -49,14 +49,16 @@ export const option = {
|
|||||||
{
|
{
|
||||||
label: '车牌号',
|
label: '车牌号',
|
||||||
prop: 'vehicleNo',
|
prop: 'vehicleNo',
|
||||||
|
type: 'select',
|
||||||
slot: true,
|
slot: true,
|
||||||
formslot: true,
|
formslot: true,
|
||||||
search: true,
|
search: true,
|
||||||
|
searchType: 'input',
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
span: 12,
|
span: 12,
|
||||||
placeholder: '输入车牌号模糊查询选择',
|
placeholder: '请选择车牌号',
|
||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: '请输入车牌号', trigger: 'blur' },
|
{ required: true, message: '请选择车牌号', trigger: 'change' },
|
||||||
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
|
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -206,6 +208,18 @@ export const option = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const exportColumns = [
|
||||||
|
{ prop: 'vehicleNo', label: '车牌号' },
|
||||||
|
{ prop: 'entryTime', label: '入口时间' },
|
||||||
|
{ prop: 'etcCardNo', label: 'ETC卡号' },
|
||||||
|
{ prop: 'exitTime', label: '出口时间' },
|
||||||
|
{ prop: 'entryStation', label: '入口站' },
|
||||||
|
{ prop: 'transactionAmount', label: '交易金额' },
|
||||||
|
{ prop: 'exitStation', label: '出口站' },
|
||||||
|
{ prop: 'dataSource', label: '数据来源' },
|
||||||
|
{ prop: 'remark', label: '备注' },
|
||||||
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
labelPosition: 'right',
|
labelPosition: 'right',
|
||||||
submitBtn: false,
|
submitBtn: false,
|
||||||
|
|||||||
@@ -78,17 +78,20 @@ export const option = {
|
|||||||
prop: 'policyFile',
|
prop: 'policyFile',
|
||||||
formslot: true,
|
formslot: true,
|
||||||
hide: true,
|
hide: true,
|
||||||
|
viewDisplay: false,
|
||||||
span: 24,
|
span: 24,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '车牌号/船号',
|
label: '车牌号/船号',
|
||||||
prop: 'vehicleNo',
|
prop: 'vehicleNo',
|
||||||
|
type: 'select',
|
||||||
slot: true,
|
slot: true,
|
||||||
formslot: true,
|
formslot: true,
|
||||||
search: true,
|
search: true,
|
||||||
|
searchType: 'input',
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: '请输入车牌号/船号', trigger: 'blur' },
|
{ required: true, message: '请选择车牌号/船号', trigger: 'change' },
|
||||||
{ max: 50, message: '最多 50 个字符', trigger: 'blur' },
|
{ max: 50, message: '最多 50 个字符', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -108,6 +111,7 @@ export const option = {
|
|||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: '请输入保单号', trigger: 'blur' },
|
{ required: true, message: '请输入保单号', trigger: 'blur' },
|
||||||
{ max: 80, message: '最多 80 个字符', trigger: 'blur' },
|
{ max: 80, message: '最多 80 个字符', trigger: 'blur' },
|
||||||
|
{ pattern: /^[a-zA-Z0-9]+$/, message: '保单号只能输入数字、字母', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -240,6 +244,20 @@ export const option = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const exportColumns = [
|
||||||
|
{ prop: 'vehicleType', label: '车船类型' },
|
||||||
|
{ prop: 'vehicleNo', label: '车牌号/船号' },
|
||||||
|
{ prop: 'insuranceType', label: '保险类型' },
|
||||||
|
{ prop: 'policyNo', label: '保单号' },
|
||||||
|
{ prop: 'startDate', label: '开始日期' },
|
||||||
|
{ prop: 'endDate', label: '结束日期' },
|
||||||
|
{ prop: 'insuredAmount', label: '保额' },
|
||||||
|
{ prop: 'premium', label: '保费' },
|
||||||
|
{ prop: 'invoiceNo', label: '发票号' },
|
||||||
|
{ prop: 'invoiceDate', label: '开票日期' },
|
||||||
|
{ prop: 'remark', label: '备注' },
|
||||||
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
labelPosition: 'right',
|
labelPosition: 'right',
|
||||||
submitBtn: false,
|
submitBtn: false,
|
||||||
|
|||||||
@@ -40,13 +40,15 @@ export const option = {
|
|||||||
{
|
{
|
||||||
label: '车牌号',
|
label: '车牌号',
|
||||||
prop: 'vehicleNo',
|
prop: 'vehicleNo',
|
||||||
|
type: 'select',
|
||||||
slot: true,
|
slot: true,
|
||||||
formslot: true,
|
formslot: true,
|
||||||
search: true,
|
search: true,
|
||||||
|
searchType: 'input',
|
||||||
searchSpan: 6,
|
searchSpan: 6,
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: '请输入车牌号', trigger: 'blur' },
|
{ required: true, message: '请选择车牌号', trigger: 'change' },
|
||||||
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
|
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -199,6 +201,15 @@ export const option = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const exportColumns = [
|
||||||
|
{ prop: 'vehicleNo', label: '车牌号' },
|
||||||
|
{ prop: 'previousMonthMileage', label: '上月统计里程数' },
|
||||||
|
{ prop: 'currentMonthMileage', label: '本月统计里程数' },
|
||||||
|
{ prop: 'monthlyMileage', label: '本月行驶里程数' },
|
||||||
|
{ prop: 'totalMileage', label: '累计行驶里程数' },
|
||||||
|
{ prop: 'remark', label: '备注' },
|
||||||
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
labelPosition: 'right',
|
labelPosition: 'right',
|
||||||
submitBtn: false,
|
submitBtn: false,
|
||||||
|
|||||||
@@ -92,13 +92,15 @@ export const option = {
|
|||||||
{
|
{
|
||||||
label: '车牌号/船号',
|
label: '车牌号/船号',
|
||||||
prop: 'vehicleNo',
|
prop: 'vehicleNo',
|
||||||
|
type: 'select',
|
||||||
slot: true,
|
slot: true,
|
||||||
formslot: true,
|
formslot: true,
|
||||||
search: true,
|
search: true,
|
||||||
|
searchType: 'input',
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
placeholder: '输入车牌号/船号查询选择',
|
placeholder: '请选择车牌号/船号',
|
||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: '请输入车牌号/船号', trigger: 'blur' },
|
{ required: true, message: '请选择车牌号/船号', trigger: 'change' },
|
||||||
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
|
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -287,6 +289,23 @@ export const option = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const exportColumns = [
|
||||||
|
{ prop: 'cardNo', label: '卡号' },
|
||||||
|
{ prop: 'transactionTime', label: '交易时间' },
|
||||||
|
{ prop: 'vehicleType', label: '车船类型' },
|
||||||
|
{ prop: 'feeType', label: '费用类型' },
|
||||||
|
{ prop: 'oilProduct', label: '油品' },
|
||||||
|
{ prop: 'vehicleNo', label: '车牌号/船号' },
|
||||||
|
{ prop: 'cardHolder', label: '持卡人' },
|
||||||
|
{ prop: 'dataSource', label: '数据来源' },
|
||||||
|
{ prop: 'quantity', label: '数量' },
|
||||||
|
{ prop: 'unitPrice', label: '单价' },
|
||||||
|
{ prop: 'transactionAmount', label: '交易金额' },
|
||||||
|
{ prop: 'balance', label: '余额' },
|
||||||
|
{ prop: 'station', label: '站点' },
|
||||||
|
{ prop: 'remark', label: '备注' },
|
||||||
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
labelPosition: 'right',
|
labelPosition: 'right',
|
||||||
submitBtn: false,
|
submitBtn: false,
|
||||||
|
|||||||
@@ -17,16 +17,6 @@ export const vehicleTypeDic = [
|
|||||||
{ label: '船舶', value: '船舶' },
|
{ label: '船舶', value: '船舶' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const expenseTypeDic = [
|
|
||||||
{ label: '过路费', value: '过路费' },
|
|
||||||
{ label: '停车费', value: '停车费' },
|
|
||||||
{ label: '维修费', value: '维修费' },
|
|
||||||
{ label: '保险费', value: '保险费' },
|
|
||||||
{ label: '年检费', value: '年检费' },
|
|
||||||
{ label: '装卸费', value: '装卸费' },
|
|
||||||
{ label: '其他', value: '其他' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const option = {
|
export const option = {
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
calcHeight: 32,
|
calcHeight: 32,
|
||||||
@@ -68,14 +58,16 @@ export const option = {
|
|||||||
{
|
{
|
||||||
label: '车牌号/船号',
|
label: '车牌号/船号',
|
||||||
prop: 'vehicleNo',
|
prop: 'vehicleNo',
|
||||||
|
type: 'select',
|
||||||
slot: true,
|
slot: true,
|
||||||
formslot: true,
|
formslot: true,
|
||||||
search: true,
|
search: true,
|
||||||
|
searchType: 'input',
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
span: 12,
|
span: 12,
|
||||||
placeholder: '输入车牌号/船号查询选择',
|
placeholder: '请选择车牌号/船号',
|
||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: '请输入车牌号/船号', trigger: 'blur' },
|
{ required: true, message: '请选择车牌号/船号', trigger: 'change' },
|
||||||
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
|
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -95,7 +87,7 @@ export const option = {
|
|||||||
label: '费用类型',
|
label: '费用类型',
|
||||||
prop: 'expenseType',
|
prop: 'expenseType',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
dicData: expenseTypeDic,
|
dicData: [],
|
||||||
search: true,
|
search: true,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
span: 12,
|
span: 12,
|
||||||
@@ -209,6 +201,16 @@ export const option = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const exportColumns = [
|
||||||
|
{ prop: 'vehicleType', label: '车船类型' },
|
||||||
|
{ prop: 'vehicleNo', label: '车牌号/船号' },
|
||||||
|
{ prop: 'expenseDate', label: '费用日期' },
|
||||||
|
{ prop: 'expenseType', label: '费用类型' },
|
||||||
|
{ prop: 'amount', label: '金额' },
|
||||||
|
{ prop: 'dataSource', label: '数据来源' },
|
||||||
|
{ prop: 'remark', label: '备注' },
|
||||||
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
labelPosition: 'right',
|
labelPosition: 'right',
|
||||||
submitBtn: false,
|
submitBtn: false,
|
||||||
|
|||||||
@@ -49,14 +49,16 @@ export const option = {
|
|||||||
{
|
{
|
||||||
label: '车牌号',
|
label: '车牌号',
|
||||||
prop: 'vehicleNo',
|
prop: 'vehicleNo',
|
||||||
|
type: 'select',
|
||||||
slot: true,
|
slot: true,
|
||||||
formslot: true,
|
formslot: true,
|
||||||
search: true,
|
search: true,
|
||||||
|
searchType: 'input',
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
span: 12,
|
span: 12,
|
||||||
placeholder: '输入车牌号查询选择',
|
placeholder: '请选择车牌号',
|
||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: '请输入车牌号', trigger: 'blur' },
|
{ required: true, message: '请选择车牌号', trigger: 'change' },
|
||||||
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
|
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -206,6 +208,17 @@ export const option = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const exportColumns = [
|
||||||
|
{ prop: 'vehicleNo', label: '车牌号' },
|
||||||
|
{ prop: 'handler', label: '处理人' },
|
||||||
|
{ prop: 'replacementTime', label: '换胎时间' },
|
||||||
|
{ prop: 'tireBrand', label: '轮胎品牌' },
|
||||||
|
{ prop: 'tireQuantity', label: '换胎数量' },
|
||||||
|
{ prop: 'replacementCost', label: '换胎费用' },
|
||||||
|
{ prop: 'replacementDescription', label: '换胎说明' },
|
||||||
|
{ prop: 'remark', label: '备注' },
|
||||||
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
labelPosition: 'right',
|
labelPosition: 'right',
|
||||||
submitBtn: false,
|
submitBtn: false,
|
||||||
|
|||||||
@@ -47,11 +47,13 @@ export const option = {
|
|||||||
{
|
{
|
||||||
label: '车牌号/船号',
|
label: '车牌号/船号',
|
||||||
prop: 'vehicleNo',
|
prop: 'vehicleNo',
|
||||||
|
type: 'select',
|
||||||
slot: true,
|
slot: true,
|
||||||
formslot: true,
|
formslot: true,
|
||||||
search: true,
|
search: true,
|
||||||
|
searchType: 'input',
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
placeholder: '输入车牌号/船号模糊查询选择',
|
placeholder: '请选择车牌号/船号',
|
||||||
rules: [{ max: 30, message: '最多 30 个字符', trigger: 'blur' }],
|
rules: [{ max: 30, message: '最多 30 个字符', trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -161,6 +163,14 @@ export const option = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const exportColumns = [
|
||||||
|
{ prop: 'vehicleType', label: '车船类型' },
|
||||||
|
{ prop: 'vehicleNo', label: '车牌号/船号' },
|
||||||
|
{ prop: 'changeItem', label: '变更事项' },
|
||||||
|
{ prop: 'changeContent', label: '变更内容' },
|
||||||
|
{ prop: 'remark', label: '备注' },
|
||||||
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
labelPosition: 'right',
|
labelPosition: 'right',
|
||||||
submitBtn: false,
|
submitBtn: false,
|
||||||
|
|||||||
@@ -70,14 +70,16 @@ export const option = {
|
|||||||
{
|
{
|
||||||
label: '车牌号/船号',
|
label: '车牌号/船号',
|
||||||
prop: 'vehicleNo',
|
prop: 'vehicleNo',
|
||||||
|
type: 'select',
|
||||||
slot: true,
|
slot: true,
|
||||||
formslot: true,
|
formslot: true,
|
||||||
search: true,
|
search: true,
|
||||||
|
searchType: 'input',
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
span: 12,
|
span: 12,
|
||||||
placeholder: '输入车牌号/船号查询选择',
|
placeholder: '请选择车牌号/船号',
|
||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: '请输入车牌号/船号', trigger: 'blur' },
|
{ required: true, message: '请选择车牌号/船号', trigger: 'change' },
|
||||||
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
|
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -117,13 +119,13 @@ export const option = {
|
|||||||
{
|
{
|
||||||
label: '日期',
|
label: '日期',
|
||||||
prop: 'violationTime',
|
prop: 'violationTime',
|
||||||
type: 'datetime',
|
type: 'date',
|
||||||
format: 'YYYY-MM-DD HH:mm:ss',
|
format: 'YYYY-MM-DD',
|
||||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
minWidth: 170,
|
minWidth: 140,
|
||||||
span: 12,
|
span: 12,
|
||||||
placeholder: '请选择',
|
placeholder: '请选择',
|
||||||
rules: [{ required: true, message: '请选择时间', trigger: 'click' }],
|
rules: [{ required: true, message: '请选择日期', trigger: 'click' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '地点',
|
label: '地点',
|
||||||
@@ -173,7 +175,7 @@ export const option = {
|
|||||||
prop: 'processStatus',
|
prop: 'processStatus',
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
dicData: processStatusDic,
|
dicData: processStatusDic,
|
||||||
value: '未处理',
|
value: '已处理',
|
||||||
search: true,
|
search: true,
|
||||||
clearable: true,
|
clearable: true,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
@@ -278,6 +280,22 @@ export const option = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const exportColumns = [
|
||||||
|
{ prop: 'vehicleType', label: '车船类型' },
|
||||||
|
{ prop: 'vehicleNo', label: '车牌号/船号' },
|
||||||
|
{ prop: 'driverName', label: '驾驶人' },
|
||||||
|
{ prop: 'violationType', label: '类型' },
|
||||||
|
{ prop: 'violationItem', label: '事项' },
|
||||||
|
{ prop: 'violationTime', label: '日期' },
|
||||||
|
{ prop: 'location', label: '地点' },
|
||||||
|
{ prop: 'fineAmount', label: '罚款' },
|
||||||
|
{ prop: 'deductPoints', label: '被扣分数' },
|
||||||
|
{ prop: 'penaltyUnit', label: '被罚单位' },
|
||||||
|
{ prop: 'processStatus', label: '状态' },
|
||||||
|
{ prop: 'processDescription', label: '过程描述' },
|
||||||
|
{ prop: 'processResult', label: '处理结果' },
|
||||||
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
labelPosition: 'right',
|
labelPosition: 'right',
|
||||||
submitBtn: false,
|
submitBtn: false,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<div class="login-main">
|
<div class="login-main">
|
||||||
<p class="login-title">
|
<p class="login-title">
|
||||||
{{ $t('login.title') }}{{ website.title }}
|
{{ $t('login.title') }}{{ website.title }}
|
||||||
<top-lang></top-lang>
|
<!-- <top-lang></top-lang>-->
|
||||||
</p>
|
</p>
|
||||||
<!-- 原登录方式先注释保留备用
|
<!-- 原登录方式先注释保留备用
|
||||||
<userLogin v-if="activeName === 'user'"></userLogin>
|
<userLogin v-if="activeName === 'user'"></userLogin>
|
||||||
|
|||||||
@@ -7,18 +7,6 @@
|
|||||||
:model="loginForm"
|
:model="loginForm"
|
||||||
label-width="0"
|
label-width="0"
|
||||||
>
|
>
|
||||||
<el-form-item v-if="tenantMode" prop="tenantId">
|
|
||||||
<el-input
|
|
||||||
@keyup.enter="handleLogin"
|
|
||||||
v-model="loginForm.tenantId"
|
|
||||||
auto-complete="off"
|
|
||||||
:placeholder="$t('login.tenantId')"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<i class="icon-quanxian" />
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="username">
|
<el-form-item prop="username">
|
||||||
<el-input
|
<el-input
|
||||||
@keyup.enter="handleLogin"
|
@keyup.enter="handleLogin"
|
||||||
@@ -67,13 +55,13 @@
|
|||||||
<el-button type="primary" @click.prevent="handleLogin" class="login-submit"
|
<el-button type="primary" @click.prevent="handleLogin" class="login-submit"
|
||||||
>{{ $t('login.submit') }}
|
>{{ $t('login.submit') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<!-- <el-button-->
|
||||||
v-if="this.registerMode"
|
<!-- v-if="this.registerMode"-->
|
||||||
type="danger"
|
<!-- type="danger"-->
|
||||||
@click.prevent="handleRegister"
|
<!-- @click.prevent="handleRegister"-->
|
||||||
class="register-submit"
|
<!-- class="register-submit"-->
|
||||||
>{{ $t('login.register') }}
|
<!-- >{{ $t('login.register') }}-->
|
||||||
</el-button>
|
<!-- </el-button>-->
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<behavior-captcha ref="behaviorCaptcha" @success="handleBehaviorSuccess"></behavior-captcha>
|
<behavior-captcha ref="behaviorCaptcha" @success="handleBehaviorSuccess"></behavior-captcha>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -118,7 +106,6 @@ export default {
|
|||||||
image: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
|
image: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
|
||||||
},
|
},
|
||||||
loginRules: {
|
loginRules: {
|
||||||
tenantId: [{ required: false, message: '请输入租户ID', trigger: 'blur' }],
|
|
||||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||||
password: [
|
password: [
|
||||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
|||||||
@@ -2,11 +2,35 @@ import router from './router/';
|
|||||||
import store from './store';
|
import store from './store';
|
||||||
import { tabKeyOf } from '@/router/tab';
|
import { tabKeyOf } from '@/router/tab';
|
||||||
import { getToken } from '@/utils/auth';
|
import { getToken } from '@/utils/auth';
|
||||||
|
import {
|
||||||
|
isChunkLoadError,
|
||||||
|
reloadForChunkError,
|
||||||
|
setPendingRoutePath,
|
||||||
|
} from '@/utils/chunk-reload';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
import NProgress from 'nprogress'; // progress bar
|
import NProgress from 'nprogress'; // progress bar
|
||||||
import 'nprogress/nprogress.css'; // progress bar style
|
import 'nprogress/nprogress.css'; // progress bar style
|
||||||
NProgress.configure({ showSpinner: false });
|
NProgress.configure({ showSpinner: false });
|
||||||
const lockPage = '/lock'; //锁屏页
|
const lockPage = '/lock'; //锁屏页
|
||||||
|
|
||||||
|
// 懒加载 chunk / CSS 失败时整页刷新,避免菜单点击后卡死
|
||||||
|
router.onError(error => {
|
||||||
|
if (!isChunkLoadError(error)) return;
|
||||||
|
if (reloadForChunkError()) {
|
||||||
|
ElMessage.warning('页面资源加载失败,正在重新加载…');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('unhandledrejection', event => {
|
||||||
|
if (!isChunkLoadError(event.reason)) return;
|
||||||
|
if (reloadForChunkError()) {
|
||||||
|
event.preventDefault();
|
||||||
|
ElMessage.warning('页面资源加载失败,正在重新加载…');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
|
setPendingRoutePath(to.fullPath);
|
||||||
const meta = to.meta || {};
|
const meta = to.meta || {};
|
||||||
const isMenu = meta.menu === undefined ? to.query.menu : meta.menu;
|
const isMenu = meta.menu === undefined ? to.query.menu : meta.menu;
|
||||||
store.commit('SET_IS_MENU', isMenu === undefined);
|
store.commit('SET_IS_MENU', isMenu === undefined);
|
||||||
|
|||||||
@@ -116,6 +116,33 @@ export default [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/settlement/transport-reconciliation/form',
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: '新增编辑运输对账单',
|
||||||
|
meta: {
|
||||||
|
keepAlive: false,
|
||||||
|
activeMenu: '/settlement/transport-reconciliation',
|
||||||
|
},
|
||||||
|
component: () => import('@/views/settlement/transport-reconciliation-form.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/business/voucher-manage/detail',
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: '执行凭证批次详情',
|
||||||
|
meta: { keepAlive: false, activeMenu: '/business/voucher-manage' },
|
||||||
|
component: () => import('@/views/business/voucher-manage-detail.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/business/contract-manage/form',
|
path: '/business/contract-manage/form',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
@@ -128,6 +155,18 @@ export default [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/business/contract-manage/detail',
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: '合同详情',
|
||||||
|
meta: { keepAlive: false, activeMenu: '/business/contract-manage' },
|
||||||
|
component: () => import('@/views/business/contract-manage.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/business/project-apply/form',
|
path: '/business/project-apply/form',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
@@ -152,6 +191,42 @@ export default [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/business/waybill-manage/detail',
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: '运单管理详情',
|
||||||
|
meta: { keepAlive: false, activeMenu: '/business/waybill-manage' },
|
||||||
|
component: () => import('@/views/business/waybill-manage-detail.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/business/waybill-import',
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: '导入运单',
|
||||||
|
meta: { keepAlive: false, activeMenu: '/business/waybill-import' },
|
||||||
|
component: () => import('@/views/business/waybill-import.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/business/waybill-import/form',
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: '新建导入运单',
|
||||||
|
meta: { keepAlive: false, activeMenu: '/business/waybill-import' },
|
||||||
|
component: () => import('@/views/business/waybill-import-form.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/business/loading-manage/form',
|
path: '/business/loading-manage/form',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
@@ -176,6 +251,30 @@ export default [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/business/transport-plan/import',
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: '导入运输计划',
|
||||||
|
meta: { keepAlive: false, activeMenu: '/business/transport-plan' },
|
||||||
|
component: () => import('@/views/business/transport-plan-import.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/business/transport-plan-dispatch',
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: '计划调度',
|
||||||
|
meta: { keepAlive: false, activeMenu: '/business/transport-plan' },
|
||||||
|
component: () => import('@/views/business/transport-plan-dispatch.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/business/shipping-template/form',
|
path: '/business/shipping-template/form',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
|
|||||||
@@ -149,3 +149,20 @@ a {
|
|||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
.map-picker-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
> [class$='__map'],
|
||||||
|
> .address-map {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
> .amap-container {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+47
-12
@@ -213,6 +213,17 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 全站弹窗标题统一带 4px 主色竖条(与 .dialog-section-title 保持一致)
|
||||||
|
// :not(:empty) 避免无标题弹窗出现孤立竖条
|
||||||
|
.el-dialog__title:not(:empty)::before {
|
||||||
|
content: '';
|
||||||
|
flex: none;
|
||||||
|
width: 4px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.dialog-section-title::before {
|
.dialog-section-title::before {
|
||||||
content: '';
|
content: '';
|
||||||
flex: none;
|
flex: none;
|
||||||
@@ -708,9 +719,9 @@
|
|||||||
.avue-crud .el-table__body-wrapper tr.el-table__row:nth-child(even) td.el-table__cell,
|
.avue-crud .el-table__body-wrapper tr.el-table__row:nth-child(even) td.el-table__cell,
|
||||||
.avue-crud .el-table__body-wrapper tr.el-table__row:nth-child(even) td.el-table-fixed-column--left,
|
.avue-crud .el-table__body-wrapper tr.el-table__row:nth-child(even) td.el-table-fixed-column--left,
|
||||||
.avue-crud
|
.avue-crud
|
||||||
.el-table__body-wrapper
|
.el-table__body-wrapper
|
||||||
tr.el-table__row:nth-child(even)
|
tr.el-table__row:nth-child(even)
|
||||||
td.el-table-fixed-column--right {
|
td.el-table-fixed-column--right {
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1050,21 +1061,38 @@
|
|||||||
.el-form-item--label-right .el-form-item__label {
|
.el-form-item--label-right .el-form-item__label {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
// ============ 必填星号 * 与 label 首行齐平(仅换行时生效) ============
|
||||||
|
// label 为 inline-flex,星号 ::before 与文字是两个独立 flex 子项。
|
||||||
|
// align-items: normal(= stretch):单行时星号盒子 = 一行高,位置不变;
|
||||||
|
// 折行后星号盒子被拉伸到 label 全高,但其内部按 line-height 从顶部排布 → 星号停在第一行。
|
||||||
|
// height: auto 覆盖 Element Plus 写死的 32px,让 label 收缩到内容高;
|
||||||
|
// align-self: center 再让收缩后的 label 在表单行内垂直居中(单行与输入框对齐)。
|
||||||
|
.is-required .el-form-item__label {
|
||||||
|
height: auto;
|
||||||
|
align-self: center;
|
||||||
|
align-items: normal;
|
||||||
|
}
|
||||||
.el-form-item--default {
|
.el-form-item--default {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 必填星号 * 与首行文字垂直居中 ============
|
// 配套:星号必须保持「高度 auto + 继承行高」,stretch 才会按首行排布;
|
||||||
// Element Plus 默认 .el-form-item__label 为 inline-flex + align-items: flex-start,
|
// 写死 height 或 line-height 与 height 相等时效果会退化成居中。
|
||||||
// 星号 :before 高度(1em) 与文字行盒(line-height) 不一致时,多行/折行 label 会偏上。
|
|
||||||
// 让 :before 跟随 label 行高、以 inline-block 参与行内对齐即可修复,
|
|
||||||
// 兼容单行(默认 32px)与多行(auto + 18px)两种 label 高度场景。
|
|
||||||
// 注:客商档案(customer-archive)的 label 为 auto 高度 + padding 下沉方案,scoped 特异性更高会覆盖本规则。
|
// 注:客商档案(customer-archive)的 label 为 auto 高度 + padding 下沉方案,scoped 特异性更高会覆盖本规则。
|
||||||
.el-form-item.is-required .el-form-item__label::before {
|
.el-form-item.is-required .el-form-item__label::before {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
height: auto;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ archive-form:label 与控件垂直居中(全站复用) ============
|
||||||
|
// archive-form class 被 driver / vehicle / ship / credit-score-quantification / loading-manage 等
|
||||||
|
// 多个页面复用,但「垂直居中」规则原本锁在 customer-archive.vue 的 scoped 样式里,
|
||||||
|
// 其它页面拿不到 → 高控件行(证件上传 151px)label 贴顶。提升为全局统一生效。
|
||||||
|
.archive-form .el-form-item {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
//.el-form-item {
|
//.el-form-item {
|
||||||
// margin-bottom: 15px !important;
|
// margin-bottom: 15px !important;
|
||||||
//}
|
//}
|
||||||
@@ -1117,11 +1145,18 @@
|
|||||||
color: #909399 !important;
|
color: #909399 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已选中:选中文本用主文字色 #333333,与占位符明显区分。
|
// 已选中:选中文本用主文字色 #606266,与占位符明显区分。
|
||||||
// 注意:Element Plus 选中值节点同时带 el-select__selected-item 与 el-select__placeholder 两个类,
|
// 关键:Element Plus 的「选中值文本」与「占位符」共用同一个节点,
|
||||||
// 而上面占位符规则用了 !important,故此处必须同样 !important 才能胜出(本规则 specificity 更高)。
|
// <div class="el-select__selected-item el-select__placeholder [is-transparent]">
|
||||||
|
// 只有未选值(或已展开但无搜索输入)时才会带 .is-transparent。
|
||||||
|
// 因此这里必须排除 .is-transparent,否则未选值的占位符也会被染成选中色,与占位符灰无法区分。
|
||||||
// 仅作用于非禁用态,禁用态保留 Element Plus 的禁用灰(--el-select-disabled-color),避免被覆盖。
|
// 仅作用于非禁用态,禁用态保留 Element Plus 的禁用灰(--el-select-disabled-color),避免被覆盖。
|
||||||
.el-select__wrapper:not(.is-disabled) .el-select__selected-item {
|
.el-select__wrapper:not(.is-disabled) .el-select__selected-item:not(.is-transparent) {
|
||||||
|
color: #606266 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多选:选中项渲染为 el-tag,文字色由 tag 自身控制,上面规则覆盖不到,单独处理。
|
||||||
|
.el-select__wrapper:not(.is-disabled) .el-select__tags-text {
|
||||||
color: #606266 !important;
|
color: #606266 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* 懒加载 chunk / CSS preload 失败后的整页恢复。
|
||||||
|
* 常见于:部署后旧 hash 失效、静态服务空闲断连、代理 CONNECTION_RESET。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const RELOAD_FLAG = 'app:chunk-reload-ts';
|
||||||
|
const RELOAD_COOLDOWN_MS = 10000;
|
||||||
|
|
||||||
|
const CHUNK_ERROR_RE =
|
||||||
|
/Failed to fetch dynamically imported module|Importing a module script failed|Unable to preload CSS|error loading dynamically imported module|Loading CSS chunk|Loading chunk .+ failed|ChunkLoadError/i;
|
||||||
|
|
||||||
|
/** 最近一次路由跳转目标,供 onError 时整页落到正确地址 */
|
||||||
|
let pendingFullPath = '';
|
||||||
|
|
||||||
|
export function setPendingRoutePath(fullPath = '') {
|
||||||
|
pendingFullPath = fullPath || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isChunkLoadError(error) {
|
||||||
|
if (!error) return false;
|
||||||
|
const message = error.message || String(error);
|
||||||
|
return CHUNK_ERROR_RE.test(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean} 是否已触发刷新(冷却期内返回 false,避免死循环)
|
||||||
|
*/
|
||||||
|
export function reloadForChunkError(targetPath) {
|
||||||
|
const now = Date.now();
|
||||||
|
const last = Number(sessionStorage.getItem(RELOAD_FLAG) || 0);
|
||||||
|
if (now - last < RELOAD_COOLDOWN_MS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
sessionStorage.setItem(RELOAD_FLAG, String(now));
|
||||||
|
const path = targetPath || pendingFullPath || window.location.pathname + window.location.search + window.location.hash;
|
||||||
|
window.location.assign(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -7,8 +7,20 @@ import 'nprogress/nprogress.css';
|
|||||||
const excelPattern = /\.(xls|xlsx)$/;
|
const excelPattern = /\.(xls|xlsx)$/;
|
||||||
|
|
||||||
const isExcelResponse = res => {
|
const isExcelResponse = res => {
|
||||||
const contentType = res.headers['content-type'] || res.data.type || '';
|
const headers = res.headers || {};
|
||||||
return contentType.includes('application/vnd.ms-excel');
|
const contentType = String(headers['content-type'] || res.data?.type || '').toLowerCase();
|
||||||
|
const contentDisposition = String(headers['content-disposition'] || '').toLowerCase();
|
||||||
|
if (
|
||||||
|
contentType.includes('application/vnd.ms-excel') ||
|
||||||
|
contentType.includes('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 网关或对象存储可能将 Excel 流标记为二进制类型,此时通过下载文件名识别。
|
||||||
|
return (
|
||||||
|
contentType.includes('application/octet-stream') &&
|
||||||
|
/\.(xls|xlsx)(?:["';]|$)/.test(contentDisposition)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseBlobJson = async blob => {
|
const parseBlobJson = async blob => {
|
||||||
@@ -44,9 +56,10 @@ const decorateFailExcel = async (blob, businessName, failDetailDecorator) => {
|
|||||||
* @param {Function} [onSuccess] 导入成功回调
|
* @param {Function} [onSuccess] 导入成功回调
|
||||||
* @param {Object} [options] 扩展项
|
* @param {Object} [options] 扩展项
|
||||||
* @param {Function} [options.failDetailDecorator] 失败明细加工函数,用于对后端返回的失败 Excel 标红/改写原因
|
* @param {Function} [options.failDetailDecorator] 失败明细加工函数,用于对后端返回的失败 Excel 标红/改写原因
|
||||||
|
* @param {Number} [options.timeout=60000] 导入请求超时时间(毫秒)
|
||||||
*/
|
*/
|
||||||
export const openImportDialog = (vm, businessName, onSuccess, options = {}) => {
|
export const openImportDialog = (vm, businessName, onSuccess, options = {}) => {
|
||||||
const { failDetailDecorator } = options;
|
const { failDetailDecorator, timeout = 60000 } = options;
|
||||||
const excelOption = vm.excelOption || vm.excelOptionConfig;
|
const excelOption = vm.excelOption || vm.excelOptionConfig;
|
||||||
const column = vm.findColumn(excelOption?.column || [], 'excelFile');
|
const column = vm.findColumn(excelOption?.column || [], 'excelFile');
|
||||||
if (!column) {
|
if (!column) {
|
||||||
@@ -54,12 +67,15 @@ export const openImportDialog = (vm, businessName, onSuccess, options = {}) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
column.httpRequest = (option, uploadColumn) =>
|
column.httpRequest = (option, uploadColumn) =>
|
||||||
handleImportExcel(vm, option, uploadColumn, businessName, onSuccess, { failDetailDecorator });
|
handleImportExcel(vm, option, uploadColumn, businessName, onSuccess, {
|
||||||
|
failDetailDecorator,
|
||||||
|
timeout,
|
||||||
|
});
|
||||||
vm.excelBox = true;
|
vm.excelBox = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleImportExcel = async (vm, option, column, businessName, onSuccess, options = {}) => {
|
export const handleImportExcel = async (vm, option, column, businessName, onSuccess, options = {}) => {
|
||||||
const { failDetailDecorator } = options;
|
const { failDetailDecorator, timeout = 60000 } = options;
|
||||||
const file = option.file;
|
const file = option.file;
|
||||||
const fileName = file && file.name ? file.name.toLowerCase() : '';
|
const fileName = file && file.name ? file.name.toLowerCase() : '';
|
||||||
if (!excelPattern.test(fileName)) {
|
if (!excelPattern.test(fileName)) {
|
||||||
@@ -77,7 +93,7 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
|||||||
background: 'rgba(255, 255, 255, 0.7)',
|
background: 'rgba(255, 255, 255, 0.7)',
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const res = await importBlob(column.action, file);
|
const res = await importBlob(column.action, file, { timeout });
|
||||||
if (isExcelResponse(res)) {
|
if (isExcelResponse(res)) {
|
||||||
const decorated = await decorateFailExcel(res.data, businessName, failDetailDecorator);
|
const decorated = await decorateFailExcel(res.data, businessName, failDetailDecorator);
|
||||||
downloadXls(
|
downloadXls(
|
||||||
|
|||||||
@@ -55,6 +55,19 @@
|
|||||||
{{ row.createUserName || row.createUser || '' }}
|
{{ row.createUserName || row.createUser || '' }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #remark-form>
|
||||||
|
<el-input
|
||||||
|
v-model="form.remark"
|
||||||
|
class="remark-input"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
:disabled="dialogType === 'view'"
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #parentId-form>
|
<template #parentId-form>
|
||||||
<el-select
|
<el-select
|
||||||
v-model="form.parentId"
|
v-model="form.parentId"
|
||||||
@@ -644,3 +657,18 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
/* 货物类型新增/编辑弹窗:footer 不上移,避免覆盖备注文本域底部内容 */
|
||||||
|
.cargo-type-dialog.avue-dialog.avue-crud__dialog .avue-dialog__footer {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cargo-type-dialog.avue-dialog.avue-crud__dialog .el-dialog__body {
|
||||||
|
padding-bottom: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cargo-type-dialog .remark-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -232,7 +232,10 @@
|
|||||||
搜索
|
搜索
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div ref="amap" class="common-address-page__map"></div>
|
<div class="map-picker-content">
|
||||||
|
<map-search-results :results="mapSearchResults" @select="selectMapSearchResult" />
|
||||||
|
<div ref="amap" class="common-address-page__map"></div>
|
||||||
|
</div>
|
||||||
<div class="common-address-page__map-info">
|
<div class="common-address-page__map-info">
|
||||||
<span>{{ mapStatus }}</span>
|
<span>{{ mapStatus }}</span>
|
||||||
<span v-if="mapSelected.regionName">行政区划:{{ mapSelected.regionName }}</span>
|
<span v-if="mapSelected.regionName">行政区划:{{ mapSelected.regionName }}</span>
|
||||||
@@ -298,6 +301,7 @@ export default {
|
|||||||
mapKeyword: '',
|
mapKeyword: '',
|
||||||
mapStatus: '可搜索地址或点击地图选点',
|
mapStatus: '可搜索地址或点击地图选点',
|
||||||
mapSelected: {},
|
mapSelected: {},
|
||||||
|
mapSearchResults: [],
|
||||||
amap: null,
|
amap: null,
|
||||||
amapMarker: null,
|
amapMarker: null,
|
||||||
amapGeocoder: null,
|
amapGeocoder: null,
|
||||||
@@ -1113,6 +1117,12 @@ export default {
|
|||||||
this.ensureAmapGeocoder()
|
this.ensureAmapGeocoder()
|
||||||
.then(() => this.runAmapGeocode('location', keyword))
|
.then(() => this.runAmapGeocode('location', keyword))
|
||||||
.then(result => {
|
.then(result => {
|
||||||
|
this.mapSearchResults = (result.geocodes || []).map((item, index) => ({
|
||||||
|
id: item.id || index,
|
||||||
|
name: item.formattedAddress || keyword,
|
||||||
|
address: item.formattedAddress || keyword,
|
||||||
|
location: item.location,
|
||||||
|
}));
|
||||||
const point = this.resolveMapPoint(result);
|
const point = this.resolveMapPoint(result);
|
||||||
if (!point) {
|
if (!point) {
|
||||||
this.mapStatus = '未找到匹配地址';
|
this.mapStatus = '未找到匹配地址';
|
||||||
@@ -1133,6 +1143,11 @@ export default {
|
|||||||
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
|
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
selectMapSearchResult(item) {
|
||||||
|
if (item && item.location) {
|
||||||
|
this.pickMapPoint(item.location, item.address || item.name || '');
|
||||||
|
}
|
||||||
|
},
|
||||||
pickMapPoint(lnglat, keyword) {
|
pickMapPoint(lnglat, keyword) {
|
||||||
const longitude = this.getPointLng(lnglat);
|
const longitude = this.getPointLng(lnglat);
|
||||||
const latitude = this.getPointLat(lnglat);
|
const latitude = this.getPointLat(lnglat);
|
||||||
|
|||||||
+130
-25
@@ -30,12 +30,14 @@
|
|||||||
{{ row.status === 1 ? '启用' : '停用' }}
|
{{ row.status === 1 ? '启用' : '停用' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
|
<template #feeCategory="{ row }">
|
||||||
|
{{ formatFeeCategory(row.feeCategory) }}
|
||||||
|
</template>
|
||||||
<template #feeCategory-form>
|
<template #feeCategory-form>
|
||||||
<el-select
|
<el-select
|
||||||
v-model="form.feeCategory"
|
v-model="form.feeCategory"
|
||||||
class="fee-item-form-control"
|
class="fee-item-form-control"
|
||||||
clearable
|
clearable
|
||||||
:disabled="dialogType !== 'add'"
|
|
||||||
filterable
|
filterable
|
||||||
placeholder="请选择费用类型"
|
placeholder="请选择费用类型"
|
||||||
@change="handleFeeCategoryChange"
|
@change="handleFeeCategoryChange"
|
||||||
@@ -43,18 +45,30 @@
|
|||||||
<el-option
|
<el-option
|
||||||
v-for="item in feeCategoryOptions"
|
v-for="item in feeCategoryOptions"
|
||||||
:key="item.id || item.dictKey"
|
:key="item.id || item.dictKey"
|
||||||
:label="item.dictValue"
|
:label="`${item.dictValue}/${item.dictKey}`"
|
||||||
:value="item.dictKey"
|
:value="item.dictKey"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
|
<template #taxRate="{ row }">{{ formatTaxRate(row.taxRate) }}</template>
|
||||||
|
<template #taxRate-form>
|
||||||
|
<el-input
|
||||||
|
v-model="form.taxRate"
|
||||||
|
inputmode="numeric"
|
||||||
|
maxlength="3"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入税率"
|
||||||
|
@input="handleTaxRateInput"
|
||||||
|
>
|
||||||
|
<template #append>%</template>
|
||||||
|
</el-input>
|
||||||
|
</template>
|
||||||
<template #englishName-form>
|
<template #englishName-form>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.englishName"
|
v-model="form.englishName"
|
||||||
:maxlength="feeItemCodeMaxlength"
|
:maxlength="feeItemCodeMaxlength"
|
||||||
class="fee-item-form-control"
|
class="fee-item-form-control"
|
||||||
clearable
|
clearable
|
||||||
:disabled="dialogType !== 'add'"
|
|
||||||
placeholder="请输入费用项代码"
|
placeholder="请输入费用项代码"
|
||||||
@change="handleFeeItemCodeChange"
|
@change="handleFeeItemCodeChange"
|
||||||
>
|
>
|
||||||
@@ -80,7 +94,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/fee-item';
|
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/fee-item';
|
||||||
import { exportBlob } from '@/api/common';
|
import { exportBlob } from '@/api/common';
|
||||||
import { getDictionary } 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 { getToken } from '@/utils/auth';
|
import { getToken } from '@/utils/auth';
|
||||||
@@ -136,10 +150,11 @@ export default {
|
|||||||
searchOrder: 3,
|
searchOrder: 3,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
formslot: true,
|
formslot: true,
|
||||||
|
slot: true,
|
||||||
span: 12,
|
span: 12,
|
||||||
dicData: [],
|
dicData: [],
|
||||||
props: {
|
props: {
|
||||||
label: 'dictValue',
|
label: 'displayLabel',
|
||||||
value: 'dictKey',
|
value: 'dictKey',
|
||||||
},
|
},
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
@@ -154,6 +169,30 @@ export default {
|
|||||||
maxlength: 100,
|
maxlength: 100,
|
||||||
rules: [{ required: true, message: '请输入费用项代码', trigger: 'blur' }],
|
rules: [{ required: true, message: '请输入费用项代码', trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '税率',
|
||||||
|
prop: 'taxRate',
|
||||||
|
type: 'input',
|
||||||
|
formslot: true,
|
||||||
|
slot: true,
|
||||||
|
minWidth: 100,
|
||||||
|
span: 12,
|
||||||
|
rules: [
|
||||||
|
{ required: true, message: '请输入税率', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
const text = String(value ?? '').trim();
|
||||||
|
const rate = Number(text);
|
||||||
|
if (!/^\d+$/.test(text) || rate < 0 || rate > 100) {
|
||||||
|
callback(new Error('税率必须是0到100之间的整数'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '费用项',
|
label: '费用项',
|
||||||
prop: 'name',
|
prop: 'name',
|
||||||
@@ -170,7 +209,8 @@ export default {
|
|||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
minRows: 2,
|
minRows: 2,
|
||||||
span: 24,
|
span: 24,
|
||||||
hide: true,
|
minWidth: 200,
|
||||||
|
overHidden: true,
|
||||||
maxlength: 200,
|
maxlength: 200,
|
||||||
showWordLimit: true,
|
showWordLimit: true,
|
||||||
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
||||||
@@ -275,10 +315,17 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
initFeeCategoryOptions() {
|
initFeeCategoryOptions() {
|
||||||
getDictionary({ code: 'fee_category' }).then(res => {
|
getDictionaryAll().then(res => {
|
||||||
this.feeCategoryOptions = res.data.data || [];
|
const options = (res.data.data || [])
|
||||||
|
.filter(item => item.code === 'fee_category' && Number(item.parentId) > 0)
|
||||||
|
.sort((first, second) => Number(first.sort || 0) - Number(second.sort || 0))
|
||||||
|
.map(item => ({
|
||||||
|
...item,
|
||||||
|
displayLabel: `${item.dictValue}/${item.dictKey}`,
|
||||||
|
}));
|
||||||
|
this.feeCategoryOptions = options;
|
||||||
const column = this.findColumn(this.option.column, 'feeCategory');
|
const column = this.findColumn(this.option.column, 'feeCategory');
|
||||||
column.dicData = this.feeCategoryOptions;
|
column.dicData = options;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
initDeptTree() {
|
initDeptTree() {
|
||||||
@@ -307,6 +354,21 @@ export default {
|
|||||||
);
|
);
|
||||||
return String((option && option.dictKey) || feeCategory);
|
return String((option && option.dictKey) || feeCategory);
|
||||||
},
|
},
|
||||||
|
formatFeeCategory(value) {
|
||||||
|
const feeCategory = String(value || '').trim();
|
||||||
|
if (!feeCategory) return '';
|
||||||
|
const option = this.feeCategoryOptions.find(
|
||||||
|
item =>
|
||||||
|
String(item.dictKey || '') === feeCategory || String(item.dictValue || '') === feeCategory
|
||||||
|
);
|
||||||
|
return option ? `${option.dictValue}/${option.dictKey}` : feeCategory;
|
||||||
|
},
|
||||||
|
formatTaxRate(value) {
|
||||||
|
const text = String(value ?? '').trim();
|
||||||
|
if (text === '') return '';
|
||||||
|
const rate = Number(text);
|
||||||
|
return Number.isFinite(rate) ? `${Math.trunc(Math.abs(rate))}%` : `${text}%`;
|
||||||
|
},
|
||||||
getFeeItemCodeSuffix(code, feeCategory) {
|
getFeeItemCodeSuffix(code, feeCategory) {
|
||||||
const value = String(code || '').trim();
|
const value = String(code || '').trim();
|
||||||
const prefix = this.getFeeCategoryPrefix(feeCategory);
|
const prefix = this.getFeeCategoryPrefix(feeCategory);
|
||||||
@@ -328,6 +390,7 @@ export default {
|
|||||||
const code = this.getFeeItemCodeSuffix(values.englishName, values.feeCategory);
|
const code = this.getFeeItemCodeSuffix(values.englishName, values.feeCategory);
|
||||||
values.feeCategory = String(values.feeCategory || '').trim();
|
values.feeCategory = String(values.feeCategory || '').trim();
|
||||||
values.name = String(values.name || '').trim();
|
values.name = String(values.name || '').trim();
|
||||||
|
values.taxRate = Number(values.taxRate);
|
||||||
values.englishName = prefix ? `${prefix}${code}` : code;
|
values.englishName = prefix ? `${prefix}${code}` : code;
|
||||||
values.remark = String(values.remark || '').trim() || undefined;
|
values.remark = String(values.remark || '').trim() || undefined;
|
||||||
if (!values.status) values.status = 1;
|
if (!values.status) values.status = 1;
|
||||||
@@ -337,33 +400,70 @@ export default {
|
|||||||
const values = { ...row };
|
const values = { ...row };
|
||||||
values.feeCategory = String(values.feeCategory || '').trim();
|
values.feeCategory = String(values.feeCategory || '').trim();
|
||||||
values.englishName = this.getFeeItemCodeSuffix(values.englishName, values.feeCategory);
|
values.englishName = this.getFeeItemCodeSuffix(values.englishName, values.feeCategory);
|
||||||
|
const taxRateText = String(values.taxRate ?? '').trim();
|
||||||
|
const taxRate = Number(taxRateText);
|
||||||
|
if (taxRateText !== '' && Number.isFinite(taxRate)) {
|
||||||
|
values.taxRate = String(Math.trunc(Math.abs(taxRate)));
|
||||||
|
}
|
||||||
return values;
|
return values;
|
||||||
},
|
},
|
||||||
|
validateUnique(row) {
|
||||||
|
const rowId = String(row.id || '');
|
||||||
|
const hasDuplicate = (params, prop) => {
|
||||||
|
if (!row[prop]) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
return getList(1, 100, params).then(res => {
|
||||||
|
const records = res.data.data.records || [];
|
||||||
|
return records.some(
|
||||||
|
item =>
|
||||||
|
String(item[prop] || '').trim() === String(row[prop]).trim() &&
|
||||||
|
String(item.id || '') !== rowId
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return Promise.all([
|
||||||
|
hasDuplicate({ name: row.name }, 'name'),
|
||||||
|
hasDuplicate({ englishName: row.englishName }, 'englishName'),
|
||||||
|
]).then(([nameExists, codeExists]) => {
|
||||||
|
if (nameExists) {
|
||||||
|
return Promise.reject(new Error('该费用项已存在'));
|
||||||
|
}
|
||||||
|
if (codeExists) {
|
||||||
|
return Promise.reject(new Error('该费用项代码已存在'));
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSubmitError(error, loading) {
|
||||||
|
const uniqueMessages = ['该费用项已存在', '该费用项代码已存在'];
|
||||||
|
if (uniqueMessages.includes(error.message)) {
|
||||||
|
this.$message.warning(error.message);
|
||||||
|
}
|
||||||
|
window.console.log(error);
|
||||||
|
loading();
|
||||||
|
},
|
||||||
rowSave(row, done, loading) {
|
rowSave(row, done, loading) {
|
||||||
submit(this.normalizeRow(row)).then(
|
const submitRow = this.normalizeRow(row);
|
||||||
() => {
|
this.validateUnique(submitRow)
|
||||||
|
.then(() => submit(submitRow))
|
||||||
|
.then(() => {
|
||||||
this.onLoad(this.page);
|
this.onLoad(this.page);
|
||||||
this.$message({ type: 'success', message: '操作成功!' });
|
this.$message({ type: 'success', message: '操作成功!' });
|
||||||
done();
|
done();
|
||||||
},
|
})
|
||||||
error => {
|
.catch(error => this.handleSubmitError(error, loading));
|
||||||
window.console.log(error);
|
|
||||||
loading();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
rowUpdate(row, index, done, loading) {
|
rowUpdate(row, index, done, loading) {
|
||||||
submit(this.normalizeRow(row)).then(
|
const submitRow = this.normalizeRow(row);
|
||||||
() => {
|
this.validateUnique(submitRow)
|
||||||
|
.then(() => submit(submitRow))
|
||||||
|
.then(() => {
|
||||||
this.onLoad(this.page);
|
this.onLoad(this.page);
|
||||||
this.$message({ type: 'success', message: '操作成功!' });
|
this.$message({ type: 'success', message: '操作成功!' });
|
||||||
done();
|
done();
|
||||||
},
|
})
|
||||||
error => {
|
.catch(error => this.handleSubmitError(error, loading));
|
||||||
window.console.log(error);
|
|
||||||
loading();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
rowDel(row) {
|
rowDel(row) {
|
||||||
this.$confirm('确定将选择数据删除?', {
|
this.$confirm('确定将选择数据删除?', {
|
||||||
@@ -404,6 +504,11 @@ export default {
|
|||||||
this.form.englishName = '';
|
this.form.englishName = '';
|
||||||
done();
|
done();
|
||||||
},
|
},
|
||||||
|
handleTaxRateInput(value) {
|
||||||
|
let normalized = String(value ?? '').replace(/\D/g, '');
|
||||||
|
if (normalized.length > 1) normalized = normalized.replace(/^0+/, '') || '0';
|
||||||
|
this.form.taxRate = normalized;
|
||||||
|
},
|
||||||
searchReset() {
|
searchReset() {
|
||||||
this.query = {};
|
this.query = {};
|
||||||
this.onLoad(this.page);
|
this.onLoad(this.page);
|
||||||
|
|||||||
@@ -20,6 +20,16 @@
|
|||||||
批量删除
|
批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
<div class="insurance-ocr-template-page__search">
|
<div class="insurance-ocr-template-page__search">
|
||||||
|
<el-select
|
||||||
|
v-model="query.vehicleType"
|
||||||
|
clearable
|
||||||
|
placeholder="车船类型"
|
||||||
|
style="width: 140px"
|
||||||
|
@change="search"
|
||||||
|
>
|
||||||
|
<el-option label="车辆" value="车辆" />
|
||||||
|
<el-option label="船舶" value="船舶" />
|
||||||
|
</el-select>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="query.name"
|
v-model="query.name"
|
||||||
clearable
|
clearable
|
||||||
@@ -42,6 +52,7 @@
|
|||||||
<el-table-column type="selection" width="48" />
|
<el-table-column type="selection" width="48" />
|
||||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||||
<el-table-column prop="name" label="模板名称" min-width="220" show-overflow-tooltip />
|
<el-table-column prop="name" label="模板名称" min-width="220" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="vehicleType" label="车船类型" min-width="110" />
|
||||||
<el-table-column label="已配置字段" min-width="150">
|
<el-table-column label="已配置字段" min-width="150">
|
||||||
<template #default="{ row }">{{ getMappingCount(row.mappingConfig) }}</template>
|
<template #default="{ row }">{{ getMappingCount(row.mappingConfig) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -94,6 +105,12 @@
|
|||||||
<el-form-item label="模板名称" prop="name">
|
<el-form-item label="模板名称" prop="name">
|
||||||
<el-input v-model="form.name" maxlength="100" show-word-limit />
|
<el-input v-model="form.name" maxlength="100" show-word-limit />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="车船类型" prop="vehicleType">
|
||||||
|
<el-radio-group v-model="form.vehicleType">
|
||||||
|
<el-radio label="车辆">车辆</el-radio>
|
||||||
|
<el-radio label="船舶">船舶</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="字段映射" required>
|
<el-form-item label="字段映射" required>
|
||||||
<el-table :data="mappingRows" border class="insurance-ocr-template-page__mapping-table">
|
<el-table :data="mappingRows" border class="insurance-ocr-template-page__mapping-table">
|
||||||
<el-table-column prop="key" label="键名" width="180" align="center" />
|
<el-table-column prop="key" label="键名" width="180" align="center" />
|
||||||
@@ -165,8 +182,8 @@ export default {
|
|||||||
readonly: false,
|
readonly: false,
|
||||||
data: [],
|
data: [],
|
||||||
selectionList: [],
|
selectionList: [],
|
||||||
query: { name: '' },
|
query: { name: '', vehicleType: '' },
|
||||||
form: { id: '', name: '' },
|
form: { id: '', name: '', vehicleType: '车辆' },
|
||||||
mappingRows: createMappingRows(),
|
mappingRows: createMappingRows(),
|
||||||
page: {
|
page: {
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@@ -176,9 +193,13 @@ export default {
|
|||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
name: [{ required: true, message: '请输入模板名称', trigger: 'blur' }],
|
name: [{ required: true, message: '请输入模板名称', trigger: 'blur' }],
|
||||||
|
vehicleType: [{ required: true, message: '请选择车船类型', trigger: 'change' }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.onLoad();
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['permission', 'userInfo']),
|
...mapGetters(['permission', 'userInfo']),
|
||||||
isAdmin() {
|
isAdmin() {
|
||||||
@@ -198,7 +219,7 @@ export default {
|
|||||||
},
|
},
|
||||||
onLoad() {
|
onLoad() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
getList(this.page.currentPage, this.page.pageSize, this.query)
|
return getList(this.page.currentPage, this.page.pageSize, this.query)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const pageData = res.data.data || {};
|
const pageData = res.data.data || {};
|
||||||
this.data = pageData.records || [];
|
this.data = pageData.records || [];
|
||||||
@@ -215,6 +236,7 @@ export default {
|
|||||||
},
|
},
|
||||||
resetSearch() {
|
resetSearch() {
|
||||||
this.query.name = '';
|
this.query.name = '';
|
||||||
|
this.query.vehicleType = '';
|
||||||
this.search();
|
this.search();
|
||||||
},
|
},
|
||||||
currentChange(currentPage) {
|
currentChange(currentPage) {
|
||||||
@@ -232,18 +254,23 @@ export default {
|
|||||||
openDialog(row, readonly = false) {
|
openDialog(row, readonly = false) {
|
||||||
this.readonly = readonly;
|
this.readonly = readonly;
|
||||||
if (!row?.id) {
|
if (!row?.id) {
|
||||||
|
this.form = { id: '', name: '', vehicleType: '车辆' };
|
||||||
this.dialogVisible = true;
|
this.dialogVisible = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getDetail(row.id).then(res => {
|
getDetail(row.id).then(res => {
|
||||||
const detail = res.data.data || {};
|
const detail = res.data.data || {};
|
||||||
this.form = { id: detail.id, name: detail.name || '' };
|
this.form = {
|
||||||
|
id: detail.id,
|
||||||
|
name: detail.name || '',
|
||||||
|
vehicleType: detail.vehicleType || '车辆',
|
||||||
|
};
|
||||||
this.mappingRows = createMappingRows(detail.mappingConfig);
|
this.mappingRows = createMappingRows(detail.mappingConfig);
|
||||||
this.dialogVisible = true;
|
this.dialogVisible = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
resetForm() {
|
resetForm() {
|
||||||
this.form = { id: '', name: '' };
|
this.form = { id: '', name: '', vehicleType: '车辆' };
|
||||||
this.mappingRows = createMappingRows();
|
this.mappingRows = createMappingRows();
|
||||||
this.readonly = false;
|
this.readonly = false;
|
||||||
this.submitLoading = false;
|
this.submitLoading = false;
|
||||||
@@ -261,14 +288,15 @@ export default {
|
|||||||
submit({
|
submit({
|
||||||
id: this.form.id || undefined,
|
id: this.form.id || undefined,
|
||||||
name: String(this.form.name || '').trim(),
|
name: String(this.form.name || '').trim(),
|
||||||
|
vehicleType: this.form.vehicleType || '车辆',
|
||||||
mappingConfig: JSON.stringify(
|
mappingConfig: JSON.stringify(
|
||||||
this.mappingRows.map(item => ({ key: item.key, value: String(item.value || '').trim() }))
|
this.mappingRows.map(item => ({ key: item.key, value: String(item.value || '').trim() }))
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(async () => {
|
||||||
this.$message.success('操作成功');
|
this.$message.success('操作成功');
|
||||||
this.dialogVisible = false;
|
this.dialogVisible = false;
|
||||||
this.onLoad();
|
await this.onLoad();
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.submitLoading = false;
|
this.submitLoading = false;
|
||||||
@@ -308,7 +336,7 @@ export default {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
width: 380px;
|
width: 520px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__mapping-table {
|
&__mapping-table {
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
<template>
|
||||||
|
<basic-container class="measurement-unit-page">
|
||||||
|
<avue-crud
|
||||||
|
:option="option"
|
||||||
|
:table-loading="loading"
|
||||||
|
:data="data"
|
||||||
|
v-model:page="page"
|
||||||
|
v-model="form"
|
||||||
|
ref="crud"
|
||||||
|
:permission="permissionList"
|
||||||
|
:before-open="beforeOpen"
|
||||||
|
@row-save="rowSave"
|
||||||
|
@row-update="rowUpdate"
|
||||||
|
@search-change="searchChange"
|
||||||
|
@search-reset="searchReset"
|
||||||
|
@selection-change="selectionChange"
|
||||||
|
@current-change="currentChange"
|
||||||
|
@size-change="sizeChange"
|
||||||
|
@refresh-change="refreshChange"
|
||||||
|
@on-load="onLoad"
|
||||||
|
>
|
||||||
|
<template #menu-left>
|
||||||
|
<el-button
|
||||||
|
v-if="hasPermission('measurement_unit_add')"
|
||||||
|
type="primary"
|
||||||
|
@click="$refs.crud.rowAdd()"
|
||||||
|
>
|
||||||
|
新增
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="hasPermission('measurement_unit_delete')"
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
:disabled="!selectionList.length"
|
||||||
|
@click="handleDelete"
|
||||||
|
>
|
||||||
|
批量删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<template #dimension="{ row }">
|
||||||
|
{{ row.dimension || '-' }}
|
||||||
|
</template>
|
||||||
|
<template #status="{ row }">
|
||||||
|
<el-tag :type="row.status === 1 ? 'primary' : 'info'" class="status-text">
|
||||||
|
{{ row.status === 1 ? '启用' : '停用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
<template #menu="{ row, index }">
|
||||||
|
<el-link
|
||||||
|
v-if="hasPermission('measurement_unit_edit')"
|
||||||
|
type="primary"
|
||||||
|
@click="$refs.crud.rowEdit(row, index)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
v-if="hasPermission('measurement_unit_status')"
|
||||||
|
type="primary"
|
||||||
|
@click="handleStatus(row)"
|
||||||
|
>
|
||||||
|
{{ row.status === 1 ? '停用' : '启用' }}
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
v-if="hasPermission('measurement_unit_delete')"
|
||||||
|
type="danger"
|
||||||
|
@click="handleDelete(row.id)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</avue-crud>
|
||||||
|
<empty-pagination
|
||||||
|
:page="page"
|
||||||
|
@size-change="sizeChange"
|
||||||
|
@current-change="currentChange"
|
||||||
|
@load="onLoad(page, query)"
|
||||||
|
/>
|
||||||
|
</basic-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { changeStatus, getDetail, getList, remove, submit } from '@/api/base/measurement-unit';
|
||||||
|
import { createOption } from '@/option/base/measurement-unit';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: {},
|
||||||
|
query: {},
|
||||||
|
loading: true,
|
||||||
|
data: [],
|
||||||
|
page: {
|
||||||
|
pageSize: 10,
|
||||||
|
pageSizes: [10, 20, 50, 100],
|
||||||
|
currentPage: 1,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
selectionList: [],
|
||||||
|
option: createOption(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['permission', 'userInfo']),
|
||||||
|
permissionList() {
|
||||||
|
return {
|
||||||
|
addBtn: this.hasPermission('measurement_unit_add'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
ids() {
|
||||||
|
return this.selectionList.map(item => item.id).join(',');
|
||||||
|
},
|
||||||
|
isAdmin() {
|
||||||
|
return String(this.userInfo?.authority || '').includes('admin');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
hasPermission(code) {
|
||||||
|
return this.isAdmin || this.permission?.[code] === true;
|
||||||
|
},
|
||||||
|
normalizeRow(row) {
|
||||||
|
row.unitName = String(row.unitName || '').trim();
|
||||||
|
row.dimension = String(row.dimension || '').trim();
|
||||||
|
row.remark = String(row.remark || '').trim();
|
||||||
|
if (!row.status) row.status = 1;
|
||||||
|
return row;
|
||||||
|
},
|
||||||
|
rowSave(row, done, loading) {
|
||||||
|
submit(this.normalizeRow(row)).then(
|
||||||
|
() => {
|
||||||
|
this.onLoad(this.page, this.query);
|
||||||
|
this.$message.success('操作成功');
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
() => loading()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
rowUpdate(row, index, done, loading) {
|
||||||
|
submit(this.normalizeRow(row)).then(
|
||||||
|
() => {
|
||||||
|
this.onLoad(this.page, this.query);
|
||||||
|
this.$message.success('操作成功');
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
() => loading()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
handleDelete(ids = '') {
|
||||||
|
const targetIds = ids || this.ids;
|
||||||
|
if (!targetIds) {
|
||||||
|
this.$message.warning('请选择至少一条数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$confirm('确定删除所选数据,删除后不可恢复?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
.then(() => remove(targetIds))
|
||||||
|
.then(() => {
|
||||||
|
this.onLoad(this.page, this.query);
|
||||||
|
this.$message.success('操作成功');
|
||||||
|
this.selectionList = [];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleStatus(row) {
|
||||||
|
const nextStatus = row.status === 1 ? 2 : 1;
|
||||||
|
const actionName = nextStatus === 1 ? '启用' : '停用';
|
||||||
|
this.$confirm(`是否确认${actionName}该计量单位?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
.then(() => changeStatus(row.id, nextStatus))
|
||||||
|
.then(() => {
|
||||||
|
this.onLoad(this.page, this.query);
|
||||||
|
this.$message.success('操作成功');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeOpen(done, type) {
|
||||||
|
if (['edit', 'view'].includes(type)) {
|
||||||
|
getDetail(this.form.id).then(res => {
|
||||||
|
this.form = res.data.data || {};
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.form.status = 1;
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
searchReset() {
|
||||||
|
this.query = {};
|
||||||
|
this.page.currentPage = 1;
|
||||||
|
this.onLoad(this.page, this.query);
|
||||||
|
},
|
||||||
|
searchChange(params, done) {
|
||||||
|
this.query = { ...params };
|
||||||
|
this.page.currentPage = 1;
|
||||||
|
this.onLoad(this.page, this.query);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
selectionChange(list) {
|
||||||
|
this.selectionList = list;
|
||||||
|
},
|
||||||
|
currentChange(currentPage) {
|
||||||
|
this.page.currentPage = currentPage;
|
||||||
|
this.onLoad(this.page, this.query);
|
||||||
|
},
|
||||||
|
sizeChange(pageSize) {
|
||||||
|
this.page.pageSize = pageSize;
|
||||||
|
this.page.currentPage = 1;
|
||||||
|
this.onLoad(this.page, this.query);
|
||||||
|
},
|
||||||
|
refreshChange() {
|
||||||
|
this.onLoad(this.page, this.query);
|
||||||
|
},
|
||||||
|
onLoad(page = this.page, params = this.query) {
|
||||||
|
this.loading = true;
|
||||||
|
return getList(page.currentPage, page.pageSize, params)
|
||||||
|
.then(res => {
|
||||||
|
const pageData = res.data.data || {};
|
||||||
|
this.data = pageData.records || [];
|
||||||
|
this.page.total = pageData.total || 0;
|
||||||
|
this.selectionList = [];
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.measurement-unit-page {
|
||||||
|
:deep(.avue-crud__menu) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.avue-crud__search .el-form-item__label) {
|
||||||
|
min-width: 160px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.avue-crud__menu-column) {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.avue-crud__menu .el-link) {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -93,6 +93,24 @@
|
|||||||
<el-radio label="码头">码头</el-radio>
|
<el-radio label="码头">码头</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</template>
|
</template>
|
||||||
|
<template #provinceCode-form>
|
||||||
|
<el-select
|
||||||
|
v-model="form.provinceCode"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
:disabled="isProvinceDisabled"
|
||||||
|
placeholder="请选择"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="handleProvinceChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in provinceOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.title"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
<template #code-form>
|
<template #code-form>
|
||||||
<el-input
|
<el-input
|
||||||
v-if="form.category !== '码头'"
|
v-if="form.category !== '码头'"
|
||||||
@@ -110,7 +128,9 @@
|
|||||||
maxlength="24"
|
maxlength="24"
|
||||||
placeholder="请输入码头标识"
|
placeholder="请输入码头标识"
|
||||||
@input="handleTerminalCodeChange"
|
@input="handleTerminalCodeChange"
|
||||||
/>
|
>
|
||||||
|
<template v-if="form.parentCode" #prepend>{{ form.parentCode }}</template>
|
||||||
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #menu="{ row, index }">
|
<template #menu="{ row, index }">
|
||||||
@@ -298,6 +318,8 @@ export default {
|
|||||||
excelForm: {},
|
excelForm: {},
|
||||||
portOptions: [],
|
portOptions: [],
|
||||||
countryOptions: [],
|
countryOptions: [],
|
||||||
|
provinceOptions: [],
|
||||||
|
provinceOptionCache: {},
|
||||||
cityOptions: [],
|
cityOptions: [],
|
||||||
cityLoading: false,
|
cityLoading: false,
|
||||||
cityOptionCache: {},
|
cityOptionCache: {},
|
||||||
@@ -434,13 +456,30 @@ export default {
|
|||||||
change: ({ value }) => this.handleCountryNameChange(value),
|
change: ({ value }) => this.handleCountryNameChange(value),
|
||||||
rules: [{ required: true, message: '请选择国家', trigger: 'change' }],
|
rules: [{ required: true, message: '请选择国家', trigger: 'change' }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '所属省份',
|
||||||
|
prop: 'provinceCode',
|
||||||
|
formslot: true,
|
||||||
|
hide: true,
|
||||||
|
span: 12,
|
||||||
|
order: 88,
|
||||||
|
placeholder: '请选择',
|
||||||
|
rules: [{ required: true, message: '请选择所属省份', trigger: 'change' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '所属省份',
|
||||||
|
prop: 'provinceName',
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '城市',
|
label: '城市',
|
||||||
prop: 'cityCode',
|
prop: 'cityCode',
|
||||||
formslot: true,
|
formslot: true,
|
||||||
hide: true,
|
hide: true,
|
||||||
span: 12,
|
span: 12,
|
||||||
order: 88,
|
order: 87,
|
||||||
placeholder: '请选择',
|
placeholder: '请选择',
|
||||||
rules: [{ required: true, message: '请选择所属城市', trigger: 'change' }],
|
rules: [{ required: true, message: '请选择所属城市', trigger: 'change' }],
|
||||||
},
|
},
|
||||||
@@ -463,7 +502,7 @@ export default {
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
formslot: true,
|
formslot: true,
|
||||||
hide: true,
|
hide: true,
|
||||||
order: 87,
|
order: 86,
|
||||||
props: {
|
props: {
|
||||||
label: 'title',
|
label: 'title',
|
||||||
value: 'id',
|
value: 'id',
|
||||||
@@ -532,7 +571,7 @@ export default {
|
|||||||
span: 12,
|
span: 12,
|
||||||
minWidth: 220,
|
minWidth: 220,
|
||||||
overHidden: false,
|
overHidden: false,
|
||||||
order: 86,
|
order: 85,
|
||||||
placeholder: '请输入',
|
placeholder: '请输入',
|
||||||
maxlength: 255,
|
maxlength: 255,
|
||||||
showWordLimit: true,
|
showWordLimit: true,
|
||||||
@@ -553,9 +592,8 @@ export default {
|
|||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
dicData: [
|
dicData: [
|
||||||
{ label: '全部', value: '' },
|
{ label: '全部', value: '' },
|
||||||
{ label: '初始化录入', value: '初始化录入' },
|
|
||||||
{ label: '批量导入', value: '批量导入' },
|
{ label: '批量导入', value: '批量导入' },
|
||||||
{ label: '手动录入', value: '手动录入' },
|
{ label: '手动输入', value: '手动录入' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -666,6 +704,9 @@ export default {
|
|||||||
return this.form.category === '码头';
|
return this.form.category === '码头';
|
||||||
},
|
},
|
||||||
isCityDisabled() {
|
isCityDisabled() {
|
||||||
|
return this.isParentRegionLocked || !this.form.provinceCode;
|
||||||
|
},
|
||||||
|
isProvinceDisabled() {
|
||||||
return this.isParentRegionLocked || !this.form.countryCode;
|
return this.isParentRegionLocked || !this.form.countryCode;
|
||||||
},
|
},
|
||||||
isDistrictDisabled() {
|
isDistrictDisabled() {
|
||||||
@@ -754,7 +795,7 @@ export default {
|
|||||||
this.form.parentName = '';
|
this.form.parentName = '';
|
||||||
this.form.countryCode = '';
|
this.form.countryCode = '';
|
||||||
this.form.country = '';
|
this.form.country = '';
|
||||||
this.clearCityValue();
|
this.clearProvinceValue();
|
||||||
},
|
},
|
||||||
handleCategoryChange(value, reset = true) {
|
handleCategoryChange(value, reset = true) {
|
||||||
const required = value === '码头';
|
const required = value === '码头';
|
||||||
@@ -778,13 +819,15 @@ export default {
|
|||||||
handleCountryChange(value) {
|
handleCountryChange(value) {
|
||||||
const country = this.countryOptions.find(item => item.id === value);
|
const country = this.countryOptions.find(item => item.id === value);
|
||||||
this.form.country = country ? country.title : '';
|
this.form.country = country ? country.title : '';
|
||||||
this.clearCityValue();
|
this.clearProvinceValue();
|
||||||
this.loadCityOptions(value);
|
this.loadProvinceOptions(value);
|
||||||
},
|
},
|
||||||
handleCountryNameChange(value) {
|
handleCountryNameChange(value) {
|
||||||
const country = this.countryOptions.find(item => item.title === value);
|
const country = this.countryOptions.find(item => item.title === value);
|
||||||
|
this.setProvinceOptions([]);
|
||||||
|
this.setCityOptions([]);
|
||||||
this.setDistrictOptions([]);
|
this.setDistrictOptions([]);
|
||||||
this.loadCityOptions(country ? country.id : '');
|
this.loadProvinceOptions(country ? country.id : '');
|
||||||
},
|
},
|
||||||
syncCountryName(countryCode) {
|
syncCountryName(countryCode) {
|
||||||
const country = this.countryOptions.find(item => item.id === countryCode);
|
const country = this.countryOptions.find(item => item.id === countryCode);
|
||||||
@@ -817,6 +860,18 @@ export default {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setProvinceOptions(provinceOptions) {
|
||||||
|
this.provinceOptions = provinceOptions;
|
||||||
|
const provinceCodeColumn = this.findColumn(this.option.column, 'provinceCode');
|
||||||
|
if (provinceCodeColumn) {
|
||||||
|
provinceCodeColumn.dicData = provinceOptions;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearProvinceValue() {
|
||||||
|
this.form.provinceCode = undefined;
|
||||||
|
this.form.provinceName = '';
|
||||||
|
this.clearCityValue();
|
||||||
|
},
|
||||||
clearCityValue() {
|
clearCityValue() {
|
||||||
this.form.cityCode = undefined;
|
this.form.cityCode = undefined;
|
||||||
this.form.city = '';
|
this.form.city = '';
|
||||||
@@ -827,36 +882,38 @@ export default {
|
|||||||
this.form.districtName = '';
|
this.form.districtName = '';
|
||||||
this.form.regionCode = '';
|
this.form.regionCode = '';
|
||||||
},
|
},
|
||||||
loadCityOptions(countryCode) {
|
loadProvinceOptions(countryCode) {
|
||||||
if (!countryCode) {
|
if (!countryCode) {
|
||||||
|
this.setProvinceOptions([]);
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
if (this.provinceOptionCache[countryCode]) {
|
||||||
|
this.setProvinceOptions(this.provinceOptionCache[countryCode]);
|
||||||
|
return Promise.resolve(this.provinceOptionCache[countryCode]);
|
||||||
|
}
|
||||||
|
return getLazyTree(countryCode).then(res => {
|
||||||
|
const options = (res.data.data || []).map(this.normalizeRegionOption);
|
||||||
|
this.provinceOptionCache[countryCode] = options;
|
||||||
|
this.setProvinceOptions(options);
|
||||||
|
return options;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
loadCityOptions(provinceCode) {
|
||||||
|
if (!provinceCode) {
|
||||||
this.setCityOptions([]);
|
this.setCityOptions([]);
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
if (this.cityOptionCache[countryCode]) {
|
if (this.cityOptionCache[provinceCode]) {
|
||||||
this.setCityOptions(this.cityOptionCache[countryCode]);
|
this.setCityOptions(this.cityOptionCache[provinceCode]);
|
||||||
return Promise.resolve(this.cityOptionCache[countryCode]);
|
return Promise.resolve(this.cityOptionCache[provinceCode]);
|
||||||
}
|
}
|
||||||
this.cityLoading = true;
|
this.cityLoading = true;
|
||||||
return getLazyTree(countryCode)
|
return getLazyTree(provinceCode)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const firstLevelList = (res.data.data || []).map(this.normalizeRegionOption);
|
const options = (res.data.data || []).map(this.normalizeRegionOption);
|
||||||
return Promise.all(
|
this.cityOptionCache[provinceCode] = options;
|
||||||
firstLevelList.map(parent => {
|
this.setCityOptions(options);
|
||||||
return getLazyTree(parent.id).then(childRes => {
|
return options;
|
||||||
const childList = (childRes.data.data || []).map(this.normalizeRegionOption);
|
|
||||||
return childList.map(child => ({
|
|
||||||
...child,
|
|
||||||
parentCode: child.parentCode || parent.id,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
})
|
|
||||||
).then(cityGroups => {
|
|
||||||
const cityList = cityGroups.flat();
|
|
||||||
const options = cityList.length > 0 ? cityList : firstLevelList;
|
|
||||||
this.cityOptionCache[countryCode] = options;
|
|
||||||
this.setCityOptions(options);
|
|
||||||
return options;
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.cityLoading = false;
|
this.cityLoading = false;
|
||||||
@@ -866,6 +923,12 @@ export default {
|
|||||||
const city = this.cityOptions.find(item => item.title === cityName || item.id === regionCode);
|
const city = this.cityOptions.find(item => item.title === cityName || item.id === regionCode);
|
||||||
return city ? city.id : '';
|
return city ? city.id : '';
|
||||||
},
|
},
|
||||||
|
resolveProvinceCode(provinceName) {
|
||||||
|
const province = this.provinceOptions.find(
|
||||||
|
item => item.title === provinceName || item.id === provinceName
|
||||||
|
);
|
||||||
|
return province ? province.id : '';
|
||||||
|
},
|
||||||
setDistrictOptions(districtOptions) {
|
setDistrictOptions(districtOptions) {
|
||||||
this.districtOptions = districtOptions;
|
this.districtOptions = districtOptions;
|
||||||
const districtCodeColumn = this.findColumn(this.option.column, 'districtCode');
|
const districtCodeColumn = this.findColumn(this.option.column, 'districtCode');
|
||||||
@@ -926,6 +989,8 @@ export default {
|
|||||||
this.form.terminalCode = terminalCode;
|
this.form.terminalCode = terminalCode;
|
||||||
this.handleTerminalCodeChange(terminalCode);
|
this.handleTerminalCodeChange(terminalCode);
|
||||||
this.form.country = parent.country;
|
this.form.country = parent.country;
|
||||||
|
this.form.provinceCode = parent.provinceCode;
|
||||||
|
this.form.provinceName = parent.provinceName;
|
||||||
this.form.city = parent.city;
|
this.form.city = parent.city;
|
||||||
this.form.districtCode = parent.districtCode;
|
this.form.districtCode = parent.districtCode;
|
||||||
this.form.districtName = parent.districtName;
|
this.form.districtName = parent.districtName;
|
||||||
@@ -934,7 +999,12 @@ export default {
|
|||||||
.then(countryCode => {
|
.then(countryCode => {
|
||||||
this.form.countryCode = countryCode;
|
this.form.countryCode = countryCode;
|
||||||
this.syncCountryName(countryCode);
|
this.syncCountryName(countryCode);
|
||||||
return this.loadCityOptions(countryCode);
|
return this.loadProvinceOptions(countryCode);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.form.provinceCode =
|
||||||
|
String(parent.provinceCode || '') || this.resolveProvinceCode(parent.provinceName);
|
||||||
|
return this.loadCityOptions(this.form.provinceCode);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.form.cityCode = this.resolveCityCode(parent.city, parent.regionCode);
|
this.form.cityCode = this.resolveCityCode(parent.city, parent.regionCode);
|
||||||
@@ -951,6 +1021,12 @@ export default {
|
|||||||
this.clearDistrictValue();
|
this.clearDistrictValue();
|
||||||
this.loadDistrictOptions(value);
|
this.loadDistrictOptions(value);
|
||||||
},
|
},
|
||||||
|
handleProvinceChange(value) {
|
||||||
|
const province = this.provinceOptions.find(item => item.id === value);
|
||||||
|
this.form.provinceName = province ? province.title : '';
|
||||||
|
this.clearCityValue();
|
||||||
|
this.loadCityOptions(value);
|
||||||
|
},
|
||||||
handleCityNameChange(value) {
|
handleCityNameChange(value) {
|
||||||
const city = this.cityOptions.find(item => item.title === value);
|
const city = this.cityOptions.find(item => item.title === value);
|
||||||
this.setDistrictOptions([]);
|
this.setDistrictOptions([]);
|
||||||
@@ -987,6 +1063,8 @@ export default {
|
|||||||
submitRow.regionCode = String(submitRow.regionCode || '').trim();
|
submitRow.regionCode = String(submitRow.regionCode || '').trim();
|
||||||
submitRow.districtCode = String(submitRow.districtCode || '').trim();
|
submitRow.districtCode = String(submitRow.districtCode || '').trim();
|
||||||
submitRow.districtName = String(submitRow.districtName || '').trim();
|
submitRow.districtName = String(submitRow.districtName || '').trim();
|
||||||
|
submitRow.provinceCode = String(submitRow.provinceCode || '').trim();
|
||||||
|
submitRow.provinceName = String(submitRow.provinceName || '').trim();
|
||||||
submitRow.detailAddress = String(submitRow.detailAddress || '').trim();
|
submitRow.detailAddress = String(submitRow.detailAddress || '').trim();
|
||||||
submitRow.longitude = normalizeCoordinateInput(submitRow.longitude);
|
submitRow.longitude = normalizeCoordinateInput(submitRow.longitude);
|
||||||
submitRow.latitude = normalizeCoordinateInput(submitRow.latitude);
|
submitRow.latitude = normalizeCoordinateInput(submitRow.latitude);
|
||||||
@@ -1020,6 +1098,10 @@ export default {
|
|||||||
this.$message.warning('请选择上级港口');
|
this.$message.warning('请选择上级港口');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (isBlank(row.provinceCode)) {
|
||||||
|
this.$message.warning('请选择所属省份');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (isBlank(row.regionCode)) {
|
if (isBlank(row.regionCode)) {
|
||||||
this.$message.warning('请选择区县');
|
this.$message.warning('请选择区县');
|
||||||
return false;
|
return false;
|
||||||
@@ -1152,7 +1234,13 @@ export default {
|
|||||||
.then(countryCode => {
|
.then(countryCode => {
|
||||||
this.form.countryCode = countryCode;
|
this.form.countryCode = countryCode;
|
||||||
this.syncCountryName(countryCode);
|
this.syncCountryName(countryCode);
|
||||||
return this.loadCityOptions(countryCode);
|
return this.loadProvinceOptions(countryCode);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.form.provinceCode =
|
||||||
|
String(this.form.provinceCode || '') ||
|
||||||
|
this.resolveProvinceCode(this.form.provinceName);
|
||||||
|
return this.loadCityOptions(this.form.provinceCode);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.form.cityCode = this.resolveCityCode(this.form.city, this.form.regionCode);
|
this.form.cityCode = this.resolveCityCode(this.form.city, this.form.regionCode);
|
||||||
@@ -1178,7 +1266,13 @@ export default {
|
|||||||
.then(countryCode => {
|
.then(countryCode => {
|
||||||
this.form.countryCode = countryCode;
|
this.form.countryCode = countryCode;
|
||||||
this.syncCountryName(countryCode);
|
this.syncCountryName(countryCode);
|
||||||
return this.loadCityOptions(countryCode);
|
return this.loadProvinceOptions(countryCode);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.form.provinceCode =
|
||||||
|
String(this.form.provinceCode || '') ||
|
||||||
|
this.resolveProvinceCode(this.form.provinceName);
|
||||||
|
return this.loadCityOptions(this.form.provinceCode);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.form.cityCode = this.resolveCityCode(this.form.city, this.form.regionCode);
|
this.form.cityCode = this.resolveCityCode(this.form.city, this.form.regionCode);
|
||||||
@@ -1210,6 +1304,7 @@ export default {
|
|||||||
},
|
},
|
||||||
searchReset() {
|
searchReset() {
|
||||||
this.query = {};
|
this.query = {};
|
||||||
|
this.setProvinceOptions([]);
|
||||||
this.setCityOptions([]);
|
this.setCityOptions([]);
|
||||||
this.setDistrictOptions([]);
|
this.setDistrictOptions([]);
|
||||||
this.onLoad(this.page);
|
this.onLoad(this.page);
|
||||||
@@ -1250,14 +1345,10 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleImport() {
|
handleImport() {
|
||||||
openImportDialog(
|
openImportDialog(this, '港口码头主数据', () => {
|
||||||
this,
|
this.loadPortOptions();
|
||||||
'港口码头主数据',
|
this.onLoad(this.page, this.query);
|
||||||
() => {
|
});
|
||||||
this.loadPortOptions();
|
|
||||||
this.onLoad(this.page, this.query);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
handleExport() {
|
handleExport() {
|
||||||
this.$confirm('是否导出港口码头主数据?', '提示', {
|
this.$confirm('是否导出港口码头主数据?', '提示', {
|
||||||
@@ -1270,7 +1361,10 @@ export default {
|
|||||||
feedback: true,
|
feedback: true,
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
downloadXls(res.data, `港口码头主数据${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
|
downloadXls(
|
||||||
|
res.data,
|
||||||
|
`港口码头主数据${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export default {
|
|||||||
};
|
};
|
||||||
const validateTelegraphCode = (rule, value, callback) => {
|
const validateTelegraphCode = (rule, value, callback) => {
|
||||||
if (!/^[A-Z]{3}$/.test(String(value || '').toUpperCase())) {
|
if (!/^[A-Z]{3}$/.test(String(value || '').toUpperCase())) {
|
||||||
callback(new Error('电报码格式不正确或已存在'));
|
callback(new Error('电报略码格式不正确或已存在'));
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
@@ -246,12 +246,12 @@ export default {
|
|||||||
change: ({ value }) => this.handleTmisChange(value),
|
change: ({ value }) => this.handleTmisChange(value),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '电报码',
|
label: '电报略码',
|
||||||
prop: 'telegraphCode',
|
prop: 'telegraphCode',
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
maxlength: 3,
|
maxlength: 3,
|
||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: '电报码格式不正确或已存在', trigger: 'blur' },
|
{ required: true, message: '电报略码格式不正确或已存在', trigger: 'blur' },
|
||||||
{ validator: validateTelegraphCode, trigger: 'blur' },
|
{ validator: validateTelegraphCode, trigger: 'blur' },
|
||||||
],
|
],
|
||||||
change: ({ value }) => this.handleTelegraphChange(value),
|
change: ({ value }) => this.handleTelegraphChange(value),
|
||||||
@@ -358,7 +358,10 @@ export default {
|
|||||||
formslot: true,
|
formslot: true,
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
overHidden: false,
|
overHidden: false,
|
||||||
rules: [{ validator: validateLongitude, trigger: 'blur' }],
|
rules: [
|
||||||
|
{ required: true, message: '请输入经度', trigger: 'blur' },
|
||||||
|
{ validator: validateLongitude, trigger: 'blur' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '纬度',
|
label: '纬度',
|
||||||
@@ -366,7 +369,10 @@ export default {
|
|||||||
formslot: true,
|
formslot: true,
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
overHidden: false,
|
overHidden: false,
|
||||||
rules: [{ validator: validateLatitude, trigger: 'blur' }],
|
rules: [
|
||||||
|
{ required: true, message: '请输入纬度', trigger: 'blur' },
|
||||||
|
{ validator: validateLatitude, trigger: 'blur' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '行政区划编号',
|
label: '行政区划编号',
|
||||||
@@ -448,9 +454,8 @@ export default {
|
|||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
dicData: [
|
dicData: [
|
||||||
{ label: '全部', value: '' },
|
{ label: '全部', value: '' },
|
||||||
{ label: '初始化导入', value: '初始化导入' },
|
{ label: '批量导入', value: '批量' },
|
||||||
{ label: '批量', value: '批量' },
|
{ label: '手动导入', value: '手动' },
|
||||||
{ label: '手动', value: '手动' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -661,14 +666,14 @@ export default {
|
|||||||
return row;
|
return row;
|
||||||
},
|
},
|
||||||
validateCoordinateFields(row) {
|
validateCoordinateFields(row) {
|
||||||
const longitudeMessage = getCoordinateValidationMessage(row.longitude, 'longitude');
|
const longitudeMessage = getCoordinateValidationMessage(row.longitude, 'longitude', true);
|
||||||
if (longitudeMessage) {
|
if (longitudeMessage) {
|
||||||
this.$message.warning('经度范围不正确');
|
this.$message.warning(longitudeMessage);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const latitudeMessage = getCoordinateValidationMessage(row.latitude, 'latitude');
|
const latitudeMessage = getCoordinateValidationMessage(row.latitude, 'latitude', true);
|
||||||
if (latitudeMessage) {
|
if (latitudeMessage) {
|
||||||
this.$message.warning('纬度范围不正确');
|
this.$message.warning(latitudeMessage);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -689,13 +694,13 @@ export default {
|
|||||||
return Promise.reject(new Error('该TMIS编码已存在'));
|
return Promise.reject(new Error('该TMIS编码已存在'));
|
||||||
}
|
}
|
||||||
if (telegraphExists) {
|
if (telegraphExists) {
|
||||||
return Promise.reject(new Error('电报码格式不正确或已存在'));
|
return Promise.reject(new Error('电报略码格式不正确或已存在'));
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleSubmitError(error, loading) {
|
handleSubmitError(error, loading) {
|
||||||
const uniqueMessages = ['该TMIS编码已存在', '电报码格式不正确或已存在'];
|
const uniqueMessages = ['该TMIS编码已存在', '电报略码格式不正确或已存在'];
|
||||||
if (uniqueMessages.includes(error.message)) {
|
if (uniqueMessages.includes(error.message)) {
|
||||||
this.$message.warning(error.message);
|
this.$message.warning(error.message);
|
||||||
}
|
}
|
||||||
@@ -776,10 +781,18 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
beforeOpen(done, type) {
|
beforeOpen(done, type) {
|
||||||
|
const tmisCodeColumn = this.findColumn('tmisCode');
|
||||||
|
if (tmisCodeColumn) {
|
||||||
|
// Avue 会复用列配置,打开不同记录前必须先清除上一次编辑状态。
|
||||||
|
tmisCodeColumn.disabled = false;
|
||||||
|
}
|
||||||
if (['edit', 'view'].includes(type)) {
|
if (['edit', 'view'].includes(type)) {
|
||||||
this.regionInitializing = true;
|
this.regionInitializing = true;
|
||||||
getDetail(this.form.id).then(res => {
|
getDetail(this.form.id).then(res => {
|
||||||
const detail = this.normalizeRegionForm(res.data.data || {});
|
const detail = this.normalizeRegionForm(res.data.data || {});
|
||||||
|
if (tmisCodeColumn) {
|
||||||
|
tmisCodeColumn.disabled = type === 'edit' && Number(detail.status) === 1;
|
||||||
|
}
|
||||||
this.loadCityOptions(detail.provinceCode)
|
this.loadCityOptions(detail.provinceCode)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.ensureCityOption(detail);
|
this.ensureCityOption(detail);
|
||||||
@@ -845,7 +858,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleImport() {
|
handleImport() {
|
||||||
openImportDialog(this, '铁路车站主数据');
|
openImportDialog(this, '铁路车站主数据', () => this.onLoad(this.page, this.query));
|
||||||
},
|
},
|
||||||
handleExport() {
|
handleExport() {
|
||||||
this.$confirm('是否导出铁路车站主数据?', '提示', {
|
this.$confirm('是否导出铁路车站主数据?', '提示', {
|
||||||
|
|||||||
+90
-64
@@ -4,52 +4,44 @@
|
|||||||
<div class="box">
|
<div class="box">
|
||||||
<el-scrollbar>
|
<el-scrollbar>
|
||||||
<basic-container>
|
<basic-container>
|
||||||
<avue-tree :option="treeOption" :data="treeData" @node-click="nodeClick" />
|
<avue-tree
|
||||||
|
:option="treeOption"
|
||||||
|
:data="treeData"
|
||||||
|
@node-click="nodeClick"
|
||||||
|
/>
|
||||||
</basic-container>
|
</basic-container>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col class="region-page__form-panel" :span="18">
|
<el-col class="region-page__form-panel" :span="18">
|
||||||
<basic-container>
|
<basic-container>
|
||||||
<el-form class="region-page__search" :model="searchForm" inline>
|
<section-card>
|
||||||
<el-form-item label="区域编码">
|
<el-form class="region-page__search" :model="searchForm" inline>
|
||||||
<el-input v-model="searchForm.code" clearable placeholder="请输入区域编码" />
|
<div style="display: flex; justify-content: space-between">
|
||||||
</el-form-item>
|
<el-form-item label="区域编码">
|
||||||
<el-form-item label="区域名称">
|
<el-input v-model="searchForm.code" clearable placeholder="请输入区域编码" />
|
||||||
<el-input v-model="searchForm.name" clearable placeholder="请输入区域名称" />
|
</el-form-item>
|
||||||
</el-form-item>
|
<el-form-item label="区域名称">
|
||||||
<el-form-item label="区域层级">
|
<el-input v-model="searchForm.name" clearable placeholder="请输入区域名称" />
|
||||||
<el-select v-model="searchForm.regionLevel" clearable placeholder="请选择区域层级">
|
</el-form-item>
|
||||||
<el-option label="省份" :value="1" />
|
<el-form-item label="区域层级">
|
||||||
<el-option label="省市" :value="2" />
|
<el-select v-model="searchForm.regionLevel" clearable placeholder="请选择" style="width: 200px;">
|
||||||
<el-option label="区县" :value="3" />
|
<el-option label="省份" :value="1" />
|
||||||
</el-select>
|
<el-option label="省市" :value="2" />
|
||||||
</el-form-item>
|
<el-option label="区县" :value="3" />
|
||||||
<el-form-item label="数据来源">
|
</el-select>
|
||||||
<el-select v-model="searchForm.dataSource" clearable placeholder="请选择数据来源">
|
</el-form-item>
|
||||||
<el-option label="手动录入" value="手动录入" />
|
</div>
|
||||||
<el-option label="初始化导入" value="初始化导入" />
|
<div style="display: flex; flex-direction: row; align-items: center; justify-content: flex-end;">
|
||||||
</el-select>
|
<el-form-item class="region-page__search-actions">
|
||||||
</el-form-item>
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
<el-form-item label="启停状态">
|
<el-button @click="handleSearchReset">清空</el-button>
|
||||||
<el-select v-model="searchForm.status" clearable placeholder="请选择启停状态">
|
</el-form-item>
|
||||||
<el-option label="启用" :value="1" />
|
</div>
|
||||||
<el-option label="停用" :value="2" />
|
</el-form>
|
||||||
</el-select>
|
</section-card>
|
||||||
</el-form-item>
|
|
||||||
<el-form-item class="region-page__search-actions">
|
|
||||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
|
||||||
<el-button @click="handleSearchReset">清空</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<el-button-group>
|
<el-button-group>
|
||||||
<el-button
|
|
||||||
v-if="permission.region_add"
|
|
||||||
type="primary"
|
|
||||||
icon="el-icon-plus"
|
|
||||||
@click="addCountry"
|
|
||||||
>新增国家
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
<el-button
|
||||||
v-if="permission.region_add"
|
v-if="permission.region_add"
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -157,10 +149,12 @@ import { openImportDialog } from '@/utils/import-excel';
|
|||||||
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';
|
||||||
|
import SectionCard from '@/components/section-card/main.vue';
|
||||||
|
|
||||||
const ROOT_PARENT_CODE = '0';
|
const ROOT_PARENT_CODE = '0';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: { SectionCard },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
topCode: ROOT_PARENT_CODE,
|
topCode: ROOT_PARENT_CODE,
|
||||||
@@ -171,8 +165,6 @@ export default {
|
|||||||
code: '',
|
code: '',
|
||||||
name: '',
|
name: '',
|
||||||
regionLevel: undefined,
|
regionLevel: undefined,
|
||||||
dataSource: '',
|
|
||||||
status: undefined,
|
|
||||||
},
|
},
|
||||||
parentOptions: [],
|
parentOptions: [],
|
||||||
syncLoading: false,
|
syncLoading: false,
|
||||||
@@ -182,18 +174,17 @@ export default {
|
|||||||
treeLoad: (node, resolve) => {
|
treeLoad: (node, resolve) => {
|
||||||
const parentCode = node.level === 0 ? ROOT_PARENT_CODE : node.data.id;
|
const parentCode = node.level === 0 ? ROOT_PARENT_CODE : node.data.id;
|
||||||
getLazyTree(parentCode, this.searchForm).then(res => {
|
getLazyTree(parentCode, this.searchForm).then(res => {
|
||||||
resolve(
|
const treeData = this.normalizeTreeData(res.data.data);
|
||||||
res.data.data.map(item => {
|
if (node.level === 0) {
|
||||||
return {
|
this.setDefaultExpandedKeys(treeData);
|
||||||
...item,
|
}
|
||||||
leaf: !item.hasChildren,
|
resolve(treeData);
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
addBtn: false,
|
addBtn: false,
|
||||||
menu: false,
|
menu: false,
|
||||||
|
defaultExpandAll: false,
|
||||||
|
defaultExpandedKeys: [],
|
||||||
size: 'default',
|
size: 'default',
|
||||||
props: {
|
props: {
|
||||||
labelText: '标题',
|
labelText: '标题',
|
||||||
@@ -260,6 +251,12 @@ export default {
|
|||||||
prop: 'regionLevel',
|
prop: 'regionLevel',
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
dicUrl: '/blade-system/dict/dictionary?code=region',
|
dicUrl: '/blade-system/dict/dictionary?code=region',
|
||||||
|
dicFormatter: response => {
|
||||||
|
const list = Array.isArray(response) ? response : response?.data;
|
||||||
|
return (Array.isArray(list) ? list : [])
|
||||||
|
.filter(item => Number(item.dictKey) !== 0)
|
||||||
|
.map(item => ({ ...item, dictKey: Number(item.dictKey) }));
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
label: 'dictValue',
|
label: 'dictValue',
|
||||||
value: 'dictKey',
|
value: 'dictKey',
|
||||||
@@ -411,9 +408,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['permission']),
|
...mapGetters(['permission']),
|
||||||
isCountryRegion() {
|
isCountryRegion() {
|
||||||
return (
|
return Number(this.regionForm.regionLevel) === 0;
|
||||||
Number(this.regionForm.regionLevel) === 0 || this.regionForm.parentCode === this.topCode
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
isProvinceRegion() {
|
isProvinceRegion() {
|
||||||
return Number(this.regionForm.regionLevel) === 1;
|
return Number(this.regionForm.regionLevel) === 1;
|
||||||
@@ -438,14 +433,27 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
normalizeTreeData(data) {
|
||||||
|
return data.map(item => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
leaf: !item.hasChildren,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setDefaultExpandedKeys(treeData) {
|
||||||
|
const valueKey = this.treeOption.props.value;
|
||||||
|
this.treeOption.defaultExpandedKeys = treeData
|
||||||
|
.filter(item => item.hasChildren)
|
||||||
|
.map(item => item[valueKey]);
|
||||||
|
},
|
||||||
initTree() {
|
initTree() {
|
||||||
this.treeData = [];
|
this.treeData = [];
|
||||||
getLazyTree(this.topCode, this.searchForm).then(res => {
|
getLazyTree(this.topCode, this.searchForm).then(res => {
|
||||||
this.treeData = res.data.data.map(item => {
|
const treeData = this.normalizeTreeData(res.data.data);
|
||||||
return {
|
this.treeData = treeData;
|
||||||
...item,
|
this.$nextTick(() => {
|
||||||
leaf: !item.hasChildren,
|
this.setDefaultExpandedKeys(treeData);
|
||||||
};
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -458,8 +466,6 @@ export default {
|
|||||||
code: '',
|
code: '',
|
||||||
name: '',
|
name: '',
|
||||||
regionLevel: undefined,
|
regionLevel: undefined,
|
||||||
dataSource: '',
|
|
||||||
status: undefined,
|
|
||||||
};
|
};
|
||||||
this.handleSearch();
|
this.handleSearch();
|
||||||
},
|
},
|
||||||
@@ -498,6 +504,10 @@ export default {
|
|||||||
this.$message.warning('请先选择一项区划');
|
this.$message.warning('请先选择一项区划');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (Number(this.regionForm.regionLevel) >= 3) {
|
||||||
|
this.$message.warning('区县已是末级,不能新增下级');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const parentCode = this.regionForm.code;
|
const parentCode = this.regionForm.code;
|
||||||
const parentName = this.regionForm.name;
|
const parentName = this.regionForm.name;
|
||||||
const parentGroupCode = this.regionForm.parentCode || this.topCode;
|
const parentGroupCode = this.regionForm.parentCode || this.topCode;
|
||||||
@@ -508,8 +518,7 @@ export default {
|
|||||||
this.regionForm.subCode = '';
|
this.regionForm.subCode = '';
|
||||||
this.regionForm.originalCode = '';
|
this.regionForm.originalCode = '';
|
||||||
this.regionForm.name = '';
|
this.regionForm.name = '';
|
||||||
this.regionForm.regionLevel =
|
this.regionForm.regionLevel = Number(this.regionForm.regionLevel) + 1;
|
||||||
this.regionForm.regionLevel === 5 ? 5 : this.regionForm.regionLevel + 1;
|
|
||||||
this.loadParentOptionsByGroup(parentGroupCode, parentCode, parentName);
|
this.loadParentOptionsByGroup(parentGroupCode, parentCode, parentName);
|
||||||
},
|
},
|
||||||
setRegionColumnDisplay(prop, display) {
|
setRegionColumnDisplay(prop, display) {
|
||||||
@@ -628,9 +637,14 @@ export default {
|
|||||||
},
|
},
|
||||||
handleImport() {
|
handleImport() {
|
||||||
this.syncImportAction();
|
this.syncImportAction();
|
||||||
openImportDialog(this, '行政区划', () => {
|
openImportDialog(
|
||||||
this.initTree();
|
this,
|
||||||
});
|
'行政区划',
|
||||||
|
() => {
|
||||||
|
this.initTree();
|
||||||
|
},
|
||||||
|
{ timeout: 300000 }
|
||||||
|
);
|
||||||
},
|
},
|
||||||
syncImportAction() {
|
syncImportAction() {
|
||||||
const column = this.findColumn(this.excelOption.column, 'excelFile');
|
const column = this.findColumn(this.excelOption.column, 'excelFile');
|
||||||
@@ -750,4 +764,16 @@ export default {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.el-form--inline .el-form-item{
|
||||||
|
margin-right: 10px !important;
|
||||||
|
}
|
||||||
|
.region-page__tree-panel .avue-tree__filter{
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.el-button-group--horizontal{
|
||||||
|
margin-bottom: 12px !important;
|
||||||
|
}
|
||||||
|
.el-form-item{
|
||||||
|
margin-bottom: 9px !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -113,7 +113,7 @@
|
|||||||
:maxlength="getCargoCodeMaxLength()"
|
:maxlength="getCargoCodeMaxLength()"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
placeholder="请输入"
|
placeholder="请输入"
|
||||||
:disabled="dialogReadonly || dialogType === 'edit'"
|
:disabled="dialogReadonly"
|
||||||
@input="handleCargoCodeInput"
|
@input="handleCargoCodeInput"
|
||||||
>
|
>
|
||||||
<template v-if="getCargoCodePrefix()" #prepend>
|
<template v-if="getCargoCodePrefix()" #prepend>
|
||||||
@@ -130,11 +130,13 @@
|
|||||||
placeholder="请输入"
|
placeholder="请输入"
|
||||||
:disabled="dialogReadonly"
|
:disabled="dialogReadonly"
|
||||||
@input="handleCargoValueInput"
|
@input="handleCargoValueInput"
|
||||||
|
@blur="handleCargoValueBlur"
|
||||||
/>
|
/>
|
||||||
<el-select
|
<el-select
|
||||||
v-model="form.priceUnit"
|
v-model="form.priceUnit"
|
||||||
clearable
|
clearable
|
||||||
:disabled="dialogReadonly"
|
:disabled="dialogReadonly"
|
||||||
|
:loading="priceUnitLoading"
|
||||||
placeholder="请选择"
|
placeholder="请选择"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
@@ -147,6 +149,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #cargoValue="{ row }">
|
||||||
|
{{ formatCargoValue(row.cargoValue) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #menu="{ row, index }">
|
<template #menu="{ row, index }">
|
||||||
<el-link
|
<el-link
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -196,8 +202,8 @@ import * as api from '@/api/business/common-cargo';
|
|||||||
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||||
import { exportBlob } from '@/api/common';
|
import { exportBlob } from '@/api/common';
|
||||||
import { getDeptTree } from '@/api/system/dept';
|
import { getDeptTree } from '@/api/system/dept';
|
||||||
import { config, excelOption, option } from '@/option/business/common-cargo';
|
import { config, excelOption, formatCargoValue, option } from '@/option/business/common-cargo';
|
||||||
import { priceUnitOptions } from '@/option/business/common';
|
import { getDictionary } from '@/api/system/dictbiz';
|
||||||
import { getToken } from '@/utils/auth';
|
import { getToken } from '@/utils/auth';
|
||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
@@ -211,7 +217,8 @@ export default {
|
|||||||
api,
|
api,
|
||||||
config,
|
config,
|
||||||
option,
|
option,
|
||||||
priceUnitOptions,
|
priceUnitOptions: [],
|
||||||
|
priceUnitLoading: false,
|
||||||
form: {},
|
form: {},
|
||||||
query: {},
|
query: {},
|
||||||
loading: true,
|
loading: true,
|
||||||
@@ -254,8 +261,10 @@ export default {
|
|||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.initDeptTree();
|
this.initDeptTree();
|
||||||
|
this.loadPriceUnitOptions();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formatCargoValue,
|
||||||
hasPermission(code) {
|
hasPermission(code) {
|
||||||
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
|
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
|
||||||
},
|
},
|
||||||
@@ -273,6 +282,28 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
loadPriceUnitOptions() {
|
||||||
|
this.priceUnitLoading = true;
|
||||||
|
getDictionary({ code: 'unit_fee' })
|
||||||
|
.then(res => {
|
||||||
|
const payload = res?.data?.data || res?.data || [];
|
||||||
|
const list = Array.isArray(payload) ? payload : payload.records || payload.data || [];
|
||||||
|
this.priceUnitOptions = list
|
||||||
|
.map(item => ({
|
||||||
|
label: item.dictValue || item.label || item.name || item.dictKey || item.value,
|
||||||
|
value: item.dictKey || item.value || item.dictValue || item.name,
|
||||||
|
}))
|
||||||
|
.filter(item => item.label && item.value);
|
||||||
|
if (this.dialogType === 'add' && !this.form.priceUnit && this.priceUnitOptions.length) {
|
||||||
|
this.form.priceUnit = this.priceUnitOptions[0].value;
|
||||||
|
}
|
||||||
|
const priceUnitColumn = this.findColumn(this.option.column, 'priceUnit');
|
||||||
|
if (priceUnitColumn) priceUnitColumn.dicData = this.priceUnitOptions;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.priceUnitLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
flattenDept(tree, level = 0) {
|
flattenDept(tree, level = 0) {
|
||||||
const result = [];
|
const result = [];
|
||||||
tree.forEach(item => {
|
tree.forEach(item => {
|
||||||
@@ -389,6 +420,9 @@ export default {
|
|||||||
const decimal = parts.slice(1).join('').slice(0, 2);
|
const decimal = parts.slice(1).join('').slice(0, 2);
|
||||||
this.form.cargoValue = parts.length > 1 ? `${integer}.${decimal}` : integer;
|
this.form.cargoValue = parts.length > 1 ? `${integer}.${decimal}` : integer;
|
||||||
},
|
},
|
||||||
|
handleCargoValueBlur() {
|
||||||
|
this.form.cargoValue = formatCargoValue(this.form.cargoValue);
|
||||||
|
},
|
||||||
normalizeRow(row) {
|
normalizeRow(row) {
|
||||||
const submitRow = { ...row };
|
const submitRow = { ...row };
|
||||||
Object.keys(submitRow).forEach(key => {
|
Object.keys(submitRow).forEach(key => {
|
||||||
@@ -399,14 +433,23 @@ export default {
|
|||||||
if (submitRow.cargoValue !== undefined && submitRow.cargoValue !== null) {
|
if (submitRow.cargoValue !== undefined && submitRow.cargoValue !== null) {
|
||||||
submitRow.cargoValue = String(submitRow.cargoValue).trim();
|
submitRow.cargoValue = String(submitRow.cargoValue).trim();
|
||||||
}
|
}
|
||||||
if (!submitRow.cargoValue || String(submitRow.cargoValue) === '-1') {
|
const cargoValue = submitRow.cargoValue;
|
||||||
|
if (
|
||||||
|
cargoValue === undefined ||
|
||||||
|
cargoValue === null ||
|
||||||
|
String(cargoValue).trim() === '' ||
|
||||||
|
String(cargoValue) === '-1'
|
||||||
|
) {
|
||||||
submitRow.cargoValue = null;
|
submitRow.cargoValue = null;
|
||||||
|
} else {
|
||||||
|
submitRow.cargoValue = formatCargoValue(submitRow.cargoValue);
|
||||||
}
|
}
|
||||||
return submitRow;
|
return submitRow;
|
||||||
},
|
},
|
||||||
normalizeCargoValue(row) {
|
normalizeCargoValue(row) {
|
||||||
if (row && String(row.cargoValue) === '-1') {
|
if (row && Object.prototype.hasOwnProperty.call(row, 'cargoValue')) {
|
||||||
row.cargoValue = null;
|
const formatted = formatCargoValue(row.cargoValue);
|
||||||
|
row.cargoValue = formatted || null;
|
||||||
}
|
}
|
||||||
return row;
|
return row;
|
||||||
},
|
},
|
||||||
@@ -445,6 +488,14 @@ export default {
|
|||||||
this.$message.warning('货值只能输入数字,最多保留2位小数');
|
this.$message.warning('货值只能输入数字,最多保留2位小数');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!row.model) {
|
||||||
|
this.$message.warning('请输入型号');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (row.model.length > 50) {
|
||||||
|
this.$message.warning('型号不能超过50个字符');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (row.remark && row.remark.length > 200) {
|
if (row.remark && row.remark.length > 200) {
|
||||||
this.$message.warning('备注不能超过200个字符');
|
this.$message.warning('备注不能超过200个字符');
|
||||||
return false;
|
return false;
|
||||||
@@ -539,8 +590,8 @@ export default {
|
|||||||
this.dialogType = type || 'add';
|
this.dialogType = type || 'add';
|
||||||
if (type === 'add') {
|
if (type === 'add') {
|
||||||
this.form = {
|
this.form = {
|
||||||
priceUnit: '元/吨',
|
|
||||||
...this.form,
|
...this.form,
|
||||||
|
priceUnit: this.priceUnitOptions[0]?.value || this.form.priceUnit || '',
|
||||||
};
|
};
|
||||||
this.loadFirstCargoTypeOptions();
|
this.loadFirstCargoTypeOptions();
|
||||||
done();
|
done();
|
||||||
|
|||||||
@@ -288,7 +288,10 @@
|
|||||||
搜索
|
搜索
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div ref="routeAmap" class="common-route-page__map"></div>
|
<div class="map-picker-content">
|
||||||
|
<map-search-results :results="routeMapSearchResults" @select="selectRouteMapSearchResult" />
|
||||||
|
<div ref="routeAmap" class="common-route-page__map"></div>
|
||||||
|
</div>
|
||||||
<div class="common-route-page__map-info">
|
<div class="common-route-page__map-info">
|
||||||
<span>{{ routeMapStatus }}</span>
|
<span>{{ routeMapStatus }}</span>
|
||||||
<span v-if="routeMapSelected.regionName"
|
<span v-if="routeMapSelected.regionName"
|
||||||
@@ -359,6 +362,7 @@ export default {
|
|||||||
routeMapKeyword: '',
|
routeMapKeyword: '',
|
||||||
routeMapStatus: '可搜索地址或点击地图选点',
|
routeMapStatus: '可搜索地址或点击地图选点',
|
||||||
routeMapSelected: {},
|
routeMapSelected: {},
|
||||||
|
routeMapSearchResults: [],
|
||||||
routeAmap: null,
|
routeAmap: null,
|
||||||
routeAmapMarker: null,
|
routeAmapMarker: null,
|
||||||
routeAmapGeocoder: null,
|
routeAmapGeocoder: null,
|
||||||
@@ -612,6 +616,12 @@ export default {
|
|||||||
})
|
})
|
||||||
.then(() => this.runAmapGeocode('location', keyword))
|
.then(() => this.runAmapGeocode('location', keyword))
|
||||||
.then(result => {
|
.then(result => {
|
||||||
|
this.routeMapSearchResults = (result.geocodes || []).map((item, index) => ({
|
||||||
|
id: item.id || index,
|
||||||
|
name: item.formattedAddress || keyword,
|
||||||
|
address: item.formattedAddress || keyword,
|
||||||
|
location: item.location,
|
||||||
|
}));
|
||||||
const point = this.resolveMapPoint(result);
|
const point = this.resolveMapPoint(result);
|
||||||
if (!point) {
|
if (!point) {
|
||||||
this.routeMapStatus = '未找到匹配地址';
|
this.routeMapStatus = '未找到匹配地址';
|
||||||
@@ -628,6 +638,11 @@ export default {
|
|||||||
this.routeMapLoading = false;
|
this.routeMapLoading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
selectRouteMapSearchResult(item) {
|
||||||
|
if (item && item.location) {
|
||||||
|
this.pickRouteMapPoint(item.location, item.address || item.name || '');
|
||||||
|
}
|
||||||
|
},
|
||||||
pickRouteMapPoint(lnglat, keyword) {
|
pickRouteMapPoint(lnglat, keyword) {
|
||||||
const longitude = this.getPointLng(lnglat);
|
const longitude = this.getPointLng(lnglat);
|
||||||
const latitude = this.getPointLat(lnglat);
|
const latitude = this.getPointLat(lnglat);
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
:value="item.value" /></el-select></el-form-item
|
:value="item.value" /></el-select></el-form-item
|
||||||
></el-col>
|
></el-col>
|
||||||
<el-col :span="8"
|
<el-col :span="8"
|
||||||
><el-form-item label="默认方案"
|
><el-form-item
|
||||||
><el-checkbox v-model="draft.defaultPlan" :disabled="readonly">默认方案</el-checkbox
|
><el-checkbox v-model="draft.defaultPlan" :disabled="readonly">默认方案</el-checkbox
|
||||||
><el-tooltip content="同一运输方式仅支持配置一个默认计费方案" placement="top">
|
><el-tooltip content="同一运输方式仅支持配置一个默认计费方案" placement="top">
|
||||||
<el-icon class="billing-plan-editor__default-tip"
|
<el-icon class="billing-plan-editor__default-tip"
|
||||||
@@ -103,12 +103,28 @@
|
|||||||
filterable
|
filterable
|
||||||
:disabled="!row.feeType"
|
:disabled="!row.feeType"
|
||||||
:loading="feeItemLoadingMap[feeTypeKey(row)]"
|
:loading="feeItemLoadingMap[feeTypeKey(row)]"
|
||||||
|
@change="value => handleFeeItemChange(row, value)"
|
||||||
><el-option
|
><el-option
|
||||||
v-for="item in feeItemOptions(row)"
|
v-for="item in feeItemOptions(row)"
|
||||||
:key="item.id || item.name || item.englishName"
|
:key="item.id || item.name || item.englishName"
|
||||||
:label="item.name || item.englishName"
|
:label="item.name || item.englishName"
|
||||||
:value="item.name || item.englishName" /></el-select></template
|
:value="item.name || item.englishName" /></el-select></template
|
||||||
></el-table-column>
|
></el-table-column>
|
||||||
|
<el-table-column label="税率" width="180"
|
||||||
|
><template #default="{ row }"
|
||||||
|
><span v-if="readonly">{{ formatTaxRate(row.taxRate) }}</span
|
||||||
|
><el-input
|
||||||
|
v-else
|
||||||
|
v-model="row.taxRate"
|
||||||
|
inputmode="numeric"
|
||||||
|
maxlength="3"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入"
|
||||||
|
@input="value => taxRateInput(row, value)"
|
||||||
|
><template #append>%</template></el-input
|
||||||
|
></template
|
||||||
|
></el-table-column
|
||||||
|
>
|
||||||
<el-table-column label="计费要素" width="170"
|
<el-table-column label="计费要素" width="170"
|
||||||
><template #default="{ row }"
|
><template #default="{ row }"
|
||||||
><span v-if="readonly">{{ displayValue(row.billingElement) }}</span
|
><span v-if="readonly">{{ displayValue(row.billingElement) }}</span
|
||||||
@@ -213,32 +229,38 @@
|
|||||||
</div></template
|
</div></template
|
||||||
></el-table-column
|
></el-table-column
|
||||||
>
|
>
|
||||||
<el-table-column label="保底计费重量" width="240"
|
<el-table-column width="240"
|
||||||
|
><template #header
|
||||||
|
><span class="billing-plan-editor__column-title"
|
||||||
|
>保底计费重量<el-tooltip content="实际计量值小于保底数值时按保底计费" placement="top"
|
||||||
|
><el-icon class="billing-plan-editor__default-tip"><InfoFilled /></el-icon></el-tooltip></span></template
|
||||||
><template #default="{ row }"
|
><template #default="{ row }"
|
||||||
><div v-if="readonly && usesRangeMinimum(row)" class="limit-list">
|
><div v-if="readonly && usesRangeMinimum(row)" class="limit-list">
|
||||||
<span v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex">{{
|
<span v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex">{{
|
||||||
displayValue(item.minimumBillingWeight)
|
rangeIndex === 0
|
||||||
|
? displayValue(item.minimumBillingWeight || row.minimumBillingWeight)
|
||||||
|
: ''
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-else-if="readonly">{{ displayValue(row.minimumBillingWeight) }}</span>
|
<span v-else-if="readonly">{{ displayValue(row.minimumBillingWeight) }}</span>
|
||||||
<div v-else-if="usesRangeMinimum(row)" class="limit-list">
|
<div v-else-if="usesRangeMinimum(row)" class="limit-list">
|
||||||
<el-input
|
<template v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex">
|
||||||
v-for="(item, rangeIndex) in getRanges(row)"
|
<el-input
|
||||||
:key="rangeIndex"
|
v-if="rangeIndex === 0"
|
||||||
v-model="item.minimumBillingWeight"
|
v-model="item.minimumBillingWeight"
|
||||||
:disabled="!canEditMinimum(row)"
|
:disabled="!canEditMinimum(row)"
|
||||||
:placeholder="canEditMinimum(row) ? '请输入保底计费重量' : '无需配置'"
|
:placeholder="canEditMinimum(row) ? '请输入' : '无需配置'"
|
||||||
@input="value => rangeMinimumInput(row, rangeIndex, value)"
|
@input="value => rangeMinimumInput(row, rangeIndex, value)"
|
||||||
/>
|
/>
|
||||||
|
<div v-else class="limit-placeholder" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<el-input
|
<el-input
|
||||||
v-else
|
v-else
|
||||||
v-model="row.minimumBillingWeight"
|
v-model="row.minimumBillingWeight"
|
||||||
:disabled="!canEditMinimum(row)"
|
:disabled="!canEditMinimum(row)"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
canEditMinimum(row)
|
canEditMinimum(row) ? '请输入' : '仅按重量、按吨·公里可填写'
|
||||||
? '实际计量值小于保底数值时按保底计费'
|
|
||||||
: '仅按重量、按吨·公里可填写'
|
|
||||||
"
|
"
|
||||||
@input="value => decimalInput(row, 'minimumBillingWeight', value)" /></template
|
@input="value => decimalInput(row, 'minimumBillingWeight', value)" /></template
|
||||||
></el-table-column>
|
></el-table-column>
|
||||||
@@ -281,9 +303,6 @@
|
|||||||
<el-descriptions-item label="目的地">{{
|
<el-descriptions-item label="目的地">{{
|
||||||
displayValue(matchForm.destination)
|
displayValue(matchForm.destination)
|
||||||
}}</el-descriptions-item>
|
}}</el-descriptions-item>
|
||||||
<el-descriptions-item label="运输方式">{{
|
|
||||||
transportModeLabel(matchForm.transportMode)
|
|
||||||
}}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="货物类型">{{
|
<el-descriptions-item label="货物类型">{{
|
||||||
displayValue(matchForm.cargoType)
|
displayValue(matchForm.cargoType)
|
||||||
}}</el-descriptions-item>
|
}}</el-descriptions-item>
|
||||||
@@ -318,21 +337,6 @@
|
|||||||
@visible-change="v => v && ensureRegions()"
|
@visible-change="v => v && ensureRegions()"
|
||||||
@change="v => matchRegionChange('destination', v)" /></el-form-item
|
@change="v => matchRegionChange('destination', v)" /></el-form-item
|
||||||
></el-col>
|
></el-col>
|
||||||
<el-col :span="12"
|
|
||||||
><el-form-item label="运输方式"
|
|
||||||
><el-select
|
|
||||||
v-model="matchForm.transportMode"
|
|
||||||
:loading="transportModeLoading"
|
|
||||||
:disabled="readonly"
|
|
||||||
placeholder="请选择运输方式"
|
|
||||||
clearable
|
|
||||||
filterable
|
|
||||||
><el-option
|
|
||||||
v-for="item in transportModeOptions"
|
|
||||||
:key="item.value"
|
|
||||||
:label="item.label"
|
|
||||||
:value="item.value" /></el-select></el-form-item
|
|
||||||
></el-col>
|
|
||||||
<el-col :span="12"
|
<el-col :span="12"
|
||||||
><el-form-item label="货物类型"
|
><el-form-item label="货物类型"
|
||||||
><el-cascader
|
><el-cascader
|
||||||
@@ -372,6 +376,7 @@ const clone = value => JSON.parse(JSON.stringify(value));
|
|||||||
const defaultRule = () => ({
|
const defaultRule = () => ({
|
||||||
feeType: '',
|
feeType: '',
|
||||||
feeItem: '',
|
feeItem: '',
|
||||||
|
taxRate: '',
|
||||||
billingElement: '',
|
billingElement: '',
|
||||||
billingType: '',
|
billingType: '',
|
||||||
billingUnit: '',
|
billingUnit: '',
|
||||||
@@ -467,7 +472,6 @@ export default {
|
|||||||
label: 'cargoName',
|
label: 'cargoName',
|
||||||
value: 'id',
|
value: 'id',
|
||||||
children: 'children',
|
children: 'children',
|
||||||
disabled: (data, node) => node.level === 1,
|
|
||||||
leaf: 'leaf',
|
leaf: 'leaf',
|
||||||
checkStrictly: true,
|
checkStrictly: true,
|
||||||
emitPath: true,
|
emitPath: true,
|
||||||
@@ -544,30 +548,24 @@ export default {
|
|||||||
return next;
|
return next;
|
||||||
},
|
},
|
||||||
normalizeRanges(row) {
|
normalizeRanges(row) {
|
||||||
const ranges = Array.isArray(row.limitRanges)
|
const rawRanges = Array.isArray(row.limitRanges) ? row.limitRanges : [];
|
||||||
? row.limitRanges
|
const ranges = rawRanges
|
||||||
.map(item => ({
|
.map((item, index) => ({
|
||||||
lowerLimit: item?.lowerLimit ?? '',
|
lowerLimit: item?.lowerLimit ?? '',
|
||||||
upperLimit: item?.upperLimit ?? '',
|
upperLimit: item?.upperLimit ?? '',
|
||||||
unitPrice:
|
unitPrice:
|
||||||
item?.unitPrice === undefined || item?.unitPrice === ''
|
item?.unitPrice === undefined || item?.unitPrice === ''
|
||||||
? row.unitPrice ?? ''
|
? row.unitPrice ?? ''
|
||||||
: item.unitPrice,
|
: item.unitPrice,
|
||||||
minimumBillingWeight:
|
minimumBillingWeight: this.resolveRangeMinimum(row, item, index),
|
||||||
item?.minimumBillingWeight === undefined || item?.minimumBillingWeight === ''
|
}))
|
||||||
? this.usesRangeMinimum(row)
|
.filter(
|
||||||
? row.minimumBillingWeight ?? ''
|
item =>
|
||||||
: ''
|
item.lowerLimit !== '' ||
|
||||||
: item.minimumBillingWeight,
|
item.upperLimit !== '' ||
|
||||||
}))
|
item.unitPrice !== '' ||
|
||||||
.filter(
|
item.minimumBillingWeight !== ''
|
||||||
item =>
|
);
|
||||||
item.lowerLimit !== '' ||
|
|
||||||
item.upperLimit !== '' ||
|
|
||||||
item.unitPrice !== '' ||
|
|
||||||
item.minimumBillingWeight !== ''
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
if (
|
if (
|
||||||
!ranges.length &&
|
!ranges.length &&
|
||||||
(row.lowerLimit !== '' ||
|
(row.lowerLimit !== '' ||
|
||||||
@@ -582,9 +580,23 @@ export default {
|
|||||||
minimumBillingWeight: this.usesRangeMinimum(row) ? row.minimumBillingWeight ?? '' : '',
|
minimumBillingWeight: this.usesRangeMinimum(row) ? row.minimumBillingWeight ?? '' : '',
|
||||||
});
|
});
|
||||||
return ranges.length
|
return ranges.length
|
||||||
? ranges
|
? ranges.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
minimumBillingWeight: this.usesRangeMinimum(row)
|
||||||
|
? index === 0
|
||||||
|
? item.minimumBillingWeight
|
||||||
|
: ''
|
||||||
|
: '',
|
||||||
|
}))
|
||||||
: [{ lowerLimit: '', upperLimit: '', unitPrice: '', minimumBillingWeight: '' }];
|
: [{ lowerLimit: '', upperLimit: '', unitPrice: '', minimumBillingWeight: '' }];
|
||||||
},
|
},
|
||||||
|
resolveRangeMinimum(row, item, index) {
|
||||||
|
if (!this.usesRangeMinimum(row) || index !== 0) return '';
|
||||||
|
if (item?.minimumBillingWeight !== undefined && item?.minimumBillingWeight !== '') {
|
||||||
|
return item.minimumBillingWeight;
|
||||||
|
}
|
||||||
|
return row.minimumBillingWeight ?? '';
|
||||||
|
},
|
||||||
syncLegacyLimit(row) {
|
syncLegacyLimit(row) {
|
||||||
const first = row.limitRanges?.[0] || {};
|
const first = row.limitRanges?.[0] || {};
|
||||||
row.lowerLimit = first.lowerLimit ?? '';
|
row.lowerLimit = first.lowerLimit ?? '';
|
||||||
@@ -622,27 +634,67 @@ export default {
|
|||||||
feeItemOptions(row) {
|
feeItemOptions(row) {
|
||||||
return this.feeItems[this.feeTypeKey(row)] || [];
|
return this.feeItems[this.feeTypeKey(row)] || [];
|
||||||
},
|
},
|
||||||
|
formatTaxRate(value) {
|
||||||
|
return value === undefined || value === null || value === '' ? '-' : `${value}%`;
|
||||||
|
},
|
||||||
loadRuleItems() {
|
loadRuleItems() {
|
||||||
(this.draft.rules || []).forEach(row => this.loadFeeItems(row.feeType));
|
(this.draft.rules || []).forEach(row => this.loadFeeItems(row.feeType));
|
||||||
},
|
},
|
||||||
loadFeeItems(value) {
|
loadFeeItems(value) {
|
||||||
const key = this.feeTypeKey({ feeType: value });
|
const key = this.feeTypeKey({ feeType: value });
|
||||||
if (!key || this.feeItems[key]) return;
|
if (!key) return;
|
||||||
|
if (this.feeItems[key]) {
|
||||||
|
this.fillLoadedRuleTaxRates(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.feeItemLoadingMap[key] = true;
|
this.feeItemLoadingMap[key] = true;
|
||||||
getFeeItemList(1, 9999, { feeCategory: key })
|
getFeeItemList(1, 9999, { feeCategory: key })
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const data = res?.data?.data || res?.data || {};
|
const data = res?.data?.data || res?.data || {};
|
||||||
this.feeItems[key] = Array.isArray(data) ? data : data.records || [];
|
this.feeItems[key] = Array.isArray(data) ? data : data.records || [];
|
||||||
|
this.fillLoadedRuleTaxRates(key);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.feeItemLoadingMap[key] = false;
|
this.feeItemLoadingMap[key] = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
fillLoadedRuleTaxRates(key) {
|
||||||
|
(this.draft.rules || [])
|
||||||
|
.filter(row => this.feeTypeKey(row) === key)
|
||||||
|
.forEach(row => this.fillRuleTaxRate(row));
|
||||||
|
},
|
||||||
handleFeeTypeChange(row, value) {
|
handleFeeTypeChange(row, value) {
|
||||||
row.feeType = this.feeTypeKey({ feeType: value });
|
row.feeType = this.feeTypeKey({ feeType: value });
|
||||||
row.feeItem = '';
|
row.feeItem = '';
|
||||||
|
row.taxRate = '';
|
||||||
this.loadFeeItems(row.feeType);
|
this.loadFeeItems(row.feeType);
|
||||||
},
|
},
|
||||||
|
handleFeeItemChange(row, value) {
|
||||||
|
row.feeItem = value;
|
||||||
|
const feeItem = this.feeItemOptions(row).find(
|
||||||
|
item => String(item.name || item.englishName || '') === String(value || '')
|
||||||
|
);
|
||||||
|
row.taxRate = this.hasTaxRate(feeItem?.taxRate) ? this.normalizeTaxRate(feeItem.taxRate) : '';
|
||||||
|
},
|
||||||
|
fillRuleTaxRate(row) {
|
||||||
|
if (this.hasTaxRate(row.taxRate) || !row.feeItem) return;
|
||||||
|
const feeItem = this.feeItemOptions(row).find(
|
||||||
|
item => String(item.name || item.englishName || '') === String(row.feeItem)
|
||||||
|
);
|
||||||
|
if (this.hasTaxRate(feeItem?.taxRate)) row.taxRate = this.normalizeTaxRate(feeItem.taxRate);
|
||||||
|
},
|
||||||
|
hasTaxRate(value) {
|
||||||
|
return value !== undefined && value !== null && String(value).trim() !== '';
|
||||||
|
},
|
||||||
|
normalizeTaxRate(value) {
|
||||||
|
const rate = Number(String(value ?? '').trim());
|
||||||
|
return Number.isFinite(rate) ? String(Math.trunc(Math.abs(rate))) : '';
|
||||||
|
},
|
||||||
|
taxRateInput(row, value) {
|
||||||
|
let text = String(value ?? '').replace(/\D/g, '');
|
||||||
|
if (text.length > 1) text = text.replace(/^0+/, '') || '0';
|
||||||
|
row.taxRate = text;
|
||||||
|
},
|
||||||
billingTypes(row) {
|
billingTypes(row) {
|
||||||
return this.typeMap[row.billingElement] || [];
|
return this.typeMap[row.billingElement] || [];
|
||||||
},
|
},
|
||||||
@@ -747,11 +799,16 @@ export default {
|
|||||||
if (index === 0) row.unitPrice = target.value;
|
if (index === 0) row.unitPrice = target.value;
|
||||||
},
|
},
|
||||||
rangeMinimumInput(row, index, value) {
|
rangeMinimumInput(row, index, value) {
|
||||||
|
if (index !== 0) return;
|
||||||
row.limitRanges = this.getRanges(row);
|
row.limitRanges = this.getRanges(row);
|
||||||
const target = { value };
|
const target = { value };
|
||||||
this.decimalInput(target, 'value', value);
|
this.decimalInput(target, 'value', value);
|
||||||
row.limitRanges[index].minimumBillingWeight = target.value;
|
row.limitRanges[0].minimumBillingWeight = target.value;
|
||||||
if (index === 0) row.minimumBillingWeight = target.value;
|
row.minimumBillingWeight = target.value;
|
||||||
|
row.limitRanges = row.limitRanges.map((item, rangeIndex) => ({
|
||||||
|
...item,
|
||||||
|
minimumBillingWeight: rangeIndex === 0 ? target.value : '',
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
addRule() {
|
addRule() {
|
||||||
this.draft.rules.push(defaultRule());
|
this.draft.rules.push(defaultRule());
|
||||||
@@ -771,6 +828,7 @@ export default {
|
|||||||
const required = [
|
const required = [
|
||||||
['feeType', '费用类型'],
|
['feeType', '费用类型'],
|
||||||
['feeItem', '费用项'],
|
['feeItem', '费用项'],
|
||||||
|
['taxRate', '税率'],
|
||||||
['billingElement', '计费要素'],
|
['billingElement', '计费要素'],
|
||||||
['billingType', '计费类型'],
|
['billingType', '计费类型'],
|
||||||
['billingUnit', '计费单位'],
|
['billingUnit', '计费单位'],
|
||||||
@@ -789,6 +847,13 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
feeItemSet.add(feeItem);
|
feeItemSet.add(feeItem);
|
||||||
|
if (this.hasTaxRate(row.taxRate)) {
|
||||||
|
const taxRate = Number(row.taxRate);
|
||||||
|
if (!/^\d+$/.test(String(row.taxRate).trim()) || taxRate < 0 || taxRate > 100) {
|
||||||
|
this.$message.warning(`第${i + 1}行税率必须是0到100之间的整数`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
!this.usesRangeUnitPrice(row) &&
|
!this.usesRangeUnitPrice(row) &&
|
||||||
(row.unitPrice === undefined ||
|
(row.unitPrice === undefined ||
|
||||||
@@ -803,8 +868,9 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!this.canEditLimit(row)) continue;
|
if (!this.canEditLimit(row)) continue;
|
||||||
const key = row.billingElement;
|
const billingElement = String(row.billingElement).trim();
|
||||||
groups[key] = groups[key] || [];
|
const key = JSON.stringify([feeItem, billingElement]);
|
||||||
|
groups[key] = groups[key] || { feeItem, billingElement, ranges: [] };
|
||||||
for (const range of this.getRanges(row)) {
|
for (const range of this.getRanges(row)) {
|
||||||
const lower = Number(range.lowerLimit);
|
const lower = Number(range.lowerLimit);
|
||||||
const upper = Number(range.upperLimit);
|
const upper = Number(range.upperLimit);
|
||||||
@@ -830,22 +896,24 @@ export default {
|
|||||||
this.$message.warning('计费要素下限不能大于上限');
|
this.$message.warning('计费要素下限不能大于上限');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
groups[key].push({ lower, upper });
|
groups[key].ranges.push({ lower, upper });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.validateRangeGroups(groups);
|
return this.validateRangeGroups(groups);
|
||||||
},
|
},
|
||||||
validateRangeGroups(groups) {
|
validateRangeGroups(groups) {
|
||||||
const precision = 0.000001;
|
const precision = 0.000001;
|
||||||
for (const [element, ranges] of Object.entries(groups)) {
|
for (const { feeItem, billingElement, ranges } of Object.values(groups)) {
|
||||||
const sorted = [...ranges].sort((a, b) => a.lower - b.lower || a.upper - b.upper);
|
const sorted = [...ranges].sort((a, b) => a.lower - b.lower || a.upper - b.upper);
|
||||||
for (let i = 1; i < sorted.length; i += 1) {
|
for (let i = 1; i < sorted.length; i += 1) {
|
||||||
if (sorted[i].lower < sorted[i - 1].upper - precision) {
|
if (sorted[i].lower < sorted[i - 1].upper - precision) {
|
||||||
this.$message.warning(`${element}的计费要素区间不能重叠`);
|
this.$message.warning(`费用项“${feeItem}”${billingElement}的计费要素区间不能重叠`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (sorted[i].lower > sorted[i - 1].upper + precision) {
|
if (sorted[i].lower > sorted[i - 1].upper + precision) {
|
||||||
this.$message.warning(`${element}的计费要素区间必须连续,不能存在间隙`);
|
this.$message.warning(
|
||||||
|
`费用项“${feeItem}”${billingElement}的计费要素区间必须连续,不能存在间隙`
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -862,15 +930,22 @@ export default {
|
|||||||
plan.transportModeLabel = transportMode?.label || plan.transportMode;
|
plan.transportModeLabel = transportMode?.label || plan.transportMode;
|
||||||
plan.rules = plan.rules.map(rule => {
|
plan.rules = plan.rules.map(rule => {
|
||||||
const ranges = this.canEditLimit(rule) ? this.getRanges(rule) : [];
|
const ranges = this.canEditLimit(rule) ? this.getRanges(rule) : [];
|
||||||
|
const normalizedRanges = ranges.map((item, rangeIndex) => ({
|
||||||
|
...item,
|
||||||
|
minimumBillingWeight:
|
||||||
|
this.usesRangeMinimum(rule) && rangeIndex === 0 ? item.minimumBillingWeight || '' : '',
|
||||||
|
}));
|
||||||
return {
|
return {
|
||||||
...rule,
|
...rule,
|
||||||
unitPrice: this.usesRangeUnitPrice(rule) ? ranges[0]?.unitPrice || '' : rule.unitPrice,
|
unitPrice: this.usesRangeUnitPrice(rule)
|
||||||
limitRanges: ranges,
|
? normalizedRanges[0]?.unitPrice || ''
|
||||||
lowerLimit: ranges[0]?.lowerLimit || '',
|
: rule.unitPrice,
|
||||||
upperLimit: ranges[0]?.upperLimit || '',
|
limitRanges: normalizedRanges,
|
||||||
|
lowerLimit: normalizedRanges[0]?.lowerLimit || '',
|
||||||
|
upperLimit: normalizedRanges[0]?.upperLimit || '',
|
||||||
minimumBillingWeight: this.canEditMinimum(rule)
|
minimumBillingWeight: this.canEditMinimum(rule)
|
||||||
? this.usesRangeMinimum(rule)
|
? this.usesRangeMinimum(rule)
|
||||||
? ranges[0]?.minimumBillingWeight || ''
|
? normalizedRanges[0]?.minimumBillingWeight || ''
|
||||||
: rule.minimumBillingWeight
|
: rule.minimumBillingWeight
|
||||||
: '',
|
: '',
|
||||||
};
|
};
|
||||||
@@ -883,9 +958,6 @@ export default {
|
|||||||
this.matchIndex = index;
|
this.matchIndex = index;
|
||||||
this.matchForm = { ...defaultRule().matchCondition, ...(row.matchCondition || {}) };
|
this.matchForm = { ...defaultRule().matchCondition, ...(row.matchCondition || {}) };
|
||||||
this.matchVisible = true;
|
this.matchVisible = true;
|
||||||
this.ensureTransportModes().then(() => {
|
|
||||||
this.matchForm.transportMode = this.normalizeTransportMode(this.matchForm.transportMode);
|
|
||||||
});
|
|
||||||
this.ensureRegions();
|
this.ensureRegions();
|
||||||
this.ensureCargo().then(() => {
|
this.ensureCargo().then(() => {
|
||||||
const path = this.resolveCargoPath(this.matchForm);
|
const path = this.resolveCargoPath(this.matchForm);
|
||||||
@@ -910,21 +982,6 @@ export default {
|
|||||||
});
|
});
|
||||||
return this.transportModeRequest;
|
return this.transportModeRequest;
|
||||||
},
|
},
|
||||||
normalizeTransportMode(value) {
|
|
||||||
const transportMode = String(value || '').trim();
|
|
||||||
if (!transportMode) return '';
|
|
||||||
const option = this.transportModeOptions.find(item => {
|
|
||||||
const optionValue = String(item.value || '').trim();
|
|
||||||
const optionLabel = String(item.label || '').trim();
|
|
||||||
return (
|
|
||||||
optionValue === transportMode ||
|
|
||||||
optionLabel === transportMode ||
|
|
||||||
optionLabel.includes(transportMode) ||
|
|
||||||
transportMode.includes(optionLabel)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return option?.value || transportMode;
|
|
||||||
},
|
|
||||||
ensureRegions() {
|
ensureRegions() {
|
||||||
if (this.regionOptions.length) return Promise.resolve(this.regionOptions);
|
if (this.regionOptions.length) return Promise.resolve(this.regionOptions);
|
||||||
if (this.regionRequest) return this.regionRequest;
|
if (this.regionRequest) return this.regionRequest;
|
||||||
@@ -1078,25 +1135,27 @@ export default {
|
|||||||
},
|
},
|
||||||
matchCargoChange(value) {
|
matchCargoChange(value) {
|
||||||
const path =
|
const path =
|
||||||
Array.isArray(value) && value.length >= 2
|
Array.isArray(value) && value.length
|
||||||
? value.slice(0, 2).map(item => String(item))
|
? value.slice(0, 2).map(item => String(item))
|
||||||
: [];
|
: [];
|
||||||
const item = this.cargoFlatOptions.find(
|
const item = this.cargoFlatOptions.find(
|
||||||
option => String(option.id) === String(path[1]) && option.path?.length >= 2
|
option =>
|
||||||
|
option.path?.length === path.length &&
|
||||||
|
option.path.every((itemValue, index) => String(itemValue) === path[index])
|
||||||
);
|
);
|
||||||
this.matchForm.cargoTypePath = path;
|
this.matchForm.cargoTypePath = path;
|
||||||
this.matchForm.cargoType = item ? this.cargoLabels(path).join('/') : '';
|
this.matchForm.cargoType = item ? this.cargoLabels(path).join('/') : '';
|
||||||
this.matchForm.cargoTypeCode = item ? item.cargoCode || item.code || item.id : '';
|
this.matchForm.cargoTypeCode = item ? item.cargoCode || item.code || item.id : '';
|
||||||
},
|
},
|
||||||
resolveCargoPath(condition) {
|
resolveCargoPath(condition) {
|
||||||
if (Array.isArray(condition.cargoTypePath) && condition.cargoTypePath.length >= 2)
|
if (Array.isArray(condition.cargoTypePath) && condition.cargoTypePath.length)
|
||||||
return condition.cargoTypePath;
|
return condition.cargoTypePath.slice(0, 2).map(item => String(item));
|
||||||
const item = this.cargoFlatOptions.find(
|
const item = this.cargoFlatOptions.find(
|
||||||
option =>
|
option =>
|
||||||
option.path?.length >= 2 &&
|
|
||||||
(String(option.cargoCode || option.code || option.id) ===
|
(String(option.cargoCode || option.code || option.id) ===
|
||||||
String(condition.cargoTypeCode) ||
|
String(condition.cargoTypeCode) ||
|
||||||
option.cargoName === condition.cargoType)
|
option.cargoName === condition.cargoType ||
|
||||||
|
this.cargoLabels(option.path).join('/') === condition.cargoType)
|
||||||
);
|
);
|
||||||
return item?.path || [];
|
return item?.path || [];
|
||||||
},
|
},
|
||||||
@@ -1145,6 +1204,11 @@ export default {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.billing-plan-editor__column-title {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.limit-list {
|
.limit-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -1160,4 +1224,8 @@ export default {
|
|||||||
.limit-row .el-input {
|
.limit-row .el-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.limit-placeholder {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,596 @@
|
|||||||
|
<template>
|
||||||
|
<section
|
||||||
|
class="contract-manage-form__section contract-manage-form__section--panel contract-manage-form__section--attachment"
|
||||||
|
>
|
||||||
|
<div class="contract-manage-form__attachment-head">
|
||||||
|
<div class="dialog-section-title">{{ title }}</div>
|
||||||
|
<el-button type="primary" :disabled="!rows.length" @click="batchDownload">批量下载</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
:data="rows"
|
||||||
|
border
|
||||||
|
class="contract-manage-form__attachment-table"
|
||||||
|
@selection-change="selected = $event"
|
||||||
|
>
|
||||||
|
<el-table-column v-if="!readonly" type="selection" width="55" align="center" />
|
||||||
|
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||||
|
<el-table-column v-if="attachmentType" label="附件类型" min-width="160" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="readonly || isRowLocked(row)">{{ row.fileType || '-' }}</span>
|
||||||
|
<el-select
|
||||||
|
v-else
|
||||||
|
:model-value="row.fileType"
|
||||||
|
placeholder="请选择"
|
||||||
|
:teleported="false"
|
||||||
|
:fit-input-width="true"
|
||||||
|
@update:model-value="value => updateRowFileType(row, value)"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in contractAttachmentTypeOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
label="文件名"
|
||||||
|
min-width="240"
|
||||||
|
align="left"
|
||||||
|
header-align="left"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link type="primary" @click="preview?.(row, rows)">{{ attachmentName(row) }}</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column v-if="description" label="附件描述" min-width="220">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="readonly || isRowLocked(row)">{{ row.description || '-' }}</span>
|
||||||
|
<el-input
|
||||||
|
v-else
|
||||||
|
:model-value="row.description"
|
||||||
|
maxlength="200"
|
||||||
|
@update:model-value="value => updateRowDescription(row, value)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="文件大小" width="120" align="center">
|
||||||
|
<template #default="{ row }">{{ formatSize(row.size) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
||||||
|
<el-table-column prop="uploadTime" label="上传时间" width="180" align="center" sortable />
|
||||||
|
<el-table-column v-if="!readonly" label="操作" width="100" align="center" fixed="right">
|
||||||
|
<template #default="{ row, $index }">
|
||||||
|
<span v-if="isRowLocked(row)">-</span>
|
||||||
|
<el-link v-else type="danger" @click="remove($index)">删除</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div v-if="!readonly" class="contract-manage-form__attachment-upload">
|
||||||
|
<template v-if="useUploadDialog">
|
||||||
|
<el-button type="primary" plain @click="openUploadDialog">上传附件</el-button>
|
||||||
|
<span class="contract-manage-form__attachment-tip">
|
||||||
|
支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过50M
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<vehicle-attachment-upload
|
||||||
|
v-else
|
||||||
|
:model-value="rows"
|
||||||
|
:file-types="attachmentFileTypes"
|
||||||
|
:max-size="50"
|
||||||
|
show-tip
|
||||||
|
tip="支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过50M"
|
||||||
|
:show-file-list="false"
|
||||||
|
button-text="上传附件"
|
||||||
|
@update:model-value="handleChange"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="uploadDialogVisible"
|
||||||
|
title="附件上传"
|
||||||
|
width="560px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
class="contract-attachment-upload-dialog"
|
||||||
|
@closed="resetUploadDialog"
|
||||||
|
>
|
||||||
|
<div class="contract-attachment-upload-dialog__body">
|
||||||
|
<div class="contract-attachment-upload-dialog__field">
|
||||||
|
<span class="contract-attachment-upload-dialog__label">附件位置</span>
|
||||||
|
<el-select :model-value="attachmentLocation" disabled style="width: 100%">
|
||||||
|
<el-option :label="attachmentLocation" :value="attachmentLocation" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="contract-attachment-upload-dialog__field">
|
||||||
|
<span class="contract-attachment-upload-dialog__label">附件类型</span>
|
||||||
|
<el-select v-model="uploadDialogType" placeholder="请选择附件类型" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="item in contractAttachmentTypeOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="contract-attachment-upload-dialog__uploader">
|
||||||
|
<el-upload
|
||||||
|
drag
|
||||||
|
multiple
|
||||||
|
:action="uploadAction"
|
||||||
|
:headers="uploadHeaders"
|
||||||
|
:accept="acceptText"
|
||||||
|
:show-file-list="true"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:on-success="handleDialogSuccess"
|
||||||
|
:on-error="handleDialogError"
|
||||||
|
:on-remove="handleDialogRemove"
|
||||||
|
:file-list="uploadDialogFileList"
|
||||||
|
>
|
||||||
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
|
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="contract-attachment-upload-dialog__footer">
|
||||||
|
<el-button type="primary" @click="confirmUploadDialog">确定</el-button>
|
||||||
|
<el-button type="primary" @click="uploadDialogVisible = false">取消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { UploadFilled } from '@element-plus/icons-vue';
|
||||||
|
import { baseUrl } from '@/config/env';
|
||||||
|
import { getUploadHeaders } from '@/utils/upload';
|
||||||
|
import { downloadFileByUrl } from '@/utils/util';
|
||||||
|
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||||||
|
|
||||||
|
const CONTRACT_ATTACHMENT_TYPE_OTHER = '其它文件';
|
||||||
|
export const defaultContractAttachmentTypeOptions = [
|
||||||
|
{ label: '合同文件', value: '合同文件' },
|
||||||
|
{ label: '双章归档文件', value: '双章归档文件' },
|
||||||
|
{ label: '其它文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
|
||||||
|
];
|
||||||
|
/** @deprecated 兼容旧引用,实际选项由业务字典 contract_attachment_type 动态加载 */
|
||||||
|
export const contractAttachmentTypeOptions = defaultContractAttachmentTypeOptions;
|
||||||
|
export const attachmentFileTypes = [
|
||||||
|
'pdf',
|
||||||
|
'bmp',
|
||||||
|
'jpeg',
|
||||||
|
'png',
|
||||||
|
'jpg',
|
||||||
|
'doc',
|
||||||
|
'docx',
|
||||||
|
'ppt',
|
||||||
|
'pptx',
|
||||||
|
'xlsx',
|
||||||
|
'xls',
|
||||||
|
'eml',
|
||||||
|
'msg',
|
||||||
|
'zip',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const attachmentName = row => row?.originalName || row?.name || row?.fileName || '附件';
|
||||||
|
export const attachmentUrl = row =>
|
||||||
|
row?.url || row?.link || row?.fileUrl || row?.downloadUrl || row?.domain || '';
|
||||||
|
export const isAttachmentApproved = row =>
|
||||||
|
row?.approved === true ||
|
||||||
|
row?.approved === 1 ||
|
||||||
|
row?.approved === '1' ||
|
||||||
|
row?.approvalStatus === 'approved';
|
||||||
|
|
||||||
|
const normalizeAttachmentTypeText = value => {
|
||||||
|
let text = String(value || '').split(/[?#]/)[0];
|
||||||
|
try {
|
||||||
|
text = decodeURIComponent(text);
|
||||||
|
} catch (error) {
|
||||||
|
// ignore decode error
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
.replace(/\.[^.\\/]+$/, '')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[\s_\-—–·•()()\[\]【】{}《》<>“”"'、,,。.]/g, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const attachmentTypeKeywords = fileType => {
|
||||||
|
if (fileType === '双章归档文件') return ['双章归档文件', '双章归档', '双章'];
|
||||||
|
if (fileType === '合同文件') return ['合同文件', '合同'];
|
||||||
|
return [fileType];
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveOtherAttachmentType = (options = []) => {
|
||||||
|
const list = options.length ? options : defaultContractAttachmentTypeOptions;
|
||||||
|
const matched = list.find(
|
||||||
|
item =>
|
||||||
|
String(item.value) === CONTRACT_ATTACHMENT_TYPE_OTHER ||
|
||||||
|
String(item.label).includes('其它') ||
|
||||||
|
String(item.label).includes('其他')
|
||||||
|
);
|
||||||
|
return matched?.value || list[list.length - 1]?.value || CONTRACT_ATTACHMENT_TYPE_OTHER;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveContractAttachmentType = (
|
||||||
|
row = {},
|
||||||
|
options = defaultContractAttachmentTypeOptions
|
||||||
|
) => {
|
||||||
|
const list = options.length ? options : defaultContractAttachmentTypeOptions;
|
||||||
|
const values = list.map(item => item.value);
|
||||||
|
if (values.includes(row.fileType)) return row.fileType;
|
||||||
|
const fileName = normalizeAttachmentTypeText(attachmentName(row));
|
||||||
|
const otherType = resolveOtherAttachmentType(list);
|
||||||
|
if (!fileName) return otherType;
|
||||||
|
const matched = list
|
||||||
|
.filter(item => item.value !== otherType)
|
||||||
|
.flatMap(item =>
|
||||||
|
attachmentTypeKeywords(item.value).map(keyword => ({
|
||||||
|
value: item.value,
|
||||||
|
keyword: normalizeAttachmentTypeText(keyword),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.filter(item => item.keyword)
|
||||||
|
.sort((a, b) => b.keyword.length - a.keyword.length)
|
||||||
|
.find(item => fileName.includes(item.keyword));
|
||||||
|
return matched?.value || otherType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeContractFileRows = (rows, options = defaultContractAttachmentTypeOptions) =>
|
||||||
|
(rows || []).map(row => ({
|
||||||
|
...row,
|
||||||
|
fileType: resolveContractAttachmentType(row, options),
|
||||||
|
description: row.description || '',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mapDictOptions = response => {
|
||||||
|
const data = response?.data?.data || response?.data || [];
|
||||||
|
const records = Array.isArray(data) ? data : data.records || [];
|
||||||
|
return records
|
||||||
|
.map(item => ({
|
||||||
|
label: item.dictValue,
|
||||||
|
value: item.dictKey,
|
||||||
|
}))
|
||||||
|
.filter(item => item.label && item.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ContractAttachmentSection',
|
||||||
|
components: { UploadFilled },
|
||||||
|
props: {
|
||||||
|
title: { type: String, required: true },
|
||||||
|
rows: { type: Array, default: () => [] },
|
||||||
|
description: Boolean,
|
||||||
|
attachmentType: Boolean,
|
||||||
|
readonly: Boolean,
|
||||||
|
lockApproved: Boolean,
|
||||||
|
markApprovedOnUpload: Boolean,
|
||||||
|
useUploadDialog: Boolean,
|
||||||
|
attachmentLocation: { type: String, default: '合同文件' },
|
||||||
|
preview: { type: Function, default: null },
|
||||||
|
},
|
||||||
|
emits: ['update:rows'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selected: [],
|
||||||
|
attachmentFileTypes,
|
||||||
|
contractAttachmentTypeOptions: [...defaultContractAttachmentTypeOptions],
|
||||||
|
uploadDialogVisible: false,
|
||||||
|
uploadDialogType: resolveOtherAttachmentType(defaultContractAttachmentTypeOptions),
|
||||||
|
uploadDialogFiles: [],
|
||||||
|
uploadDialogFileList: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
uploadAction() {
|
||||||
|
return `${baseUrl}/blade-resource/oss/endpoint/put-file`;
|
||||||
|
},
|
||||||
|
uploadHeaders() {
|
||||||
|
return getUploadHeaders();
|
||||||
|
},
|
||||||
|
acceptText() {
|
||||||
|
return this.attachmentFileTypes.map(item => `.${item}`).join(',');
|
||||||
|
},
|
||||||
|
defaultAttachmentType() {
|
||||||
|
return resolveOtherAttachmentType(this.contractAttachmentTypeOptions);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadAttachmentTypeOptions();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
attachmentName,
|
||||||
|
loadAttachmentTypeOptions() {
|
||||||
|
return getBizDictionary({ code: 'contract_attachment_type' })
|
||||||
|
.then(res => {
|
||||||
|
const options = mapDictOptions(res);
|
||||||
|
if (options.length) {
|
||||||
|
this.contractAttachmentTypeOptions = options;
|
||||||
|
if (
|
||||||
|
!options.some(item => String(item.value) === String(this.uploadDialogType))
|
||||||
|
) {
|
||||||
|
this.uploadDialogType = this.defaultAttachmentType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 字典加载失败时保留默认选项
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isRowLocked(row) {
|
||||||
|
return this.lockApproved && isAttachmentApproved(row);
|
||||||
|
},
|
||||||
|
update(rows) {
|
||||||
|
this.$emit('update:rows', rows);
|
||||||
|
},
|
||||||
|
updateRowFileType(row, value) {
|
||||||
|
if (this.isRowLocked(row)) return;
|
||||||
|
row.fileType = value;
|
||||||
|
this.update([...this.rows]);
|
||||||
|
},
|
||||||
|
updateRowDescription(row, value) {
|
||||||
|
if (this.isRowLocked(row)) return;
|
||||||
|
row.description = value;
|
||||||
|
this.update([...this.rows]);
|
||||||
|
},
|
||||||
|
openUploadDialog() {
|
||||||
|
this.uploadDialogType = this.defaultAttachmentType;
|
||||||
|
this.uploadDialogFiles = [];
|
||||||
|
this.uploadDialogFileList = [];
|
||||||
|
this.uploadDialogVisible = true;
|
||||||
|
},
|
||||||
|
resetUploadDialog() {
|
||||||
|
this.uploadDialogFiles = [];
|
||||||
|
this.uploadDialogFileList = [];
|
||||||
|
this.uploadDialogType = this.defaultAttachmentType;
|
||||||
|
},
|
||||||
|
beforeUpload(file) {
|
||||||
|
const extension = String(file.name || '')
|
||||||
|
.split('.')
|
||||||
|
.pop()
|
||||||
|
?.toLowerCase();
|
||||||
|
if (!this.attachmentFileTypes.includes(extension)) {
|
||||||
|
this.$message.warning('文件类型不符合上传要求');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (file.size / 1024 / 1024 > 50) {
|
||||||
|
this.$message.warning('单文件大小不能超过 50MB');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
normalizeUploadFile(file, response) {
|
||||||
|
const data = response?.data || file.response?.data || {};
|
||||||
|
const url = data.link || data.url || data.domain || file.url || '';
|
||||||
|
const originalName =
|
||||||
|
data.originalName || file.name || data.name || attachmentName({ url }) || '附件';
|
||||||
|
return {
|
||||||
|
uid: file.uid,
|
||||||
|
originalName,
|
||||||
|
name: originalName,
|
||||||
|
url,
|
||||||
|
link: data.link || url,
|
||||||
|
size: data.size || data.attachSize || file.size || '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
handleDialogSuccess(response, file, fileList) {
|
||||||
|
if (!response || response.success === false || (response.code && response.code !== 200)) {
|
||||||
|
this.$message.error(response?.msg || '上传失败');
|
||||||
|
this.uploadDialogFileList = (fileList || []).filter(item => item.uid !== file.uid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalized = this.normalizeUploadFile(file, response);
|
||||||
|
this.uploadDialogFileList = fileList || [];
|
||||||
|
this.uploadDialogFiles = [
|
||||||
|
...this.uploadDialogFiles.filter(item => String(item.uid) !== String(file.uid)),
|
||||||
|
normalized,
|
||||||
|
];
|
||||||
|
this.uploadDialogType = resolveContractAttachmentType(
|
||||||
|
{
|
||||||
|
...normalized,
|
||||||
|
fileType: '',
|
||||||
|
},
|
||||||
|
this.contractAttachmentTypeOptions
|
||||||
|
);
|
||||||
|
this.$message.success('上传成功');
|
||||||
|
},
|
||||||
|
handleDialogError() {
|
||||||
|
this.$message.error('上传失败');
|
||||||
|
},
|
||||||
|
handleDialogRemove(file) {
|
||||||
|
this.uploadDialogFiles = this.uploadDialogFiles.filter(
|
||||||
|
item => String(item.uid) !== String(file.uid)
|
||||||
|
);
|
||||||
|
this.uploadDialogFileList = this.uploadDialogFileList.filter(
|
||||||
|
item => String(item.uid) !== String(file.uid)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
confirmUploadDialog() {
|
||||||
|
if (!this.uploadDialogFiles.length) {
|
||||||
|
this.$message.warning('请先上传附件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.uploadDialogType) {
|
||||||
|
this.$message.warning('请选择附件类型');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uploadUserName = this.$store.getters.userInfo?.realName || '';
|
||||||
|
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
const appended = this.uploadDialogFiles.map(row => ({
|
||||||
|
...row,
|
||||||
|
fileType: this.uploadDialogType,
|
||||||
|
description: row.description || '',
|
||||||
|
uploadUserName: row.uploadUserName || uploadUserName,
|
||||||
|
uploadTime: row.uploadTime || uploadTime,
|
||||||
|
...(this.markApprovedOnUpload ? { approved: true } : {}),
|
||||||
|
}));
|
||||||
|
this.update([...(this.rows || []), ...appended]);
|
||||||
|
this.uploadDialogVisible = false;
|
||||||
|
},
|
||||||
|
handleChange(rows) {
|
||||||
|
const uploadUserName = this.$store.getters.userInfo?.realName || '';
|
||||||
|
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
const existingRows = this.rows || [];
|
||||||
|
this.update(
|
||||||
|
(rows || []).map(row => {
|
||||||
|
const fileUrl = attachmentUrl(row);
|
||||||
|
const existing = existingRows.find(item => {
|
||||||
|
const sameUid = row.uid && item.uid && String(row.uid) === String(item.uid);
|
||||||
|
const sameUrl = fileUrl && attachmentUrl(item) === fileUrl;
|
||||||
|
return sameUid || sameUrl;
|
||||||
|
});
|
||||||
|
if (existing && this.isRowLocked(existing)) return { ...existing };
|
||||||
|
const next = {
|
||||||
|
...existing,
|
||||||
|
...row,
|
||||||
|
description: row.description || existing?.description || '',
|
||||||
|
uploadUserName: existing?.uploadUserName || row.uploadUserName || uploadUserName,
|
||||||
|
uploadTime: existing?.uploadTime || row.uploadTime || uploadTime,
|
||||||
|
approved: existing?.approved || row.approved || false,
|
||||||
|
};
|
||||||
|
if (this.attachmentType) {
|
||||||
|
next.fileType = resolveContractAttachmentType(
|
||||||
|
{
|
||||||
|
...next,
|
||||||
|
fileType: existing?.fileType || row.fileType,
|
||||||
|
},
|
||||||
|
this.contractAttachmentTypeOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.markApprovedOnUpload && !existing) next.approved = true;
|
||||||
|
return next;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
remove(index) {
|
||||||
|
const row = this.rows[index];
|
||||||
|
if (this.isRowLocked(row)) {
|
||||||
|
this.$message.warning('已审核通过的附件不允许删除');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rows = [...this.rows];
|
||||||
|
rows.splice(index, 1);
|
||||||
|
this.update(rows);
|
||||||
|
},
|
||||||
|
download(row) {
|
||||||
|
const url = attachmentUrl(row);
|
||||||
|
if (!url) {
|
||||||
|
this.$message.warning('附件地址为空,无法下载');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
downloadFileByUrl(url, attachmentName(row));
|
||||||
|
},
|
||||||
|
batchDownload() {
|
||||||
|
(this.selected.length ? this.selected : this.rows).forEach(this.download);
|
||||||
|
},
|
||||||
|
formatSize(size) {
|
||||||
|
const value = Number(size);
|
||||||
|
if (!value) return '';
|
||||||
|
if (value < 1024) return `${value}B`;
|
||||||
|
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)}KB`;
|
||||||
|
return `${(value / 1024 / 1024).toFixed(1)}MB`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.contract-manage-form__section {
|
||||||
|
margin: 12px 0 0;
|
||||||
|
padding: 14px 16px 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
.dialog-section-title {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: inline-block;
|
||||||
|
width: 4px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 8px;
|
||||||
|
vertical-align: -2px;
|
||||||
|
background: #409eff;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-manage-form__attachment-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.dialog-section-title {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-manage-form__attachment-upload {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-manage-form__attachment-tip {
|
||||||
|
margin-left: 30px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-attachment-upload-dialog {
|
||||||
|
&__body {
|
||||||
|
padding: 4px 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
flex: none;
|
||||||
|
width: 72px;
|
||||||
|
color: #606266;
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__uploader {
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
:deep(.el-upload) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-upload-dragger) {
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<section v-if="master" class="detail-overview">
|
<section v-if="master" class="detail-overview">
|
||||||
<div class="detail-heading"><div><h2>多联总单详情 <span>|</span> {{ master.masterNo }}</h2><el-tag :type="statusType(master.businessStatus)" class="status-text">{{ statusName(master.businessStatus) }}</el-tag><el-tag v-if="transportFlowLabel" type="info" class="transport-flow-tag">{{ transportFlowLabel }}</el-tag></div><el-button @click="$emit('back')">取消</el-button></div>
|
<div class="detail-heading"><div><h2>多联总单详情 <span>|</span> {{ master.masterNo }}</h2><el-tag :type="statusType(master.businessStatus)" class="status-text">{{ statusName(master.businessStatus) }}</el-tag><el-tag v-if="transportFlowLabel" type="info" class="transport-flow-tag">{{ transportFlowLabel }}</el-tag></div><el-button @click="$emit('back')">取消</el-button></div>
|
||||||
<dl class="detail-meta"><div><dt>客户</dt><dd>{{ master.customerName || '-' }}</dd></div><div><dt>合同编号</dt><dd>{{ master.contractNo || '-' }}</dd></div><div><dt>项目</dt><dd>{{ master.projectName || '-' }}</dd></div><div class="detail-meta__attachments"><dt>附件</dt><dd><template v-if="attachments.length"><el-link v-for="file in attachments" :key="file.url || file.link || file.name || file.originalName" type="primary" @click="previewAttachment(file)">{{ attachmentName(file) }}</el-link></template><span v-else>-</span></dd></div></dl>
|
<dl class="detail-meta"><div><dt>客户</dt><dd>{{ master.customerName || '-' }}</dd></div><div><dt>合同编号</dt><dd>{{ master.contractNo || '-' }}</dd></div><div><dt>项目</dt><dd>{{ master.projectName || '-' }}</dd></div><div class="detail-meta__attachments"><dt>附件</dt><dd><template v-if="attachments.length"><el-link v-for="file in attachments" :key="file.url || file.link || file.name || file.originalName" type="primary" @click="previewAttachment(file)">{{ attachmentName(file) }}</el-link></template><span v-else>-</span></dd></div></dl>
|
||||||
<div class="route-map"><template v-for="(route, index) in segments" :key="route.segmentNo"><div class="route-map__node"><span :class="['route-badge', index ? 'middle' : 'start']">{{ index ? '经' : '起' }}</span><strong>{{ route.departureName || '-' }}</strong><small>{{ route.departureAddress || '-' }}</small><small class="route-map__progress">{{ index ? `已到达 ${quantity(segments[index - 1].arrivedQuantity)}/${quantity(master.totalQuantity)}` : `已调度 ${quantity(route.dispatchedQuantity)}/${quantity(master.totalQuantity)}` }}</small><small v-if="index" class="route-map__progress">已调度 {{ quantity(route.dispatchedQuantity) }}/{{ quantity(master.totalQuantity) }}</small></div><div class="route-map__line"></div><div v-if="index === segments.length - 1" class="route-map__node"><span class="route-badge end">终</span><strong>{{ route.arrivalName || '-' }}</strong><small>{{ route.arrivalAddress || '-' }}</small><small class="route-map__progress">已到达 {{ quantity(route.arrivedQuantity) }}/{{ quantity(master.totalQuantity) }}</small></div></template></div>
|
<div class="route-map"><template v-for="(route, index) in segments" :key="route.segmentNo"><div class="route-map__node"><span :class="['route-badge', index ? 'middle' : 'start']">{{ index ? '经' : '起' }}</span><strong>{{ segmentLocationName(route, 'departure') }}</strong><small>{{ route.departureAddress || '-' }}</small><small class="route-map__progress">{{ index ? `已到达 ${quantity(segments[index - 1].arrivedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(segments[index - 1])}` : `已调度 ${quantity(route.dispatchedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(route)}` }}</small><small v-if="index" class="route-map__progress">已调度 {{ quantity(route.dispatchedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div><div class="route-map__line"></div><div v-if="index === segments.length - 1" class="route-map__node"><span class="route-badge end">终</span><strong>{{ segmentLocationName(route, 'arrival') }}</strong><small>{{ route.arrivalAddress || '-' }}</small><small class="route-map__progress">已到达 {{ quantity(route.arrivedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div></template></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<el-dialog v-model="documentPreviewVisible" :title="previewFile.name || '附件预览'" append-to-body destroy-on-close width="90%" top="4vh">
|
<el-dialog v-model="documentPreviewVisible" :title="previewFile.name || '附件预览'" append-to-body destroy-on-close width="90%" top="4vh">
|
||||||
@@ -14,10 +14,10 @@
|
|||||||
<section v-if="master" class="execution-detail-card">
|
<section v-if="master" class="execution-detail-card">
|
||||||
<div class="execution-detail-heading"><h3>分段执行明细</h3></div>
|
<div class="execution-detail-heading"><h3>分段执行明细</h3></div>
|
||||||
<section v-for="(route, index) in segments" :key="route.segmentNo" class="segment-detail">
|
<section v-for="(route, index) in segments" :key="route.segmentNo" class="segment-detail">
|
||||||
<header @click="toggleSegment(route.segmentNo)"><h3><span class="segment-index">{{ segmentNumber(route, index) }}</span><span>{{ route.departureName || '-' }} → {{ route.arrivalName || '-' }}</span><span class="segment-detail__transport-type">{{ segmentTransportType(route) || '-' }}</span><span class="segment-detail__progress-bar"><span :style="{ width: `${segmentProgress(route)}%` }"></span></span><span class="segment-detail__progress">已调度 <em>{{ quantity(route.dispatchedQuantity) }}</em> / {{ quantity(plannedQuantity(route)) }} {{ plannedUnit(route) }}</span></h3><div class="segment-detail__header-right"><el-tooltip :content="isSegmentExpanded(route.segmentNo) ? '折叠' : '展开'" placement="top"><el-button circle :icon="isSegmentExpanded(route.segmentNo) ? ArrowUp : ArrowDown" @click.stop="toggleSegment(route.segmentNo)" /></el-tooltip></div></header>
|
<header @click="toggleSegment(route.segmentNo)"><h3><span class="segment-index">{{ segmentNumber(route, index) }}</span><span>{{ segmentLocationName(route, 'departure') }} → {{ segmentLocationName(route, 'arrival') }}</span><span class="segment-detail__transport-type">{{ segmentTransportType(route) || '-' }}</span><span class="segment-detail__progress-bar"><span :style="{ width: `${segmentProgress(route)}%` }"></span></span><span class="segment-detail__progress">已调度 <em>{{ quantity(route.dispatchedQuantity) }}</em> / {{ quantity(plannedQuantity(route)) }} {{ plannedUnit(route) }}</span><el-button text class="segment-toggle" @click.stop="toggleSegment(route.segmentNo)"><span>{{ isSegmentExpanded(route.segmentNo) ? '收起' : '展开' }}</span><el-icon class="segment-toggle__icon"><component :is="isSegmentExpanded(route.segmentNo) ? CaretTop : CaretBottom" /></el-icon></el-button></h3></header>
|
||||||
<div v-show="isSegmentExpanded(route.segmentNo)" class="segment-execution">
|
<div v-show="isSegmentExpanded(route.segmentNo)" class="segment-execution">
|
||||||
<div class="segment-stats"><div><span>总量</span><strong>{{ quantity(master.totalQuantity) }}<small>吨</small></strong></div><div><span>已调度</span><strong class="dispatched">{{ quantity(route.dispatchedQuantity) }}<small>吨</small></strong></div><div><span>剩余</span><strong class="remaining">{{ quantity(remainingQuantity(route)) }}<small>吨</small></strong></div></div>
|
<div class="segment-stats"><div><span>总量</span><strong>{{ quantity(master.totalQuantity) }}<small>吨</small></strong></div><div><span>已调度</span><strong class="dispatched">{{ quantity(route.dispatchedQuantity) }}<small>吨</small></strong></div><div><span>剩余</span><strong class="remaining">{{ quantity(remainingQuantity(route)) }}<small>吨</small></strong></div></div>
|
||||||
<el-table :data="route.waybills || []" border class="waybill-table"><el-table-column label="运单号" min-width="180"><template #default="{ row }"><el-link type="primary" @click="openWaybill(row)">{{ row.waybillNo }}</el-link></template></el-table-column><el-table-column prop="carrierName" label="承运商" min-width="190" /><el-table-column label="司机/车牌" min-width="180"><template #default="{ row }">{{ driverVehicle(row) }}</template></el-table-column><el-table-column prop="cargoType" label="货物类型" min-width="120" /><el-table-column label="数量(吨)" width="130"><template #default="{ row }">{{ quantity(row.quantity) }}</template></el-table-column><el-table-column label="状态" width="130"><template #default="{ row }"><el-tag :type="waybillStatusType(row.businessStatus)" size="small" class="status-text">{{ waybillStatusName(row.businessStatus) }}</el-tag></template></el-table-column><el-table-column prop="createTime" label="创建时间" min-width="180" /></el-table>
|
<el-table :data="route.waybills || []" border class="waybill-table"><el-table-column label="运单号" min-width="180"><template #default="{ row }"><el-link type="primary" @click="openWaybill(row)">{{ row.waybillNo }}</el-link></template></el-table-column><el-table-column prop="carrierName" label="承运商" min-width="190" /><el-table-column :label="isRoadTransport(route) ? '司机/车牌' : '船长/船号'" min-width="180"><template #default="{ row }">{{ driverVehicle(row, route) }}</template></el-table-column><el-table-column prop="cargoType" label="货物类型" min-width="120" /><el-table-column label="数量(吨)" width="130"><template #default="{ row }">{{ quantity(row.quantity) }}</template></el-table-column><el-table-column label="状态" width="130"><template #default="{ row }"><el-tag :type="waybillStatusType(row.businessStatus)" effect="dark" size="small">{{ waybillStatusName(row.businessStatus) }}</el-tag></template></el-table-column><el-table-column prop="createTime" label="创建时间" min-width="180" /></el-table>
|
||||||
<el-empty v-if="!(route.waybills || []).length" description="暂无运单明细" :image-size="56" />
|
<el-empty v-if="!(route.waybills || []).length" description="暂无运单明细" :image-size="56" />
|
||||||
<div v-if="(route.transportPlans || []).length" class="transport-plan-detail"><h4>关联计划单</h4><el-table :data="route.transportPlans" border><el-table-column label="计划单号" min-width="160"><template #default="{ row }"><el-link type="primary" @click="openPlan(row)">{{ row.planNo }}</el-link></template></el-table-column><el-table-column prop="planName" label="计划名称" min-width="180" /><el-table-column prop="transportType" label="运输方式" width="130" /><el-table-column prop="planStartDate" label="计划开始日期" width="130" /><el-table-column prop="planEndDate" label="计划结束日期" width="130" /><el-table-column prop="businessStatus" label="状态" width="110" /></el-table></div>
|
<div v-if="(route.transportPlans || []).length" class="transport-plan-detail"><h4>关联计划单</h4><el-table :data="route.transportPlans" border><el-table-column label="计划单号" min-width="160"><template #default="{ row }"><el-link type="primary" @click="openPlan(row)">{{ row.planNo }}</el-link></template></el-table-column><el-table-column prop="planName" label="计划名称" min-width="180" /><el-table-column prop="transportType" label="运输方式" width="130" /><el-table-column prop="planStartDate" label="计划开始日期" width="130" /><el-table-column prop="planEndDate" label="计划结束日期" width="130" /><el-table-column prop="businessStatus" label="状态" width="110" /></el-table></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,20 +26,14 @@
|
|||||||
<section v-if="master" class="master-goods-card">
|
<section v-if="master" class="master-goods-card">
|
||||||
<div class="master-goods-card__heading"><h3>货物信息</h3></div>
|
<div class="master-goods-card__heading"><h3>货物信息</h3></div>
|
||||||
<el-table :data="master.goods || []" border>
|
<el-table :data="master.goods || []" border>
|
||||||
<el-table-column type="index" label="序号" width="64" />
|
|
||||||
<el-table-column prop="cargoName" label="货物名称" min-width="150" />
|
<el-table-column prop="cargoName" label="货物名称" min-width="150" />
|
||||||
<el-table-column prop="cargoType" label="货物类型" min-width="130" />
|
<el-table-column prop="cargoType" label="类型" min-width="130" />
|
||||||
<el-table-column prop="packageType" label="包装" min-width="120" />
|
<el-table-column prop="packageType" label="包装" min-width="120" />
|
||||||
<el-table-column label="重量(吨)" min-width="110"><template #default="{ row }">{{ row.weight ?? row.weightTon ?? (row.quantityUnit === '吨' ? row.quantity : '-') }}</template></el-table-column>
|
|
||||||
<el-table-column label="体积(方)" min-width="110"><template #default="{ row }">{{ row.volume ?? row.volumeCubic ?? '-' }}</template></el-table-column>
|
|
||||||
<el-table-column label="数量" min-width="100"><template #default="{ row }">{{ row.quantity ?? '-' }}{{ row.quantityUnit ? ` ${row.quantityUnit}` : '' }}</template></el-table-column>
|
<el-table-column label="数量" min-width="100"><template #default="{ row }">{{ row.quantity ?? '-' }}{{ row.quantityUnit ? ` ${row.quantityUnit}` : '' }}</template></el-table-column>
|
||||||
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
|
|
||||||
<el-table-column prop="deviceCode" label="设备编码" min-width="130" />
|
|
||||||
<el-table-column prop="brand" label="品牌" min-width="120" />
|
<el-table-column prop="brand" label="品牌" min-width="120" />
|
||||||
<el-table-column label="规格型号" min-width="150"><template #default="{ row }">{{ [row.specification, row.model].filter(Boolean).join('/') || '-' }}</template></el-table-column>
|
<el-table-column prop="specification" label="规格" min-width="150" />
|
||||||
<el-table-column prop="remark" label="备注" min-width="160" />
|
<el-table-column prop="model" label="型号" min-width="150" />
|
||||||
<el-table-column label="货物单价(元)" min-width="130"><template #default="{ row }">{{ row.unitPrice ?? '-' }}</template></el-table-column>
|
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
|
||||||
<el-table-column label="计划日期" min-width="130"><template #default="{ row }">{{ row.planDate || row.planStartDate || '-' }}</template></el-table-column>
|
|
||||||
</el-table>
|
</el-table>
|
||||||
<el-empty v-if="!(master.goods || []).length" description="暂无货物信息" :image-size="56" />
|
<el-empty v-if="!(master.goods || []).length" description="暂无货物信息" :image-size="56" />
|
||||||
</section>
|
</section>
|
||||||
@@ -48,7 +42,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as api from '@/api/business/master-order';
|
import * as api from '@/api/business/master-order';
|
||||||
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
import { CaretBottom, CaretTop } from '@element-plus/icons-vue';
|
||||||
import { ElImageViewer } from 'element-plus';
|
import { ElImageViewer } from 'element-plus';
|
||||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||||
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
|
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
|
||||||
@@ -61,7 +55,7 @@ export default {
|
|||||||
components: { ElImageViewer, OpenFileViewer },
|
components: { ElImageViewer, OpenFileViewer },
|
||||||
props: { id: [String, Number] },
|
props: { id: [String, Number] },
|
||||||
emits: ['back'],
|
emits: ['back'],
|
||||||
data() { return { loading: false, master: null, expandedSegments: {}, ArrowDown, ArrowUp, imagePreviewVisible: false, imagePreviewUrls: [], imagePreviewIndex: 0, documentPreviewVisible: false, previewFile: {}, viewerPlugins, viewerToolbar: { download: true, fullscreen: true, print: true, rotate: true, zoom: true } }; },
|
data() { return { loading: false, master: null, expandedSegments: {}, CaretBottom, CaretTop, imagePreviewVisible: false, imagePreviewUrls: [], imagePreviewIndex: 0, documentPreviewVisible: false, previewFile: {}, viewerPlugins, viewerToolbar: { download: true, fullscreen: true, print: true, rotate: true, zoom: true } }; },
|
||||||
computed: {
|
computed: {
|
||||||
segments() {
|
segments() {
|
||||||
const routes = this.master?.routeProgress || this.master?.routes || [];
|
const routes = this.master?.routeProgress || this.master?.routes || [];
|
||||||
@@ -94,6 +88,36 @@ export default {
|
|||||||
toggleSegment(segmentNo) { this.expandedSegments = { ...this.expandedSegments, [segmentNo]: !this.isSegmentExpanded(segmentNo) }; },
|
toggleSegment(segmentNo) { this.expandedSegments = { ...this.expandedSegments, [segmentNo]: !this.isSegmentExpanded(segmentNo) }; },
|
||||||
remainingQuantity(route) { return Math.max(0, Number(this.master?.totalQuantity || 0) - Number(route.dispatchedQuantity || 0)); },
|
remainingQuantity(route) { return Math.max(0, Number(this.master?.totalQuantity || 0) - Number(route.dispatchedQuantity || 0)); },
|
||||||
segmentTransportType(route = {}) { return route.transportTypeName || route.transportType || ''; },
|
segmentTransportType(route = {}) { return route.transportTypeName || route.transportType || ''; },
|
||||||
|
isRoadTransport(route = {}) {
|
||||||
|
const type = String(this.segmentTransportType(route)).trim().toLowerCase();
|
||||||
|
return type === 'road' || type.includes('公路');
|
||||||
|
},
|
||||||
|
formatRoadLocation(value) {
|
||||||
|
const text = String(value || '').replace(/\s+/g, '');
|
||||||
|
if (!text) return '';
|
||||||
|
const withoutProvince = text.replace(/^.*?(?:省|自治区|特别行政区)/, '');
|
||||||
|
const cityMatch = withoutProvince.match(/.+?(?:市|州(?!市)|盟(?!市))/);
|
||||||
|
if (!cityMatch) return text;
|
||||||
|
const city = cityMatch[0];
|
||||||
|
const districtSource = withoutProvince.slice(city.length);
|
||||||
|
const districtMatch = districtSource.match(/^[^市州盟]*?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/);
|
||||||
|
if (!districtMatch) return city;
|
||||||
|
const district = `${districtMatch[0]}${districtSource.slice(districtMatch[0].length).startsWith('区') ? '区' : ''}`;
|
||||||
|
return `${city} ${district}`;
|
||||||
|
},
|
||||||
|
segmentLocationName(route = {}, field) {
|
||||||
|
const name = String(route[`${field}Name`] || '').trim();
|
||||||
|
const address = String(route[`${field}Address`] || '').trim();
|
||||||
|
const formattedName = this.formatRoadLocation(name);
|
||||||
|
if (/(?:市|州|盟)/.test(formattedName)) return formattedName;
|
||||||
|
const regionalName = /(?:省|自治区|特别行政区|市|州|盟|区|县|旗)/.test(name);
|
||||||
|
if (name && !regionalName) return name;
|
||||||
|
const formattedAddress = this.formatRoadLocation(address);
|
||||||
|
if (/(?:市|州|盟)/.test(formattedAddress)) return formattedAddress;
|
||||||
|
const city = String(route[`${field}CityName`] || '').trim();
|
||||||
|
const district = String(route[`${field}DistrictName`] || '').trim();
|
||||||
|
return [city, district].filter(Boolean).join(' ') || formattedName || formattedAddress || '-';
|
||||||
|
},
|
||||||
plannedQuantity(route = {}) { return route.planQuantity || route.totalQuantity || this.master?.totalQuantity || 0; },
|
plannedQuantity(route = {}) { return route.planQuantity || route.totalQuantity || this.master?.totalQuantity || 0; },
|
||||||
plannedUnit(route = {}) { return route.quantityUnit || this.master?.goods?.[0]?.quantityUnit || '吨'; },
|
plannedUnit(route = {}) { return route.quantityUnit || this.master?.goods?.[0]?.quantityUnit || '吨'; },
|
||||||
segmentProgress(route = {}) { const planned = Number(this.plannedQuantity(route)); return planned > 0 ? Math.min(100, Math.max(0, (Number(route.dispatchedQuantity || 0) / planned) * 100)) : 0; },
|
segmentProgress(route = {}) { const planned = Number(this.plannedQuantity(route)); return planned > 0 ? Math.min(100, Math.max(0, (Number(route.dispatchedQuantity || 0) / planned) * 100)) : 0; },
|
||||||
@@ -113,11 +137,19 @@ export default {
|
|||||||
this.previewFile = { name: this.attachmentName(file), url, mimeType: file.mimeType || file.contentType || '' };
|
this.previewFile = { name: this.attachmentName(file), url, mimeType: file.mimeType || file.contentType || '' };
|
||||||
this.documentPreviewVisible = true;
|
this.documentPreviewVisible = true;
|
||||||
},
|
},
|
||||||
driverVehicle(row) { return [row.driverName, row.vehicleNo].filter(Boolean).join(' / ') || '-'; },
|
driverVehicle(row = {}, route = {}) {
|
||||||
openWaybill(row) { this.$router.push({ path: '/business/waybill-manage', query: { detailId: row.id } }); },
|
const vehicle = row.vehicleNo || row.vehicleNumber || row.shipNo || row.flightNo || row.trainNo;
|
||||||
|
const values = this.isRoadTransport(route)
|
||||||
|
? [row.driverName, vehicle]
|
||||||
|
: [row.captainName, vehicle];
|
||||||
|
return values.filter(Boolean).join(' / ') || '-';
|
||||||
|
},
|
||||||
|
openWaybill(row) {
|
||||||
|
this.$router.push({ path: '/business/waybill-manage/detail', query: { id: row.id } });
|
||||||
|
},
|
||||||
openPlan(row) { this.$router.push({ path: '/business/transport-plan', query: { detailId: row.id } }); },
|
openPlan(row) { this.$router.push({ path: '/business/transport-plan', query: { detailId: row.id } }); },
|
||||||
waybillStatusName(value) { return ({ pending: '待执行', running: '进行中', completed: '已完成', cancelled: '已取消' })[value] || value || '-'; },
|
waybillStatusName(value) { return ({ pending: '待执行', waiting: '待执行', running: '进行中', processing: '进行中', completed: '已完成', cancelled: '已取消' })[value] || value || '-'; },
|
||||||
waybillStatusType(value) { return ({ pending: 'info', running: 'warning', completed: 'success', cancelled: 'danger' })[value] || 'info'; },
|
waybillStatusType(value) { return ({ pending: 'info', waiting: 'info', running: 'warning', processing: 'warning', completed: 'success', cancelled: 'danger' })[value] || 'info'; },
|
||||||
statusName(value) { return ({ waiting_dispatch: '待调度', dispatching: '调度中', completed: '调度完成', closed: '调度关闭' })[value] || value || '-'; },
|
statusName(value) { return ({ waiting_dispatch: '待调度', dispatching: '调度中', completed: '调度完成', closed: '调度关闭' })[value] || value || '-'; },
|
||||||
statusType(value) { return ({ waiting_dispatch: 'warning', dispatching: 'success', completed: 'primary', closed: 'danger' })[value] || 'info'; },
|
statusType(value) { return ({ waiting_dispatch: 'warning', dispatching: 'success', completed: 'primary', closed: 'danger' })[value] || 'info'; },
|
||||||
},
|
},
|
||||||
@@ -132,19 +164,20 @@ export default {
|
|||||||
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #409eff; font-size: 14px; word-break: break-all; } }
|
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #409eff; font-size: 14px; word-break: break-all; } }
|
||||||
.route-map { display: flex; align-items: flex-start; padding: 16px 24px 20px; border-top: 1px solid #eff1f7; overflow-x: auto; }
|
.route-map { display: flex; align-items: flex-start; padding: 16px 24px 20px; border-top: 1px solid #eff1f7; overflow-x: auto; }
|
||||||
.route-map__node { display: grid; flex: 0 0 150px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } }
|
.route-map__node { display: grid; flex: 0 0 150px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } }
|
||||||
.route-map__progress { color: #409eff !important; }
|
.route-map__progress { color: #303133 !important; }
|
||||||
.route-badge { display: inline-flex; width: 32px; height: 32px; align-items: center; justify-content: center; border-radius: 4px; background: #67c23a; color: #fff; font-weight: 600; &.start { background: #409eff; } &.end { background: #e6a23c; } }
|
.route-badge { display: inline-flex; width: 32px; height: 32px; align-items: center; justify-content: center; border-radius: 4px; background: #67c23a; color: #fff; font-weight: 600; &.start { background: #409eff; } &.end { background: #e6a23c; } }
|
||||||
.route-map__line { flex: 1 0 64px; min-width: 64px; height: 32px; border-bottom: 6px solid #e4e7ed; }
|
.route-map__line { flex: 1 0 64px; min-width: 64px; height: 32px; border-bottom: 6px solid #e4e7ed; }
|
||||||
.execution-detail-heading { padding: 20px 24px 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
|
.execution-detail-heading { padding: 20px 24px 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
|
||||||
.segment-detail { padding: 16px 24px; border-top: 1px solid #eff1f7; header { display: flex; align-items: center; justify-content: space-between; margin: -16px -24px 8px; padding: 12px 24px; cursor: pointer; background: #fafbfc; h3 { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: 12px; margin: 0; font-size: 16px; } em { color: #409eff; font-style: normal; } } }
|
.segment-detail { padding: 16px 24px; border-top: 1px solid #eff1f7; header { display: flex; align-items: center; justify-content: space-between; margin: -16px -24px 8px; padding: 12px 24px; cursor: pointer; background: #fafbfc; h3 { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: 12px; margin: 0; font-size: 16px; } em { color: #67c23a; font-style: normal; } } }
|
||||||
.segment-detail__progress { color: #606266; font-size: 14px; font-weight: 400; }
|
.segment-detail__progress { color: #606266; font-size: 14px; font-weight: 400; }
|
||||||
.segment-detail__transport-type { color: #409eff; font-size: 14px; font-weight: 500; }
|
.segment-detail__transport-type { color: #409eff; font-size: 14px; font-weight: 500; }
|
||||||
.segment-detail__progress-bar { display: inline-flex; width: 144px; height: 6px; overflow: hidden; border-radius: 3px; background: #e4e7ed; }
|
.segment-detail__progress-bar { display: inline-flex; width: 144px; height: 6px; overflow: hidden; border-radius: 3px; background: #e4e7ed; }
|
||||||
.segment-detail__progress-bar span { display: block; height: 100%; background: #409eff; transition: width 0.2s ease; }
|
.segment-detail__progress-bar span { display: block; height: 100%; background: #409eff; transition: width 0.2s ease; }
|
||||||
.segment-detail__header-right { display: flex; align-items: center; gap: 12px; }
|
.segment-toggle { gap: 4px; padding: 4px 0; }
|
||||||
|
.segment-toggle__icon { font-size: 12px; }
|
||||||
.segment-index { display: inline-flex; width: 36px; height: 36px; align-items: center; justify-content: center; border-radius: 50%; background: #2088ee; color: #fff; font-size: 20px; font-weight: 500; }
|
.segment-index { display: inline-flex; width: 36px; height: 36px; align-items: center; justify-content: center; border-radius: 50%; background: #2088ee; color: #fff; font-size: 20px; font-weight: 500; }
|
||||||
.segment-execution { padding-top: 12px; }
|
.segment-execution { padding-top: 12px; }
|
||||||
.segment-stats { display: flex; gap: 72px; padding: 4px 0 24px; div { display: flex; flex-direction: column; gap: 8px; } span { color: #8a9bb8; font-size: 14px; } strong { color: #1f2d3d; font-size: 32px; line-height: 1; } small { margin-left: 6px; color: #8a9bb8; font-size: 16px; font-weight: 400; } .dispatched { color: #409eff; } .remaining { color: #f56c6c; } }
|
.segment-stats { display: flex; gap: 72px; padding: 4px 0 24px; div { display: flex; flex-direction: column; gap: 8px; } span { color: #8a9bb8; font-size: 14px; } strong { color: #1f2d3d; font-size: 32px; line-height: 1; } small { margin-left: 6px; color: #8a9bb8; font-size: 16px; font-weight: 400; } .dispatched { color: #67c23a; } .remaining { color: #e6a23c; } }
|
||||||
.waybill-table { :deep(th.el-table__cell) { background: #f5f7fa; color: #60738f; } }
|
.waybill-table { :deep(th.el-table__cell) { background: #f5f7fa; color: #60738f; } }
|
||||||
.transport-plan-detail { margin-top: 20px; h4 { margin: 0 0 12px; padding-left: 12px; border-left: 4px solid #409eff; font-size: 15px; } }
|
.transport-plan-detail { margin-top: 20px; h4 { margin: 0 0 12px; padding-left: 12px; border-left: 4px solid #409eff; font-size: 15px; } }
|
||||||
.master-goods-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
.master-goods-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
||||||
|
|||||||
@@ -55,28 +55,28 @@
|
|||||||
<span class="segment-checkbox-label">
|
<span class="segment-checkbox-label">
|
||||||
<span>{{ route.segmentNo }}:</span>
|
<span>{{ route.segmentNo }}:</span>
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
:content="`${route.departureName || '-'} → ${route.arrivalName || '-'}`"
|
:content="`${dispatchLocationName(route, 'departure')} → ${dispatchLocationName(route, 'arrival')}`"
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<span class="segment-checkbox-path"
|
<span class="segment-checkbox-path"
|
||||||
>{{ route.departureName || '-' }} → {{ route.arrivalName || '-' }}</span
|
>{{ dispatchLocationName(route, 'departure') }} → {{ dispatchLocationName(route, 'arrival') }}</span
|
||||||
>
|
>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</span>
|
</span>
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div>总量 {{ formatQuantity(totalQuantity) }},已调度 <em>{{ formatQuantity(dispatchedQuantity(route)) }}</em>,剩余 <em>{{ formatQuantity(remaining(route)) }}</em></div>
|
<div>总量 {{ formatQuantity(totalQuantity) }} {{ quantityUnitLabel }},已调度 <em>{{ formatQuantity(dispatchedQuantity(route)) }} {{ quantityUnitLabel }}</em>,剩余 <em>{{ formatQuantity(remaining(route)) }} {{ quantityUnitLabel }}</em></div>
|
||||||
</header>
|
</header>
|
||||||
<div v-show="route.selected" class="segment-content">
|
<div v-show="route.selected" class="segment-content">
|
||||||
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form">
|
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form">
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item label="运输方式" required class="transport-type-field"><el-input :model-value="route.transportType" readonly /></el-form-item>
|
<el-form-item label="运输方式" required class="transport-type-field"><el-input :model-value="route.transportType" disabled /></el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="发货地址" required><el-input :model-value="addressText(route.departureName, route.departureAddress)" readonly /></el-form-item>
|
<el-form-item label="发货地址" required><el-input :model-value="addressText(route.departureName, route.departureAddress)" disabled /></el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="4"><el-form-item label="联系人"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="4"><el-form-item label="联系人"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item></el-col>
|
||||||
<el-col :span="4"><el-form-item label="联系方式"><el-input v-model="route.departurePhone" placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="4"><el-form-item label="联系方式"><el-input v-model="route.departurePhone" placeholder="请输入" /></el-form-item></el-col>
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="收货地址" required><el-input :model-value="addressText(route.arrivalName, route.arrivalAddress)" readonly /></el-form-item>
|
<el-form-item label="收货地址" required><el-input :model-value="addressText(route.arrivalName, route.arrivalAddress)" disabled /></el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="4"><el-form-item label="联系人"><el-input v-model="route.arrivalContact" placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="4"><el-form-item label="联系人"><el-input v-model="route.arrivalContact" placeholder="请输入" /></el-form-item></el-col>
|
||||||
<el-col :span="4"><el-form-item label="联系方式"><el-input v-model="route.arrivalPhone" placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="4"><el-form-item label="联系方式"><el-input v-model="route.arrivalPhone" placeholder="请输入" /></el-form-item></el-col>
|
||||||
@@ -95,11 +95,11 @@
|
|||||||
<div class="goods-heading"><h3>货物信息</h3><div><el-link type="primary" @click="addGoods(route)">新增货物</el-link></div></div>
|
<div class="goods-heading"><h3>货物信息</h3><div><el-link type="primary" @click="addGoods(route)">新增货物</el-link></div></div>
|
||||||
<el-table :data="route.goods" border class="goods-table">
|
<el-table :data="route.goods" border class="goods-table">
|
||||||
<el-table-column type="index" label="序号" width="64" />
|
<el-table-column type="index" label="序号" width="64" />
|
||||||
<el-table-column label="货物类型" min-width="180"><template #default="{ row }"><el-cascader v-model="row.cargoTypePath" class="goods-table__cargo-type" :options="cargoTypeOptions" :props="cargoTypeCascaderProps" placeholder="请选择货物类型" clearable filterable @change="value => handleCargoTypeChange(route, row, value)" /></template></el-table-column>
|
<el-table-column min-width="180"><template #header><span>货物类型<span class="goods-required-mark">*</span></span></template><template #default="{ row }"><el-cascader v-model="row.cargoTypePath" class="goods-table__cargo-type" :options="cargoTypeOptions" :props="cargoTypeCascaderProps" placeholder="请选择货物类型" clearable filterable @change="value => handleCargoTypeChange(route, row, value)" /></template></el-table-column>
|
||||||
<el-table-column label="货物名称" min-width="180"><template #default="{ row }"><el-select v-model="row.sourceIndex" class="goods-table__cargo-name" placeholder="请选择总单货物" @change="selectGoods(route, row)"><el-option v-for="item in masterGoodsByCargoType(row)" :key="item.sourceIndex" :label="item.cargoName" :value="item.sourceIndex" /></el-select></template></el-table-column>
|
<el-table-column min-width="180"><template #header><span>货物名称<span class="goods-required-mark">*</span></span></template><template #default="{ row }"><el-select v-model="row.sourceIndex" class="goods-table__cargo-name" placeholder="请选择总单货物" @change="selectGoods(route, row)"><el-option v-for="item in masterGoodsByCargoType(row)" :key="item.sourceIndex" :label="item.cargoName" :value="item.sourceIndex" /></el-select></template></el-table-column>
|
||||||
<el-table-column prop="remainingQuantity" column-key="remainingQuantity" label="剩余数量" width="150" class-name="goods-table__remaining"><template #default="{ row }">{{ formatQuantity(goodsRemainingQuantity(route, row)) }}</template></el-table-column>
|
<el-table-column prop="remainingQuantity" column-key="remainingQuantity" label="剩余数量" width="150" class-name="goods-table__remaining"><template #default="{ row }">{{ formatQuantity(goodsRemainingQuantity(route, row)) }}</template></el-table-column>
|
||||||
<el-table-column prop="dispatchQuantity" column-key="dispatchQuantity" label="本次数量" width="200" class-name="goods-table__dispatch"><template #default="{ row }"><el-input :model-value="row.dispatchQuantity" inputmode="decimal" placeholder="请输入" @input="value => handleDispatchQuantityInput(route, row, value)" /></template></el-table-column>
|
<el-table-column prop="dispatchQuantity" column-key="dispatchQuantity" width="200" class-name="goods-table__dispatch"><template #header><span>本次数量<span class="goods-required-mark">*</span></span></template><template #default="{ row }"><el-input :model-value="row.dispatchQuantity" inputmode="decimal" placeholder="请输入" @input="value => handleDispatchQuantityInput(route, row, value)" /></template></el-table-column>
|
||||||
<el-table-column prop="quantityUnit" label="数量单位" width="110" />
|
<el-table-column prop="quantityUnit" width="110"><template #header><span>数量单位<span class="goods-required-mark">*</span></span></template></el-table-column>
|
||||||
<el-table-column prop="packageType" label="包装" min-width="110" />
|
<el-table-column prop="packageType" label="包装" min-width="110" />
|
||||||
<el-table-column prop="brand" label="品牌" min-width="110" />
|
<el-table-column prop="brand" label="品牌" min-width="110" />
|
||||||
<el-table-column prop="specification" label="规格" min-width="110" />
|
<el-table-column prop="specification" label="规格" min-width="110" />
|
||||||
@@ -108,62 +108,62 @@
|
|||||||
<el-table-column label="操作" width="90" fixed="right"><template #default="{ $index }"><el-link type="danger" @click="removeGoods(route, $index)">删除</el-link></template></el-table-column>
|
<el-table-column label="操作" width="90" fixed="right"><template #default="{ $index }"><el-link type="danger" @click="removeGoods(route, $index)">删除</el-link></template></el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="freight-heading"><h3>运费信息</h3></div>
|
|
||||||
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form freight-form">
|
|
||||||
<el-row v-for="(item, index) in route.freightItems" :key="item.sourceIndex" :gutter="16">
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item :label="`单价${index + 1}`">
|
|
||||||
<el-input :model-value="item.unitPrice" inputmode="decimal" placeholder="请输入" @input="value => handleFreightUnitPriceInput(item, value)">
|
|
||||||
<template #append><el-select v-model="item.priceUnit" class="freight-unit-select"><el-option label="元/吨" value="元/吨" /><el-option label="元/件" value="元/件" /><el-option label="元/方" value="元/方" /></el-select></template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item :label="`数量${index + 1}`">
|
|
||||||
<el-input :model-value="formatQuantity(item.quantity)" readonly>
|
|
||||||
<template #append><el-select v-model="item.quantityUnit" class="freight-unit-select" @change="value => handleFreightQuantityUnitChange(route, item, value)"><el-option v-for="unit in quantityUnitOptions" :key="unit" :label="unit" :value="unit" /></el-select></template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item :label="`运费${index + 1}`">
|
|
||||||
<el-input :model-value="formatAmount(freightAmount(item))" readonly><template #suffix>{{ currencyLabel(route.currency) }}</template></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-row v-if="route.freightItems.length" :gutter="16">
|
|
||||||
<el-col :span="6"><el-form-item label="运费合计"><el-input :model-value="formatAmount(freightTotal(route))" readonly><template #suffix>{{ currencyLabel(route.currency) }}</template></el-input></el-form-item></el-col>
|
|
||||||
<el-col :span="6"><el-form-item label="其他费用合计"><el-input :model-value="route.otherFeeTotal" inputmode="decimal" placeholder="请输入" @input="value => handleOtherFeeTotalInput(route, value)" /></el-form-item></el-col>
|
|
||||||
<el-col :span="6"><el-form-item label="币种"><el-select v-model="route.currency"><el-option label="人民币" value="CNY" /><el-option label="美元" value="USD" /><el-option label="欧元" value="EUR" /></el-select></el-form-item></el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template v-if="route.documentType === '运单'">
|
<template v-if="route.documentType === '运单'">
|
||||||
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form carrier-type-form"><el-form-item label="承运类型" required><el-radio-group v-model="route.carrierType" @change="value => handleCarrierTypeChange(route, value)"><el-radio-button label="承运商" /><el-radio-button label="自运" /><el-radio-button label="网货平台" /></el-radio-group></el-form-item></el-form>
|
<div class="goods-heading"><h3>任务信息</h3></div>
|
||||||
|
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form carrier-type-form"><el-form-item label="承运类型" required><el-radio-group v-model="route.carrierType" @change="value => handleCarrierTypeChange(route, value)"><el-radio-button label="承运商" /><el-radio-button label="自运" /></el-radio-group></el-form-item></el-form>
|
||||||
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form carrier-form">
|
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form carrier-form">
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<template v-if="isRoad(route)">
|
<template v-if="isRoad(route)">
|
||||||
<el-col v-if="route.carrierType === '承运商'" :span="6"><el-form-item label="承运商" required><el-select v-model="route.carrierContractId" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="item.contractId" :label="carrierOptionLabel(item)" :value="item.contractId" /></el-select></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="承运商" :required="route.carrierType !== '自运'"><el-select :model-value="route.carrierType !== '自运' ? route.carrierContractId : route.carrierName" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="carrierOptionValue(item)" :label="carrierOptionLabel(item)" :value="route.carrierType !== '自运' ? carrierOptionValue(item) : item.carrierName" /></el-select></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="司机" :required="route.carrierType !== '承运商'"><el-autocomplete :ref="element => setDriverInput(route.segmentNo, element)" v-model="route.driverName" :debounce="300" :fetch-suggestions="fetchDriverSuggestions" clearable placeholder="请输入司机" :loading="driverLoading" @select="item => handleDriverSuggestionSelect(route, item)" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="司机" :required="route.carrierType !== '承运商'"><el-autocomplete :ref="element => setDriverInput(route.segmentNo, element)" v-model="route.driverName" :debounce="300" :fetch-suggestions="fetchDriverSuggestions" clearable placeholder="请输入司机" :loading="driverLoading" @select="item => handleDriverSuggestionSelect(route, item)" /></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item :label="route.carrierType === '承运商' ? '手机号' : '司机手机号'" :required="route.carrierType !== '承运商'"><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item :label="route.carrierType === '承运商' ? '手机号' : '司机手机号'" :required="route.carrierType !== '承运商'"><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="车牌号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="车牌号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
|
||||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="挂车车牌号" required><el-input v-model="route.trailerVehicleNo" placeholder="请输入" /></el-form-item></el-col>
|
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="挂车车牌号"><el-input v-model="route.trailerVehicleNo" placeholder="请输入" /></el-form-item></el-col>
|
||||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人" required><el-input v-model="route.escortName" placeholder="请输入" /></el-form-item></el-col>
|
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人"><el-autocomplete v-model="route.escortName" :debounce="300" :fetch-suggestions="fetchEscortSuggestions" clearable placeholder="请输入" :loading="escortLoading" @select="item => handleEscortSuggestionSelect(route, item)" /></el-form-item></el-col>
|
||||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人手机号" required><el-input v-model="route.escortPhone" placeholder="请输入" /></el-form-item></el-col>
|
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人手机号"><el-input v-model="route.escortPhone" placeholder="请输入" /></el-form-item></el-col>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-col v-if="route.carrierType === '承运商'" :span="6"><el-form-item label="承运商" required><el-select v-model="route.carrierContractId" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="item.contractId" :label="carrierOptionLabel(item)" :value="item.contractId" /></el-select></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="承运商" :required="route.carrierType !== '自运'"><el-select :model-value="route.carrierType !== '自运' ? route.carrierContractId : route.carrierName" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="carrierOptionValue(item)" :label="carrierOptionLabel(item)" :value="route.carrierType !== '自运' ? carrierOptionValue(item) : item.carrierName" /></el-select></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="船/航/班列号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="船/航/班列号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="船长" required><el-input v-model="route.captainName" placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="船长"><el-input v-model="route.captainName" placeholder="请输入" /></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="联系电话" required><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="联系电话"><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="箱号" required><el-input v-model="route.containerNo" placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="箱号"><el-input v-model="route.containerNo" placeholder="请输入" /></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="舱位" required><el-input v-model="route.cabinNo" placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="舱位"><el-input v-model="route.cabinNo" placeholder="请输入" /></el-form-item></el-col>
|
||||||
</template>
|
</template>
|
||||||
<el-col :span="6"><el-form-item label="里程(km)"><el-input :model-value="route.mileage" inputmode="numeric" maxlength="10" placeholder="请输入" @input="value => handleMileageInput(route, value)" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="里程(km)"><el-input :model-value="route.mileage" inputmode="numeric" maxlength="10" placeholder="请输入" @input="value => handleMileageInput(route, value)" /></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="备注"><el-input v-model="route.remark" maxlength="200" show-word-limit placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="备注"><el-input v-model="route.remark" maxlength="200" show-word-limit placeholder="请输入" /></el-form-item></el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<div class="freight-heading"><h3>运费信息</h3></div>
|
||||||
|
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form freight-form">
|
||||||
|
<el-row v-for="(group, index) in freightGroups(route)" :key="group.key" :gutter="16">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="单价">
|
||||||
|
<el-input :model-value="freightGroupUnitPrice(group)" inputmode="decimal" placeholder="请输入" @input="value => handleFreightGroupUnitPriceInput(group, value)" @clear="() => handleFreightGroupUnitPriceInput(group, '')">
|
||||||
|
<template #append><el-select :model-value="freightGroupPriceUnit(group)" class="freight-unit-select" @change="value => handleFreightGroupPriceUnitChange(group, value)"><el-option label="元/吨" value="元/吨" /><el-option label="元/件" value="元/件" /><el-option label="元/方" value="元/方" /></el-select></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="数量">
|
||||||
|
<el-input :model-value="formatQuantity(freightGroupQuantity(group))" readonly>
|
||||||
|
<template #append><el-select :model-value="group.quantityUnit" class="freight-unit-select" @change="value => handleFreightGroupQuantityUnitChange(route, group, value)"><el-option v-for="unit in quantityUnitOptions" :key="unit" :label="unit" :value="unit" /></el-select></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="运费">
|
||||||
|
<el-input :model-value="formatAmount(freightGroupAmount(group))" readonly><template #suffix>{{ currencyLabel(route.currency) }}</template></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row v-if="route.freightItems.length" :gutter="16">
|
||||||
|
<el-col :span="6"><el-form-item label="运费合计"><el-input :model-value="formatAmount(freightTotal(route))" readonly><template #suffix>{{ currencyLabel(route.currency) }}</template></el-input></el-form-item></el-col>
|
||||||
|
<el-col :span="6"><el-form-item label="其他费用合计"><el-input :model-value="route.otherFeeTotal" inputmode="decimal" placeholder="请输入" @input="value => handleOtherFeeTotalInput(route, value)" /></el-form-item></el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
<div class="segment-actions"><el-button plain @click="addToPending(route, true)">加入并继续调度</el-button><el-button plain @click="addToPending(route, false)">加入调度清单</el-button></div>
|
<div class="segment-actions"><el-button plain @click="addToPending(route, true)">加入并继续调度</el-button><el-button plain @click="addToPending(route, false)">加入调度清单</el-button></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -172,15 +172,15 @@
|
|||||||
<section class="pending-panel">
|
<section class="pending-panel">
|
||||||
<div class="pending-bar">
|
<div class="pending-bar">
|
||||||
<div class="pending-summary">
|
<div class="pending-summary">
|
||||||
<strong>待提交 <em>{{ pending.length }}</em> 条 <span class="pending-summary-text">({{ pendingSegmentSummary }})</span></strong>
|
<strong>待提交 <em>{{ pendingBatchCount }}</em> 条 <span class="pending-summary-text">({{ pendingSegmentSummary }})</span></strong>
|
||||||
<el-link type="primary" @click="pendingExpanded = !pendingExpanded">{{ pendingExpanded ? '收起待调度列表' : '展开待调度列表' }}</el-link>
|
<el-link type="primary" @click="pendingExpanded = !pendingExpanded">{{ pendingExpanded ? '收起待调度列表' : '展开待调度列表' }}</el-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="pending-actions"><el-button @click="$emit('back')">取消</el-button><el-button type="primary" :loading="submitting" @click="submit">确认调度</el-button></div>
|
<div class="pending-actions"><el-button @click="$emit('back')">取消</el-button><el-button type="primary" :loading="submitting" @click="submit">确认调度</el-button></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="pendingExpanded" class="pending-list">
|
<div v-show="pendingExpanded" class="pending-list">
|
||||||
<template v-for="(items, segmentNo) in pendingGroups" :key="segmentNo">
|
<template v-for="(items, segmentNo) in pendingGroups" :key="segmentNo">
|
||||||
<h3>{{ segmentNo }}:{{ items[0].departureName }} → {{ items[0].arrivalName }}</h3>
|
<h3>{{ segmentNo }}:{{ dispatchLocationName(items[0], 'departure') }} → {{ dispatchLocationName(items[0], 'arrival') }}</h3>
|
||||||
<el-table :data="items" border><el-table-column type="index" label="序号" width="70" /><el-table-column label="单据类型" prop="documentType" width="100" /><el-table-column label="货物" min-width="220"><template #default="{ row }">{{ row.cargoName }}{{ row.cargoType ? `,${row.cargoType}` : '' }}</template></el-table-column><el-table-column label="本次数量" width="150"><template #default="{ row }">{{ formatQuantity(row.quantity) }} {{ row.quantityUnit }}</template></el-table-column><el-table-column prop="carrierName" label="承运商" min-width="180" /><el-table-column prop="driverName" label="司机" width="130" /><el-table-column prop="vehicleNo" label="车牌" width="130" /><el-table-column label="操作" width="120" fixed="right"><template #default="{ row }"><div class="table-actions"><el-link type="primary" @click="editPending(row)">编辑</el-link><el-link type="danger" @click="removePending(row.id)">删除</el-link></div></template></el-table-column></el-table>
|
<el-table :data="items" border><el-table-column type="index" label="序号" width="70" /><el-table-column label="货物" min-width="220"><template #default="{ row }">{{ pendingGoodsText(row) }}</template></el-table-column><el-table-column label="本次数量" min-width="150"><template #default="{ row }">{{ pendingQuantityText(row) }}</template></el-table-column><el-table-column prop="carrierName" label="承运商" min-width="180" /><el-table-column prop="driverName" label="司机" width="130" /><el-table-column prop="vehicleNo" label="车牌" width="130" /><el-table-column label="操作" width="120" fixed="right"><template #default="{ row }"><div class="table-actions"><el-link type="primary" @click="editPending(row)">编辑</el-link><el-link type="danger" @click="removePendingBatch(row)">删除</el-link></div></template></el-table-column></el-table>
|
||||||
</template>
|
</template>
|
||||||
<el-empty v-if="!pending.length" description="暂无待提交调度条目" :image-size="60" />
|
<el-empty v-if="!pending.length" description="暂无待提交调度条目" :image-size="60" />
|
||||||
</div>
|
</div>
|
||||||
@@ -208,21 +208,37 @@ export default {
|
|||||||
props: { id: [String, Number] },
|
props: { id: [String, Number] },
|
||||||
emits: ['back'],
|
emits: ['back'],
|
||||||
data() {
|
data() {
|
||||||
return { loading: false, submitting: false, master: null, routes: [], pending: [], pendingExpanded: false, editingId: null, driverInputs: {}, carrierOptions: [], driverOptions: [], carrierLoading: false, driverLoading: false, cargoTypeOptions: [], cargoTypeCascaderProps: { label: 'cargoName', value: 'id', children: 'children', emitPath: true }, quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'] };
|
return { loading: false, submitting: false, master: null, routes: [], pending: [], pendingExpanded: false, editingId: null, driverInputs: {}, carrierOptions: [], driverOptions: [], carrierLoading: false, driverLoading: false, escortLoading: false, cargoTypeOptions: [], cargoTypeCascaderProps: { label: 'cargoName', value: 'id', children: 'children', emitPath: true }, quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'] };
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
totalQuantity() { return (this.master?.goods || []).reduce((sum, item) => sum + Number(item.quantity || 0), 0); },
|
totalQuantity() { return (this.master?.goods || []).reduce((sum, item) => sum + Number(item.quantity || 0), 0); },
|
||||||
|
quantityUnitLabel() { return this.master?.goods?.[0]?.quantityUnit || '吨'; },
|
||||||
goodsNames() { return (this.master?.goods || []).map(item => item.cargoName).filter(Boolean).join('、') || '-'; },
|
goodsNames() { return (this.master?.goods || []).map(item => item.cargoName).filter(Boolean).join('、') || '-'; },
|
||||||
goodsTypes() { return [...new Set((this.master?.goods || []).map(item => item.cargoType).filter(Boolean))].join('、') || '-'; },
|
goodsTypes() { return [...new Set((this.master?.goods || []).map(item => item.cargoType).filter(Boolean))].join('、') || '-'; },
|
||||||
masterGoods() { return (this.master?.goods || []).map((item, sourceIndex) => ({ ...item, sourceIndex, label: [item.cargoName, item.cargoType].filter(Boolean).join(' / ') })); },
|
masterGoods() { return (this.master?.goods || []).map((item, sourceIndex) => ({ ...item, sourceIndex, label: [item.cargoName, item.cargoType].filter(Boolean).join(' / ') })); },
|
||||||
dateRange() { return this.master?.planStartTime && this.master?.planEndTime ? `${String(this.master.planStartTime).slice(0, 10)} ~ ${String(this.master.planEndTime).slice(0, 10)}` : '-'; },
|
dateRange() { return this.master?.planStartTime && this.master?.planEndTime ? `${String(this.master.planStartTime).slice(0, 10)} ~ ${String(this.master.planEndTime).slice(0, 10)}` : '-'; },
|
||||||
pendingGroups() { return this.pending.reduce((groups, item) => { (groups[item.segmentNo] ||= []).push(item); return groups; }, {}); },
|
pendingGroups() {
|
||||||
|
return this.pending.reduce((groups, item, index) => {
|
||||||
|
const segmentRows = (groups[item.segmentNo] ||= []);
|
||||||
|
const batchKey = item.batchNo || item.id || `${item.segmentNo}-${index}`;
|
||||||
|
let batch = segmentRows.find(row => row.batchKey === batchKey);
|
||||||
|
if (!batch) {
|
||||||
|
batch = { ...item, batchKey, items: [] };
|
||||||
|
segmentRows.push(batch);
|
||||||
|
}
|
||||||
|
batch.items.push(item);
|
||||||
|
return groups;
|
||||||
|
}, {});
|
||||||
|
},
|
||||||
|
pendingBatchCount() {
|
||||||
|
return Object.values(this.pendingGroups).reduce((count, rows) => count + rows.length, 0);
|
||||||
|
},
|
||||||
pendingSegmentSummary() {
|
pendingSegmentSummary() {
|
||||||
return this.routes.map(route => {
|
return this.routes.map(route => {
|
||||||
const items = this.pending.filter(item => item.segmentNo === route.segmentNo);
|
const batches = this.pendingGroups[route.segmentNo] || [];
|
||||||
if (!items.length) return `${route.segmentNo}:无`;
|
if (!batches.length) return `${route.segmentNo}:无`;
|
||||||
const documentSummary = items.reduce((result, item) => {
|
const documentSummary = batches.reduce((result, batch) => {
|
||||||
result[item.documentType] = (result[item.documentType] || 0) + 1;
|
result[batch.documentType] = (result[batch.documentType] || 0) + 1;
|
||||||
return result;
|
return result;
|
||||||
}, {});
|
}, {});
|
||||||
return `${route.segmentNo}:${Object.entries(documentSummary).map(([type, count]) => `${type} x ${count}`).join('、')}`;
|
return `${route.segmentNo}:${Object.entries(documentSummary).map(([type, count]) => `${type} x ${count}`).join('、')}`;
|
||||||
@@ -231,6 +247,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await Promise.all([this.load(), this.loadCarrierOptions(), this.loadDriverOptions()]);
|
await Promise.all([this.load(), this.loadCarrierOptions(), this.loadDriverOptions()]);
|
||||||
|
this.applySelfOperatedCarrierDefaults();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async load() {
|
async load() {
|
||||||
@@ -238,10 +255,36 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const res = await api.getDetail(this.id);
|
const res = await api.getDetail(this.id);
|
||||||
this.master = res.data?.data || res.data || res;
|
this.master = res.data?.data || res.data || res;
|
||||||
|
await this.loadContractCurrency();
|
||||||
this.cargoTypeOptions = this.buildMasterCargoTypeOptions(this.master.goods || []);
|
this.cargoTypeOptions = this.buildMasterCargoTypeOptions(this.master.goods || []);
|
||||||
this.routes = this.dispatchRouteNodes().map((node, index) => this.createRoute(node, index));
|
this.routes = this.dispatchRouteNodes().map((node, index) => this.createRoute(node, index));
|
||||||
} finally { this.loading = false; }
|
} finally { this.loading = false; }
|
||||||
},
|
},
|
||||||
|
normalizeCurrencyValue(value) {
|
||||||
|
const raw = typeof value === 'object'
|
||||||
|
? value.dictKey || value.value || value.code || value.dictValue || ''
|
||||||
|
: value;
|
||||||
|
const normalized = String(raw || 'RMB').trim().toUpperCase();
|
||||||
|
return normalized.includes(' - ') ? normalized.split(' - ')[0].trim() : normalized;
|
||||||
|
},
|
||||||
|
async loadContractCurrency() {
|
||||||
|
const directCurrency = this.master?.settlementCurrency || this.master?.settlementCurrencyCode || this.master?.currency;
|
||||||
|
if (!this.master?.projectId || !this.master?.contractId) {
|
||||||
|
this.master.settlementCurrency = this.normalizeCurrencyValue(directCurrency || 'RMB');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await api.getProjectContracts(this.master.projectId);
|
||||||
|
const contract = unwrapRecords(response).find(item => String(item.id) === String(this.master.contractId));
|
||||||
|
this.master.customerContractPartyB = contract?.partyB || this.master.customerContractPartyB || '';
|
||||||
|
this.master.settlementCurrency = this.normalizeCurrencyValue(
|
||||||
|
directCurrency || contract?.settlementCurrency || contract?.settlementCurrencyCode || contract?.currency || 'RMB'
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
this.master.customerContractPartyB = '';
|
||||||
|
this.master.settlementCurrency = 'RMB';
|
||||||
|
}
|
||||||
|
},
|
||||||
dispatchRouteNodes() {
|
dispatchRouteNodes() {
|
||||||
const routes = Array.isArray(this.master?.routes) ? this.master.routes : [];
|
const routes = Array.isArray(this.master?.routes) ? this.master.routes : [];
|
||||||
if (routes.length) return routes;
|
if (routes.length) return routes;
|
||||||
@@ -261,13 +304,15 @@ export default {
|
|||||||
...node,
|
...node,
|
||||||
selected: false,
|
selected: false,
|
||||||
documentType: '运单',
|
documentType: '运单',
|
||||||
carrierType: '承运商',
|
carrierType: node.carrierType || this.master?.carrierType || '承运商',
|
||||||
carrierContractId: '',
|
carrierContractId: '',
|
||||||
currency: 'CNY',
|
currency: this.normalizeCurrencyValue(this.master?.settlementCurrency || 'RMB'),
|
||||||
otherFeeTotal: '',
|
otherFeeTotal: '',
|
||||||
freightItems: [],
|
freightItems: [],
|
||||||
departureName: previous.departureName || '', departureAddress: previous.departureAddress || '', departureContact: previous.departureContact || '', departurePhone: previous.departurePhone || '',
|
departureName: previous.departureName || '', departureAddress: previous.departureAddress || '', departureContact: previous.departureContact || '', departurePhone: previous.departurePhone || '',
|
||||||
|
departureCityName: previous.departureCityName || previous.arrivalCityName || '', departureDistrictName: previous.departureDistrictName || previous.arrivalDistrictName || '', departureSiteCode: previous.departureSiteCode || previous.arrivalSiteCode || '',
|
||||||
arrivalName: node.departureName || this.master.arrivalName || '', arrivalAddress: node.departureAddress || this.master.arrivalAddress || '', arrivalContact: node.departureContact || '', arrivalPhone: node.departurePhone || '',
|
arrivalName: node.departureName || this.master.arrivalName || '', arrivalAddress: node.departureAddress || this.master.arrivalAddress || '', arrivalContact: node.departureContact || '', arrivalPhone: node.departurePhone || '',
|
||||||
|
arrivalCityName: node.departureCityName || node.arrivalCityName || this.master.arrivalCityName || '', arrivalDistrictName: node.departureDistrictName || node.arrivalDistrictName || this.master.arrivalDistrictName || '', arrivalSiteCode: node.departureSiteCode || node.arrivalSiteCode || this.master.arrivalSiteCode || '',
|
||||||
goods: (this.master.goods || []).map((goods, sourceIndex) => this.createGoodsRow(goods, sourceIndex)),
|
goods: (this.master.goods || []).map((goods, sourceIndex) => this.createGoodsRow(goods, sourceIndex)),
|
||||||
};
|
};
|
||||||
this.syncFreightItems(route);
|
this.syncFreightItems(route);
|
||||||
@@ -315,7 +360,56 @@ export default {
|
|||||||
else Object.assign(row, { sourceIndex: undefined, cargoName: '', quantity: '', quantityUnit: '' });
|
else Object.assign(row, { sourceIndex: undefined, cargoName: '', quantity: '', quantityUnit: '' });
|
||||||
this.syncFreightItems(route);
|
this.syncFreightItems(route);
|
||||||
},
|
},
|
||||||
isRoad(route) { const type = String(route.transportType || '').toLowerCase(); return type.includes('公路') || type === 'road'; },
|
isRoad(route) {
|
||||||
|
const values = [
|
||||||
|
route.transportTypeName,
|
||||||
|
route.transportTypeLabel,
|
||||||
|
route.transportModeName,
|
||||||
|
route.transportMode,
|
||||||
|
route.transportType,
|
||||||
|
];
|
||||||
|
const type = values
|
||||||
|
.map(value => {
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return value.label || value.name || value.dictValue || value.value || value.code || '';
|
||||||
|
}
|
||||||
|
return value || '';
|
||||||
|
})
|
||||||
|
.join(' ')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
return type.includes('公路') || type.includes('道路') || type.includes('陆运') || type.includes('汽车') || type === 'road';
|
||||||
|
},
|
||||||
|
formatRoadLocation(value) {
|
||||||
|
const text = String(value || '').replace(/\s+/g, '');
|
||||||
|
if (!text) return '';
|
||||||
|
const withoutProvince = text.replace(/^.*?(?:省|自治区|特别行政区)/, '');
|
||||||
|
const cityMatch = withoutProvince.match(/.+?(?:市|州(?!市)|盟(?!市))/);
|
||||||
|
if (!cityMatch) return text;
|
||||||
|
const city = cityMatch[0];
|
||||||
|
const districtSource = withoutProvince.slice(city.length);
|
||||||
|
const districtMatch = districtSource.match(/^[^市州盟]*?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/);
|
||||||
|
if (!districtMatch) return city;
|
||||||
|
const district = `${districtMatch[0]}${districtSource.slice(districtMatch[0].length).startsWith('区') ? '区' : ''}`;
|
||||||
|
return `${city} ${district}`;
|
||||||
|
},
|
||||||
|
dispatchLocationName(route = {}, field) {
|
||||||
|
const name = String(route[`${field}Name`] || '').trim();
|
||||||
|
const address = String(route[`${field}Address`] || '').trim();
|
||||||
|
const formattedName = this.formatRoadLocation(name);
|
||||||
|
if (/(?:市|州|盟)/.test(formattedName)) return formattedName;
|
||||||
|
const regionalName = /(?:省|自治区|特别行政区|市|州|盟|区|县|旗)/.test(name);
|
||||||
|
if (name && !regionalName) return name;
|
||||||
|
const city = String(route[`${field}CityName`] || '').trim();
|
||||||
|
const district = String(route[`${field}DistrictName`] || '').trim();
|
||||||
|
const formattedAddress = this.formatRoadLocation(address);
|
||||||
|
if (/(?:市|州|盟)/.test(formattedAddress)) return formattedAddress;
|
||||||
|
return [city, district].filter(Boolean).join(' ') || formattedName || formattedAddress || '-';
|
||||||
|
},
|
||||||
|
isWater(route) {
|
||||||
|
const type = String(route.transportType || '').trim().toLowerCase();
|
||||||
|
return type === 'river' || type === 'water' || type === 'sl' || type.includes('水路') || type.includes('水运');
|
||||||
|
},
|
||||||
dispatchedQuantity(route) {
|
dispatchedQuantity(route) {
|
||||||
return this.quantityNumber(route.dispatchedQuantity) + this.pendingSegmentQuantity(route);
|
return this.quantityNumber(route.dispatchedQuantity) + this.pendingSegmentQuantity(route);
|
||||||
},
|
},
|
||||||
@@ -330,6 +424,42 @@ export default {
|
|||||||
formatAmount(value) { return Number(value || 0).toFixed(2); },
|
formatAmount(value) { return Number(value || 0).toFixed(2); },
|
||||||
dispatchWeight(route) { return (route.goods || []).reduce((sum, item) => sum + Number(item.dispatchQuantity || 0), 0); },
|
dispatchWeight(route) { return (route.goods || []).reduce((sum, item) => sum + Number(item.dispatchQuantity || 0), 0); },
|
||||||
freightAmount(item = {}) { return Number(item.unitPrice || 0) * Number(item.quantity || 0); },
|
freightAmount(item = {}) { return Number(item.unitPrice || 0) * Number(item.quantity || 0); },
|
||||||
|
freightGroups(route = {}) {
|
||||||
|
const groups = [];
|
||||||
|
const groupMap = new Map();
|
||||||
|
(route.freightItems || []).forEach((item, index) => {
|
||||||
|
const quantityUnit = String(item.quantityUnit || '').trim();
|
||||||
|
const key = quantityUnit || `__empty_${index}`;
|
||||||
|
let group = groupMap.get(key);
|
||||||
|
if (!group) {
|
||||||
|
group = { key, quantityUnit, rows: [] };
|
||||||
|
groupMap.set(key, group);
|
||||||
|
groups.push(group);
|
||||||
|
}
|
||||||
|
group.rows.push(item);
|
||||||
|
});
|
||||||
|
return groups;
|
||||||
|
},
|
||||||
|
freightGroupQuantity(group = {}) { return (group.rows || []).reduce((sum, item) => sum + Number(item.quantity || 0), 0); },
|
||||||
|
freightGroupUnitPrice(group = {}) {
|
||||||
|
const prices = (group.rows || []).map(item => String(item.unitPrice ?? '').trim()).filter(Boolean);
|
||||||
|
return !prices.length || prices.some(price => price !== prices[0]) ? '' : prices[0];
|
||||||
|
},
|
||||||
|
freightGroupPriceUnit(group = {}) { return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : ''); },
|
||||||
|
freightGroupAmount(group = {}) { return (group.rows || []).reduce((sum, item) => sum + this.freightAmount(item), 0); },
|
||||||
|
handleFreightGroupUnitPriceInput(group, value) {
|
||||||
|
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||||
|
const normalized = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||||
|
(group.rows || []).forEach(item => { item.unitPrice = normalized; });
|
||||||
|
},
|
||||||
|
handleFreightGroupPriceUnitChange(group, value) { (group.rows || []).forEach(item => { item.priceUnit = value || ''; }); },
|
||||||
|
handleFreightGroupQuantityUnitChange(route, group, value) {
|
||||||
|
(group.rows || []).forEach(item => {
|
||||||
|
item.quantityUnit = value || '';
|
||||||
|
const goods = (route.goods || []).find(row => String(row.sourceIndex) === String(item.sourceIndex));
|
||||||
|
if (goods) goods.quantityUnit = value || '';
|
||||||
|
});
|
||||||
|
},
|
||||||
freightTotal(route) {
|
freightTotal(route) {
|
||||||
const goodsFreight = (route.freightItems || []).reduce(
|
const goodsFreight = (route.freightItems || []).reduce(
|
||||||
(sum, item) => sum + this.freightAmount(item),
|
(sum, item) => sum + this.freightAmount(item),
|
||||||
@@ -339,7 +469,7 @@ export default {
|
|||||||
},
|
},
|
||||||
buildFreightJson(route) {
|
buildFreightJson(route) {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
currency: route.currency || 'CNY',
|
currency: route.currency || 'RMB',
|
||||||
totalFreightAmount: this.freightTotal(route),
|
totalFreightAmount: this.freightTotal(route),
|
||||||
otherFreightAmount: route.otherFeeTotal || '',
|
otherFreightAmount: route.otherFeeTotal || '',
|
||||||
freightItems: (route.freightItems || []).map(item => ({
|
freightItems: (route.freightItems || []).map(item => ({
|
||||||
@@ -348,7 +478,7 @@ export default {
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
currencyLabel(value) { return ({ CNY: '人民币', USD: '美元', EUR: '欧元' })[value] || value || '人民币'; },
|
currencyLabel(value) { return ({ CNY: '人民币', RMB: '人民币', USD: '美元', EUR: '欧元' })[value] || value || '人民币'; },
|
||||||
quantityNumber(value) { return Number(value || 0); },
|
quantityNumber(value) { return Number(value || 0); },
|
||||||
goodsKey(goods = {}, includeUnit = true) {
|
goodsKey(goods = {}, includeUnit = true) {
|
||||||
return [
|
return [
|
||||||
@@ -392,6 +522,26 @@ export default {
|
|||||||
return sum + this.quantityNumber(item.quantity);
|
return sum + this.quantityNumber(item.quantity);
|
||||||
}, 0);
|
}, 0);
|
||||||
},
|
},
|
||||||
|
validatePreviousSegmentCompletedQuantity(route, goods) {
|
||||||
|
const routeIndex = this.routes.indexOf(route);
|
||||||
|
if (routeIndex <= 0) return true;
|
||||||
|
const previousCompletedQuantity = this.quantityNumber(
|
||||||
|
this.routes[routeIndex - 1]?.arrivedQuantity
|
||||||
|
);
|
||||||
|
const currentDispatchQuantity = (goods || []).reduce(
|
||||||
|
(sum, item) => sum + this.quantityNumber(item.dispatchQuantity),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const dispatchTotal =
|
||||||
|
this.quantityNumber(route.dispatchedQuantity) +
|
||||||
|
this.pendingSegmentQuantity(route) +
|
||||||
|
currentDispatchQuantity;
|
||||||
|
if (dispatchTotal <= previousCompletedQuantity) return true;
|
||||||
|
this.$message.warning(
|
||||||
|
`${route.segmentNo}调度总量不能大于上一段已完成数量(${this.formatQuantity(previousCompletedQuantity)} ${this.quantityUnitLabel})`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
pendingGoodsQuantity(route, row) {
|
pendingGoodsQuantity(route, row) {
|
||||||
const goodsIndex = this.masterGoodsIndex(row);
|
const goodsIndex = this.masterGoodsIndex(row);
|
||||||
return this.pending.reduce((sum, item) => {
|
return this.pending.reduce((sum, item) => {
|
||||||
@@ -463,6 +613,23 @@ export default {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
validateDispatchGoods(route, goods) {
|
||||||
|
for (const item of goods) {
|
||||||
|
const index = (route.goods || []).indexOf(item) + 1;
|
||||||
|
const fields = [
|
||||||
|
[!String(item.cargoName || '').trim() || item.sourceIndex === undefined || item.sourceIndex === null || item.sourceIndex === '', '货物名称'],
|
||||||
|
[!String(item.cargoType || '').trim() && !(item.cargoTypePath || []).length, '货物类型'],
|
||||||
|
[item.dispatchQuantity === undefined || item.dispatchQuantity === null || item.dispatchQuantity === '' || Number(item.dispatchQuantity) <= 0, '本次数量'],
|
||||||
|
[!String(item.quantityUnit || '').trim(), '数量单位'],
|
||||||
|
];
|
||||||
|
const emptyField = fields.find(([isEmpty]) => isEmpty);
|
||||||
|
if (emptyField) {
|
||||||
|
this.$message.warning(`第${index}条货物的${emptyField[1]}不能为空`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
handleFreightQuantityUnitChange(route, freightItem, value) {
|
handleFreightQuantityUnitChange(route, freightItem, value) {
|
||||||
const goods = (route.goods || []).find(item => String(item.sourceIndex) === String(freightItem.sourceIndex));
|
const goods = (route.goods || []).find(item => String(item.sourceIndex) === String(freightItem.sourceIndex));
|
||||||
if (goods) goods.quantityUnit = value;
|
if (goods) goods.quantityUnit = value;
|
||||||
@@ -522,23 +689,32 @@ export default {
|
|||||||
},
|
},
|
||||||
handleCarrierTypeChange(route, value) {
|
handleCarrierTypeChange(route, value) {
|
||||||
route.carrierType = value;
|
route.carrierType = value;
|
||||||
|
route.carrierName = '';
|
||||||
|
route.carrierId = '';
|
||||||
|
route.carrierContractId = '';
|
||||||
if (value === '承运商') {
|
if (value === '承运商') {
|
||||||
route.trailerVehicleNo = '';
|
route.trailerVehicleNo = '';
|
||||||
route.escortName = '';
|
route.escortName = '';
|
||||||
route.escortPhone = '';
|
route.escortPhone = '';
|
||||||
}
|
}
|
||||||
if (value !== '承运商') {
|
this.loadCarrierOptions().then(() => {
|
||||||
route.carrierContractId = '';
|
if (value === '自运') this.applySelfOperatedCarrierDefaults(route);
|
||||||
route.carrierName = '';
|
});
|
||||||
}
|
|
||||||
},
|
},
|
||||||
handleCarrierContractChange(route, contractId) {
|
handleCarrierContractChange(route, contractId) {
|
||||||
const contract = this.carrierOptions.find(
|
const contract = this.carrierOptions.find(
|
||||||
item => String(item.contractId) === String(contractId)
|
item =>
|
||||||
|
[item.contractId, item.carrierName, item.id].some(
|
||||||
|
value => String(value || '') === String(contractId || '')
|
||||||
|
)
|
||||||
);
|
);
|
||||||
route.carrierContractId = contract?.contractId || '';
|
route.carrierContractId = route.carrierType !== '自运' ? contract?.contractId || '' : '';
|
||||||
|
route.carrierId = route.carrierType !== '自运' ? contract?.carrierId || '' : '';
|
||||||
route.carrierName = contract?.carrierName || '';
|
route.carrierName = contract?.carrierName || '';
|
||||||
},
|
},
|
||||||
|
carrierOptionValue(item = {}) {
|
||||||
|
return item.contractId || item.id || item.value || item.carrierName || '';
|
||||||
|
},
|
||||||
carrierOptionLabel(item) {
|
carrierOptionLabel(item) {
|
||||||
const duplicateCount = this.carrierOptions.filter(option => option.carrierName === item.carrierName).length;
|
const duplicateCount = this.carrierOptions.filter(option => option.carrierName === item.carrierName).length;
|
||||||
return duplicateCount > 1
|
return duplicateCount > 1
|
||||||
@@ -559,15 +735,59 @@ export default {
|
|||||||
async loadCarrierOptions() {
|
async loadCarrierOptions() {
|
||||||
this.carrierLoading = true;
|
this.carrierLoading = true;
|
||||||
try {
|
try {
|
||||||
this.carrierOptions = unwrapRecords(await api.getCarriers(this.id));
|
this.carrierOptions = unwrapRecords(await api.getCarriers(this.id))
|
||||||
|
.map(item => ({
|
||||||
|
...item,
|
||||||
|
contractId: item.contractId || item.carrierContractId || item.id || '',
|
||||||
|
carrierName:
|
||||||
|
item.partyB ||
|
||||||
|
item.partyBName ||
|
||||||
|
item.contractPartyB ||
|
||||||
|
item.customerContractPartyB ||
|
||||||
|
item.carrierName ||
|
||||||
|
'',
|
||||||
|
carrierId:
|
||||||
|
item.partyBId ||
|
||||||
|
item.partyBUserId ||
|
||||||
|
item.contractPartyBId ||
|
||||||
|
item.customerContractPartyBId ||
|
||||||
|
item.carrierId ||
|
||||||
|
'',
|
||||||
|
}))
|
||||||
|
.filter(item => item.carrierName);
|
||||||
} finally {
|
} finally {
|
||||||
this.carrierLoading = false;
|
this.carrierLoading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
applySelfOperatedCarrierDefaults(targetRoute) {
|
||||||
|
const routes = targetRoute ? [targetRoute] : this.routes;
|
||||||
|
if (!this.carrierOptions.length) {
|
||||||
|
const carrierName = String(this.master?.customerContractPartyB || '').trim();
|
||||||
|
if (carrierName) {
|
||||||
|
this.carrierOptions = [{
|
||||||
|
id: `customer-contract-${carrierName}`,
|
||||||
|
contractId: `customer-contract-${carrierName}`,
|
||||||
|
carrierName,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
routes.forEach(route => {
|
||||||
|
if (route.carrierType !== '自运') return;
|
||||||
|
if (this.carrierOptions.length) {
|
||||||
|
const current = this.carrierOptions.find(
|
||||||
|
item => String(item.carrierName || '') === String(route.carrierName || '')
|
||||||
|
);
|
||||||
|
route.carrierName = current?.carrierName || this.carrierOptions[0].carrierName || '';
|
||||||
|
}
|
||||||
|
route.carrierContractId = '';
|
||||||
|
});
|
||||||
|
},
|
||||||
async loadDriverOptions(keyword = '') {
|
async loadDriverOptions(keyword = '') {
|
||||||
this.driverLoading = true;
|
this.driverLoading = true;
|
||||||
try {
|
try {
|
||||||
this.driverOptions = unwrapRecords(await getDriverList(1, 20, { driverName: keyword }));
|
this.driverOptions = unwrapRecords(
|
||||||
|
await getDriverList(1, 20, { driverName: keyword, posts: '司机' })
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
this.driverLoading = false;
|
this.driverLoading = false;
|
||||||
}
|
}
|
||||||
@@ -591,6 +811,28 @@ export default {
|
|||||||
const drivingVehicle = String(item.drivingVehicle || '').trim();
|
const drivingVehicle = String(item.drivingVehicle || '').trim();
|
||||||
if (drivingVehicle) route.vehicleNo = drivingVehicle;
|
if (drivingVehicle) route.vehicleNo = drivingVehicle;
|
||||||
},
|
},
|
||||||
|
fetchEscortSuggestions(queryString, callback) {
|
||||||
|
const keyword = String(queryString || '').trim();
|
||||||
|
this.escortLoading = true;
|
||||||
|
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '押运员' })
|
||||||
|
.then(res => {
|
||||||
|
callback(
|
||||||
|
unwrapRecords(res).map(item => ({
|
||||||
|
...item,
|
||||||
|
value: this.driverOptionLabel(item),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => callback([]))
|
||||||
|
.finally(() => {
|
||||||
|
this.escortLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleEscortSuggestionSelect(route, item = {}) {
|
||||||
|
route.escortName = this.driverOptionLabel(item);
|
||||||
|
const phone = item.mobile || item.driverPhone || item.phone || '';
|
||||||
|
if (phone) route.escortPhone = phone;
|
||||||
|
},
|
||||||
setDriverInput(segmentNo, element) {
|
setDriverInput(segmentNo, element) {
|
||||||
if (element) this.driverInputs[segmentNo] = element;
|
if (element) this.driverInputs[segmentNo] = element;
|
||||||
else delete this.driverInputs[segmentNo];
|
else delete this.driverInputs[segmentNo];
|
||||||
@@ -611,15 +853,17 @@ export default {
|
|||||||
addToPending(route, continueDispatch) {
|
addToPending(route, continueDispatch) {
|
||||||
const goods = route.goods.filter(item => Number(item.dispatchQuantity) > 0);
|
const goods = route.goods.filter(item => Number(item.dispatchQuantity) > 0);
|
||||||
if (!goods.length) return this.$message.warning('请填写本次数量');
|
if (!goods.length) return this.$message.warning('请填写本次数量');
|
||||||
|
if (!this.validateDispatchGoods(route, goods)) return;
|
||||||
if (goods.some(item => Number(item.dispatchQuantity) > this.availableDispatchQuantity(route, item))) return this.$message.warning('本次数量不能超过剩余数量');
|
if (goods.some(item => Number(item.dispatchQuantity) > this.availableDispatchQuantity(route, item))) return this.$message.warning('本次数量不能超过剩余数量');
|
||||||
|
if (!this.validatePreviousSegmentCompletedQuantity(route, goods)) return;
|
||||||
this.syncFreightItems(route);
|
this.syncFreightItems(route);
|
||||||
if (!this.validateFreightItems(route)) return;
|
if (!this.validateFreightItems(route)) return;
|
||||||
if (!this.validateRoutePhones(route)) return;
|
if (!this.validateRoutePhones(route)) return;
|
||||||
if (route.documentType === '运单') {
|
if (route.documentType === '运单') {
|
||||||
if (this.isRoad(route)) {
|
if (this.isRoad(route)) {
|
||||||
if (route.carrierType === '承运商' && (!route.carrierContractId || !route.carrierName || !route.vehicleNo)) return this.$message.warning('请选择承运商并填写车牌号');
|
if (route.carrierType === '承运商' && (!route.carrierContractId || !route.carrierName || !route.vehicleNo)) return this.$message.warning('请选择承运商并填写车牌号');
|
||||||
if (route.carrierType !== '承运商' && (!route.driverName || !route.driverPhone || !route.vehicleNo || !route.trailerVehicleNo || !route.escortName || !route.escortPhone)) return this.$message.warning('请补全自运或网货平台的车辆与人员信息');
|
if (route.carrierType !== '承运商' && (!route.driverName || !route.driverPhone || !route.vehicleNo)) return this.$message.warning('请补全自运的车辆与人员信息');
|
||||||
} else if (!route.vehicleNo || !route.captainName || !route.driverPhone || !route.containerNo || !route.cabinNo || (route.carrierType === '承运商' && (!route.carrierContractId || !route.carrierName))) {
|
} else if (!route.vehicleNo || (route.carrierType === '承运商' && (!route.carrierContractId || !route.carrierName))) {
|
||||||
return this.$message.warning('请补全非公路运输的承运信息');
|
return this.$message.warning('请补全非公路运输的承运信息');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -628,11 +872,11 @@ export default {
|
|||||||
id: `${route.segmentNo}-${item.cargoName}-${Date.now()}-${Math.random()}`,
|
id: `${route.segmentNo}-${item.cargoName}-${Date.now()}-${Math.random()}`,
|
||||||
batchNo,
|
batchNo,
|
||||||
segmentNo: route.segmentNo, relationNo: route.segmentNo, documentType: route.documentType, transportType: route.transportType, carrierType: route.carrierType,
|
segmentNo: route.segmentNo, relationNo: route.segmentNo, documentType: route.documentType, transportType: route.transportType, carrierType: route.carrierType,
|
||||||
carrierContractId: route.carrierContractId, carrierName: route.carrierName, driverName: route.driverName, driverPhone: route.driverPhone, vehicleNo: route.vehicleNo, captainName: route.captainName, cabinNo: route.cabinNo, containerNo: route.containerNo, trailerVehicleNo: route.trailerVehicleNo, escortName: route.escortName, escortPhone: route.escortPhone, mileage: this.normalizeMileage(route.mileage), unitPrice: this.freightItemsForGoods(route, item).unitPrice || '', priceUnit: this.freightItemsForGoods(route, item).priceUnit || '', currency: route.currency || 'CNY', freightAmount: this.freightAmount(this.freightItemsForGoods(route, item)), freightTotal: this.freightTotal(route), otherFeeTotal: route.otherFeeTotal || '', freightJson: this.buildFreightJson(route), remark: route.remark,
|
carrierContractId: route.carrierContractId, carrierName: route.carrierName, driverName: route.driverName, driverPhone: route.driverPhone, vehicleNo: route.vehicleNo, captainName: route.captainName, cabinNo: route.cabinNo, containerNo: route.containerNo, trailerVehicleNo: route.trailerVehicleNo, escortName: route.escortName, escortPhone: route.escortPhone, mileage: this.normalizeMileage(route.mileage), unitPrice: this.freightItemsForGoods(route, item).unitPrice || '', priceUnit: this.freightItemsForGoods(route, item).priceUnit || '', currency: route.currency || 'RMB', freightAmount: this.freightAmount(this.freightItemsForGoods(route, item)), freightTotal: this.freightTotal(route), otherFeeTotal: route.otherFeeTotal || '', freightJson: this.buildFreightJson(route), routeRemark: route.remark,
|
||||||
departureName: route.departureName, departureAddress: route.departureAddress, departureContact: route.departureContact, departurePhone: route.departurePhone,
|
departureName: route.departureName, departureAddress: route.departureAddress, departureContact: route.departureContact, departurePhone: route.departurePhone,
|
||||||
arrivalName: route.arrivalName, arrivalAddress: route.arrivalAddress, arrivalContact: route.arrivalContact, arrivalPhone: route.arrivalPhone,
|
arrivalName: route.arrivalName, arrivalAddress: route.arrivalAddress, arrivalContact: route.arrivalContact, arrivalPhone: route.arrivalPhone,
|
||||||
estimatedStartTime: route.estimatedStartTime, estimatedEndTime: route.estimatedEndTime,
|
estimatedStartTime: route.estimatedStartTime, estimatedEndTime: route.estimatedEndTime,
|
||||||
sourceIndex: this.masterGoodsIndex(item), cargoName: item.cargoName, cargoType: item.cargoType, quantity: item.dispatchQuantity, quantityUnit: item.quantityUnit, packageType: item.packageType, brand: item.brand, specification: item.specification, model: item.model, materialCode: item.materialCode,
|
sourceIndex: this.masterGoodsIndex(item), cargoName: item.cargoName, cargoType: item.cargoType, quantity: item.dispatchQuantity, quantityUnit: item.quantityUnit, packageType: item.packageType, brand: item.brand, specification: item.specification, model: item.model, materialCode: item.materialCode, deviceCode: item.deviceCode, remark: item.remark,
|
||||||
}));
|
}));
|
||||||
this.editingId = null;
|
this.editingId = null;
|
||||||
this.pendingExpanded = true;
|
this.pendingExpanded = true;
|
||||||
@@ -643,32 +887,59 @@ export default {
|
|||||||
}
|
}
|
||||||
this.$message.success('已加入调度清单');
|
this.$message.success('已加入调度清单');
|
||||||
},
|
},
|
||||||
editPending(item) {
|
pendingGoodsText(batch = {}) {
|
||||||
const route = this.routes.find(routeItem => routeItem.segmentNo === item.segmentNo);
|
return (batch.items || [batch])
|
||||||
|
.map(item => [item.cargoName, item.cargoType].filter(Boolean).join(','))
|
||||||
|
.join(' | ');
|
||||||
|
},
|
||||||
|
pendingQuantityText(batch = {}) {
|
||||||
|
return (batch.items || [batch])
|
||||||
|
.map(item => `${this.formatQuantity(item.quantity)} ${item.quantityUnit || ''}`.trim())
|
||||||
|
.join(' | ');
|
||||||
|
},
|
||||||
|
editPending(batch) {
|
||||||
|
const items = batch.items || [batch];
|
||||||
|
const first = items[0];
|
||||||
|
const route = this.routes.find(routeItem => routeItem.segmentNo === first.segmentNo);
|
||||||
if (!route) return;
|
if (!route) return;
|
||||||
const itemSourceIndex = Number(item.sourceIndex);
|
Object.assign(route, first);
|
||||||
const sourceIndex = Number.isInteger(itemSourceIndex) && itemSourceIndex >= 0
|
const editedGoods = items.map(item => {
|
||||||
? itemSourceIndex
|
const itemSourceIndex = Number(item.sourceIndex);
|
||||||
: (this.master.goods || []).findIndex(goods => goods.cargoName === item.cargoName && goods.cargoType === item.cargoType);
|
const sourceIndex = Number.isInteger(itemSourceIndex) && itemSourceIndex >= 0
|
||||||
let goods = route.goods.find(goodsItem => String(goodsItem.sourceIndex) === String(sourceIndex));
|
? itemSourceIndex
|
||||||
if (!goods && sourceIndex >= 0) {
|
: (this.master.goods || []).findIndex(goods => goods.cargoName === item.cargoName && goods.cargoType === item.cargoType);
|
||||||
goods = this.createGoodsRow(this.master.goods[sourceIndex], sourceIndex);
|
let goods = route.goods.find(goodsItem => String(goodsItem.sourceIndex) === String(sourceIndex));
|
||||||
route.goods.push(goods);
|
if (!goods && sourceIndex >= 0) {
|
||||||
}
|
goods = this.createGoodsRow(this.master.goods[sourceIndex], sourceIndex);
|
||||||
if (goods) goods.dispatchQuantity = item.quantity;
|
route.goods.push(goods);
|
||||||
Object.assign(route, item);
|
}
|
||||||
|
if (goods) goods.dispatchQuantity = item.quantity;
|
||||||
|
return { goods, item };
|
||||||
|
});
|
||||||
|
route.currency = this.normalizeCurrencyValue(
|
||||||
|
this.master?.settlementCurrency || route.currency || 'RMB'
|
||||||
|
);
|
||||||
this.syncFreightItems(route);
|
this.syncFreightItems(route);
|
||||||
const freightItem = this.freightItemsForGoods(route, goods);
|
editedGoods.forEach(({ goods, item }) => {
|
||||||
if (freightItem) { freightItem.unitPrice = item.unitPrice || ''; freightItem.priceUnit = item.priceUnit || freightItem.priceUnit; }
|
const freightItem = this.freightItemsForGoods(route, goods);
|
||||||
this.removePending(item.id);
|
if (freightItem) {
|
||||||
this.editingId = item.id;
|
freightItem.unitPrice = item.unitPrice || '';
|
||||||
|
freightItem.priceUnit = item.priceUnit || freightItem.priceUnit;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.removePendingBatch(batch);
|
||||||
|
this.editingId = batch.batchKey;
|
||||||
route.selected = true;
|
route.selected = true;
|
||||||
this.$nextTick(() => document.querySelector(`[data-segment="${item.segmentNo}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
|
this.$nextTick(() => document.querySelector(`[data-segment="${first.segmentNo}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
|
||||||
},
|
},
|
||||||
freightItemsForGoods(route, goods) {
|
freightItemsForGoods(route, goods) {
|
||||||
return (route.freightItems || []).find(item => String(item.sourceIndex) === String(goods?.sourceIndex)) || {};
|
return (route.freightItems || []).find(item => String(item.sourceIndex) === String(goods?.sourceIndex)) || {};
|
||||||
},
|
},
|
||||||
removePending(id) { this.pending = this.pending.filter(item => item.id !== id); if (this.editingId === id) this.editingId = null; },
|
removePendingBatch(batch = {}) {
|
||||||
|
const ids = new Set((batch.items || [batch]).map(item => item.id));
|
||||||
|
this.pending = this.pending.filter(item => !ids.has(item.id));
|
||||||
|
if (this.editingId === batch.batchKey) this.editingId = null;
|
||||||
|
},
|
||||||
validateRoutePhones(route = {}) {
|
validateRoutePhones(route = {}) {
|
||||||
const invalidPhone = [
|
const invalidPhone = [
|
||||||
[route.departurePhone, '发货联系方式'],
|
[route.departurePhone, '发货联系方式'],
|
||||||
@@ -715,6 +986,7 @@ export default {
|
|||||||
.goods-heading { display: flex; align-items: center; justify-content: space-between; margin: 0 -24px 12px; padding: 14px 24px; border-top: 1px solid #eff1f7; border-bottom: 1px solid #eff1f7; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } span { color: #909399; font-size: 13px; } }
|
.goods-heading { display: flex; align-items: center; justify-content: space-between; margin: 0 -24px 12px; padding: 14px 24px; border-top: 1px solid #eff1f7; border-bottom: 1px solid #eff1f7; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } span { color: #909399; font-size: 13px; } }
|
||||||
.freight-heading { margin: 16px 0 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
|
.freight-heading { margin: 16px 0 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
|
||||||
.goods-table { :deep(th.el-table__cell), :deep(td.el-table__cell) { border-color: #eff1f7; } :deep(.el-table__row--striped td.el-table__cell) { background: #fafafa; } :deep(.goods-table__remaining .cell) { white-space: nowrap; } :deep(.goods-table__dispatch .el-input) { width: 100%; min-width: 0; } :deep(.goods-table__cargo-type .el-input__inner), :deep(.goods-table__cargo-name .el-select__selected-item) { color: #303133; } }
|
.goods-table { :deep(th.el-table__cell), :deep(td.el-table__cell) { border-color: #eff1f7; } :deep(.el-table__row--striped td.el-table__cell) { background: #fafafa; } :deep(.goods-table__remaining .cell) { white-space: nowrap; } :deep(.goods-table__dispatch .el-input) { width: 100%; min-width: 0; } :deep(.goods-table__cargo-type .el-input__inner), :deep(.goods-table__cargo-name .el-select__selected-item) { color: #303133; } }
|
||||||
|
.goods-required-mark { margin-left: 2px; color: #f56c6c; }
|
||||||
.freight-form { :deep(.freight-unit-select) { width: 92px; flex: 0 0 92px; min-width: 0; } }
|
.freight-form { :deep(.freight-unit-select) { width: 92px; flex: 0 0 92px; min-width: 0; } }
|
||||||
.carrier-type-form { margin-top: 16px; }
|
.carrier-type-form { margin-top: 16px; }
|
||||||
.carrier-form { padding-top: 8px; }
|
.carrier-form { padding-top: 8px; }
|
||||||
|
|||||||
@@ -29,12 +29,12 @@
|
|||||||
:label="contract.contractName"
|
:label="contract.contractName"
|
||||||
:value="contract.id" /></el-select></el-form-item></el-col
|
:value="contract.id" /></el-select></el-form-item></el-col
|
||||||
><el-col :span="6"
|
><el-col :span="6"
|
||||||
><el-form-item label="总单号"
|
><el-form-item label="总单号" required
|
||||||
><el-input
|
><el-input
|
||||||
:model-value="form.masterNo || '系统自动生成'"
|
:model-value="form.masterNo || '系统自动生成'"
|
||||||
readonly /></el-form-item></el-col
|
readonly /></el-form-item></el-col
|
||||||
><el-col :span="6"
|
><el-col :span="6"
|
||||||
><el-form-item label="运输组织类型"
|
><el-form-item label="运输类型" required
|
||||||
><el-select v-model="form.transportOrganizationType" disabled
|
><el-select v-model="form.transportOrganizationType" disabled
|
||||||
><el-option label="多式联运" value="多式联运" /></el-select></el-form-item></el-col
|
><el-option label="多式联运" value="多式联运" /></el-select></el-form-item></el-col
|
||||||
><el-col :span="24"
|
><el-col :span="24"
|
||||||
@@ -54,75 +54,77 @@
|
|||||||
<el-link type="primary" @click="openRouteDialog">选择线路</el-link>
|
<el-link type="primary" @click="openRouteDialog">选择线路</el-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-form :model="form" label-position="right" label-width="auto"
|
<el-form class="route-info-form" :model="form" label-position="right" label-width="96px"
|
||||||
><div class="route-steps">
|
><div class="route-flow">
|
||||||
<el-steps direction="vertical" :active="form.routes.length + 1">
|
<div class="route-flow-row">
|
||||||
<el-step>
|
<span class="route-flow-marker route-flow-marker--start">起</span>
|
||||||
<template #icon><span class="route-step-icon route-step-icon--start">起</span></template>
|
<div class="route-flow-body">
|
||||||
<template #description>
|
<div class="route-node-form">
|
||||||
<div class="route-node-form">
|
<el-form-item label="发货地" required class="route-address-form-item"
|
||||||
<el-form-item label="发货地址" required class="route-address-form-item"
|
|
||||||
><div class="route-address-inputs"
|
><div class="route-address-inputs"
|
||||||
><el-cascader v-model="addressRegionPaths.departure" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('departure', value)" /><el-input v-model="form.departureAddress" class="route-address-detail" readonly placeholder="请输入详细地址" @click="openAddressMap('departure')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" @click="openCommonAddress('departure')" /></el-tooltip></div
|
><el-input v-if="isNonRoadAddress('departure')" v-model="form.departureName" class="route-address-name" readonly placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths.departure" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('departure', value)" /><el-input v-model="form.departureAddress" class="route-address-detail" readonly placeholder="请输入详细地址" @click="handleAddressDetailClick('departure')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" @click="openCommonAddress('departure')" /></el-tooltip></div
|
||||||
></el-form-item>
|
></el-form-item>
|
||||||
<el-form-item label="联系人"
|
<el-form-item label="联系人" class="route-contact-form-item"
|
||||||
><el-input v-model="form.departureContact" placeholder="请输入"
|
><el-input v-model="form.departureContact" placeholder="请输入"
|
||||||
/></el-form-item>
|
/></el-form-item>
|
||||||
<el-form-item label="联系方式"
|
<el-form-item label="联系方式" class="route-phone-form-item"
|
||||||
><el-input v-model="form.departurePhone" placeholder="请输入"
|
><el-input v-model="form.departurePhone" placeholder="请输入"
|
||||||
/></el-form-item>
|
/></el-form-item>
|
||||||
<el-form-item label="计划开始日期"
|
<el-form-item label="计划开始" class="route-date-form-item"
|
||||||
><el-date-picker
|
><el-date-picker
|
||||||
v-model="form.planStartTime"
|
v-model="form.planStartTime"
|
||||||
type="date"
|
type="date"
|
||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
placeholder="请选择"
|
placeholder="请选择"
|
||||||
style="width: 200px"
|
|
||||||
/></el-form-item>
|
/></el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</el-step>
|
</div>
|
||||||
<el-step v-for="(route, index) in form.routes" :key="index">
|
<template v-for="(route, index) in form.routes" :key="index">
|
||||||
<template #icon><span class="route-step-icon route-step-icon--middle">经</span></template>
|
<div class="route-flow-segment">
|
||||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ index + 1 }}</span><el-tooltip :content="getRouteTitle(index).replace(`段${index + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getRouteTitle(index), index + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式</span><el-select v-model="route.transportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template>
|
<div class="route-step-title"><span class="route-segment-label">{{ circledSegmentNumber(index + 1) }} 段{{ index + 1 }}</span><el-tooltip :content="getRouteTitle(index).replace(`段${index + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getRouteTitle(index), index + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式<span class="required-asterisk">*</span></span><el-select v-model="route.transportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div>
|
||||||
<template #description>
|
</div>
|
||||||
|
<div class="route-flow-row">
|
||||||
|
<span class="route-flow-marker route-flow-marker--middle">经</span>
|
||||||
|
<div class="route-flow-body">
|
||||||
<div class="route-node-form">
|
<div class="route-node-form">
|
||||||
<el-form-item label="途经地" required class="route-address-form-item"
|
<el-form-item label="途经地" required class="route-address-form-item"
|
||||||
><div class="route-address-inputs"
|
><div class="route-address-inputs"
|
||||||
><el-cascader v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!route.transportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="!route.transportType || !isRoadTransportType(route.transportType)" placeholder="请先选择运输方式" @click="openAddressMap(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="!route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
><el-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!route.transportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="!route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||||
></el-form-item>
|
></el-form-item>
|
||||||
<el-form-item label="联系人"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item>
|
<el-form-item label="联系人" class="route-contact-form-item"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item>
|
||||||
<el-form-item label="联系方式"><div class="route-contact-actions"><el-input v-model="route.departurePhone" placeholder="请输入" /><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></div></el-form-item>
|
<el-form-item label="联系方式" class="route-phone-form-item"><div class="route-contact-actions"><el-input v-model="route.departurePhone" placeholder="请输入" /><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></div></el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</el-step>
|
</div>
|
||||||
<el-step>
|
</template>
|
||||||
<template #icon><span class="route-step-icon route-step-icon--end">终</span></template>
|
<div class="route-flow-segment">
|
||||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ form.routes.length + 1 }}</span><el-tooltip :content="getFinalRouteTitle().replace(`段${form.routes.length + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getFinalRouteTitle(), form.routes.length + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式</span><el-select v-model="form.finalTransportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template>
|
<div class="route-step-title"><span class="route-segment-label">{{ circledSegmentNumber(form.routes.length + 1) }} 段{{ form.routes.length + 1 }}</span><el-tooltip :content="getFinalRouteTitle().replace(`段${form.routes.length + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getFinalRouteTitle(), form.routes.length + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式<span class="required-asterisk">*</span></span><el-select v-model="form.finalTransportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div>
|
||||||
<template #description>
|
</div>
|
||||||
<div class="route-node-form">
|
<div class="route-flow-row">
|
||||||
<el-form-item label="收货地址" required class="route-address-form-item"
|
<span class="route-flow-marker route-flow-marker--end">终</span>
|
||||||
|
<div class="route-flow-body">
|
||||||
|
<div class="route-node-form">
|
||||||
|
<el-form-item label="收货地" required class="route-address-form-item"
|
||||||
><div class="route-address-inputs"
|
><div class="route-address-inputs"
|
||||||
><el-cascader v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!form.finalTransportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="!form.finalTransportType || !isRoadTransportType(form.finalTransportType)" placeholder="请选择省市区" @click="openAddressMap('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="!form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
><el-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!form.finalTransportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="!form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||||
></el-form-item>
|
></el-form-item>
|
||||||
<el-form-item label="联系人"
|
<el-form-item label="联系人" class="route-contact-form-item"
|
||||||
><el-input v-model="form.arrivalContact" placeholder="请输入" /></el-form-item>
|
><el-input v-model="form.arrivalContact" placeholder="请输入" /></el-form-item>
|
||||||
<el-form-item label="联系方式"
|
<el-form-item label="联系方式" class="route-phone-form-item"
|
||||||
><el-input v-model="form.arrivalPhone" placeholder="请输入" /></el-form-item>
|
><el-input v-model="form.arrivalPhone" placeholder="请输入" /></el-form-item>
|
||||||
<el-form-item label="计划结束日期"
|
<el-form-item label="计划结束" class="route-date-form-item"
|
||||||
><el-date-picker
|
><el-date-picker
|
||||||
v-model="form.planEndTime"
|
v-model="form.planEndTime"
|
||||||
type="date"
|
type="date"
|
||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
placeholder="请选择"
|
placeholder="请选择"
|
||||||
style="width: 200px"
|
|
||||||
/></el-form-item>
|
/></el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</el-step>
|
</div>
|
||||||
</el-steps>
|
|
||||||
</div></el-form
|
</div></el-form
|
||||||
>
|
>
|
||||||
</section>
|
</section>
|
||||||
@@ -130,7 +132,7 @@
|
|||||||
<div class="goods-heading">
|
<div class="goods-heading">
|
||||||
<h3>货物信息</h3>
|
<h3>货物信息</h3>
|
||||||
<div class="goods-toolbar">
|
<div class="goods-toolbar">
|
||||||
<el-link type="primary" @click="cargoImportVisible = true">导入货物</el-link>
|
<!-- <el-link type="primary" @click="cargoImportVisible = true">导入货物</el-link> -->
|
||||||
<el-link type="primary" @click="openCommonCargoDialog">常用货物</el-link>
|
<el-link type="primary" @click="openCommonCargoDialog">常用货物</el-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,7 +159,8 @@
|
|||||||
</template></el-autocomplete
|
</template></el-autocomplete
|
||||||
></template></el-table-column
|
></template></el-table-column
|
||||||
><el-table-column label="货物类型" min-width="130"
|
><el-table-column label="货物类型" min-width="130"
|
||||||
><template #default="{ row }"
|
><template #header><span>货物类型<span class="required-asterisk">*</span></span></template>
|
||||||
|
<template #default="{ row }"
|
||||||
><el-cascader
|
><el-cascader
|
||||||
v-model="row.cargoTypePath"
|
v-model="row.cargoTypePath"
|
||||||
:options="cargoTypeOptions"
|
:options="cargoTypeOptions"
|
||||||
@@ -170,14 +173,16 @@
|
|||||||
@change="value => handleCargoTypeChange(row, value)"
|
@change="value => handleCargoTypeChange(row, value)"
|
||||||
/></template></el-table-column
|
/></template></el-table-column
|
||||||
><el-table-column label="数量" width="180" class-name="master-editor__quantity-cell"
|
><el-table-column label="数量" width="180" class-name="master-editor__quantity-cell"
|
||||||
><template #default="{ row }"
|
><template #header><span>数量<span class="required-asterisk">*</span></span></template>
|
||||||
|
<template #default="{ row }"
|
||||||
><el-input
|
><el-input
|
||||||
v-model="row.quantity"
|
v-model="row.quantity"
|
||||||
inputmode="decimal"
|
inputmode="decimal"
|
||||||
placeholder="请输入"
|
placeholder="请输入"
|
||||||
@input="value => handleQuantityInput(row, value)" /></template></el-table-column
|
@input="value => handleQuantityInput(row, value)" /></template></el-table-column
|
||||||
><el-table-column label="数量单位" width="120"
|
><el-table-column label="数量单位" width="120"
|
||||||
><template #default="{ row }"
|
><template #header><span>数量单位<span class="required-asterisk">*</span></span></template>
|
||||||
|
<template #default="{ row }"
|
||||||
><el-select v-model="row.quantityUnit"
|
><el-select v-model="row.quantityUnit"
|
||||||
><el-option v-for="unit in quantityUnitOptions" :key="unit" :label="unit" :value="unit" /></el-select></template></el-table-column
|
><el-option v-for="unit in quantityUnitOptions" :key="unit" :label="unit" :value="unit" /></el-select></template></el-table-column
|
||||||
><el-table-column label="包装" width="130"
|
><el-table-column label="包装" width="130"
|
||||||
@@ -194,6 +199,15 @@
|
|||||||
><el-table-column label="型号" min-width="120"
|
><el-table-column label="型号" min-width="120"
|
||||||
><template #default="{ row }"
|
><template #default="{ row }"
|
||||||
><el-input v-model="row.model" placeholder="请输入" /></template></el-table-column
|
><el-input v-model="row.model" placeholder="请输入" /></template></el-table-column
|
||||||
|
><el-table-column label="物料编码" min-width="140"
|
||||||
|
><template #default="{ row }"
|
||||||
|
><el-input v-model="row.materialCode" placeholder="请输入" /></template></el-table-column
|
||||||
|
><el-table-column label="设备编码" min-width="140"
|
||||||
|
><template #default="{ row }"
|
||||||
|
><el-input v-model="row.deviceCode" placeholder="请输入" /></template></el-table-column
|
||||||
|
><el-table-column label="备注" min-width="180"
|
||||||
|
><template #default="{ row }"
|
||||||
|
><el-input v-model="row.remark" maxlength="200" show-word-limit placeholder="请输入" /></template></el-table-column
|
||||||
><el-table-column label="操作" width="140"
|
><el-table-column label="操作" width="140"
|
||||||
><template #default="{ $index }"
|
><template #default="{ $index }"
|
||||||
><div class="goods-actions"
|
><div class="goods-actions"
|
||||||
@@ -214,9 +228,9 @@
|
|||||||
>
|
>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h3>附件</h3>
|
<div class="section-head">
|
||||||
<div class="master-editor__attachment">
|
<h3>附件</h3>
|
||||||
<div class="master-editor__attachment-head">
|
<div class="section-head__actions">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="!attachmentRows.length"
|
:disabled="!attachmentRows.length"
|
||||||
@@ -225,6 +239,8 @@
|
|||||||
批量下载
|
批量下载
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="master-editor__attachment">
|
||||||
<el-table
|
<el-table
|
||||||
:data="attachmentRows"
|
:data="attachmentRows"
|
||||||
border
|
border
|
||||||
@@ -240,6 +256,17 @@
|
|||||||
</el-link>
|
</el-link>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="附件描述" min-width="220" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input
|
||||||
|
v-model="row.description"
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入附件描述"
|
||||||
|
@input="syncAttachmentsJson"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="文件大小" width="120" align="center">
|
<el-table-column label="文件大小" width="120" align="center">
|
||||||
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
|
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -256,7 +283,7 @@
|
|||||||
<vehicle-attachment-upload
|
<vehicle-attachment-upload
|
||||||
v-model="attachmentRows"
|
v-model="attachmentRows"
|
||||||
:file-types="attachmentFileTypes"
|
:file-types="attachmentFileTypes"
|
||||||
:max-size="500"
|
:max-size="50"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
button-text="上传附件"
|
button-text="上传附件"
|
||||||
@change="handleAttachmentChange"
|
@change="handleAttachmentChange"
|
||||||
@@ -374,7 +401,10 @@
|
|||||||
>搜索</el-button
|
>搜索</el-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div ref="addressMap" class="address-map" />
|
<div class="map-picker-content">
|
||||||
|
<map-search-results :results="mapSearchResults" @select="selectAddressMapSearchResult" />
|
||||||
|
<div ref="addressMap" class="address-map" />
|
||||||
|
</div>
|
||||||
<div class="address-map-status">
|
<div class="address-map-status">
|
||||||
{{ mapStatus
|
{{ mapStatus
|
||||||
}}<span v-if="mapSelected.regionName"> 行政区划:{{ mapSelected.regionName }}</span>
|
}}<span v-if="mapSelected.regionName"> 行政区划:{{ mapSelected.regionName }}</span>
|
||||||
@@ -612,6 +642,7 @@ export default {
|
|||||||
mapKeyword: '',
|
mapKeyword: '',
|
||||||
mapStatus: '可搜索地址或点击地图选点',
|
mapStatus: '可搜索地址或点击地图选点',
|
||||||
mapSelected: {},
|
mapSelected: {},
|
||||||
|
mapSearchResults: [],
|
||||||
addressMapInstance: null,
|
addressMapInstance: null,
|
||||||
addressMapMarker: null,
|
addressMapMarker: null,
|
||||||
addressMapGeocoder: null,
|
addressMapGeocoder: null,
|
||||||
@@ -717,6 +748,11 @@ export default {
|
|||||||
responseData(response) {
|
responseData(response) {
|
||||||
return response?.data?.data || response?.data || response || {};
|
return response?.data?.data || response?.data || response || {};
|
||||||
},
|
},
|
||||||
|
circledSegmentNumber(number) {
|
||||||
|
const value = Number(number);
|
||||||
|
const circledNumbers = '①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳';
|
||||||
|
return circledNumbers[value - 1] || String(value);
|
||||||
|
},
|
||||||
async loadRegionOptions() {
|
async loadRegionOptions() {
|
||||||
if (this.regionOptions.length) return this.regionOptions;
|
if (this.regionOptions.length) return this.regionOptions;
|
||||||
if (this.regionRequest) return this.regionRequest;
|
if (this.regionRequest) return this.regionRequest;
|
||||||
@@ -767,9 +803,19 @@ export default {
|
|||||||
},
|
},
|
||||||
extractChinaRegionOptions(regions = []) {
|
extractChinaRegionOptions(regions = []) {
|
||||||
const china = regions.find(item => ['中国', '中华人民共和国'].includes(item.title));
|
const china = regions.find(item => ['中国', '中华人民共和国'].includes(item.title));
|
||||||
if (china?.children?.length) return china.children;
|
const chinaChildren = china?.children || [];
|
||||||
if (regions.length === 1 && regions[0].children?.length) return regions[0].children;
|
// 部分行政区划数据的省级节点 parentId 为 0,另一部分挂在“中国(+86)”下,需合并两类根节点。
|
||||||
return regions;
|
const rootRegions = regions.filter(
|
||||||
|
item => item !== china && ['0', '', 'null', 'undefined'].includes(String(item.parentId ?? ''))
|
||||||
|
);
|
||||||
|
const merged = [...chinaChildren, ...rootRegions];
|
||||||
|
const seen = new Set();
|
||||||
|
return merged.filter(item => {
|
||||||
|
const key = String(item.id ?? item.value ?? item.title ?? '');
|
||||||
|
if (seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
findRegionPathByName(options = [], regionName = '', parents = []) {
|
findRegionPathByName(options = [], regionName = '', parents = []) {
|
||||||
const target = String(regionName || '').replace(/\s+/g, '');
|
const target = String(regionName || '').replace(/\s+/g, '');
|
||||||
@@ -805,6 +851,32 @@ export default {
|
|||||||
});
|
});
|
||||||
return labels;
|
return labels;
|
||||||
},
|
},
|
||||||
|
setAddressRegionNames(field, labels = []) {
|
||||||
|
if (!field) return;
|
||||||
|
const normalized = labels.map(label => String(label || '').trim()).filter(Boolean);
|
||||||
|
const province = normalized.find(label => /省|自治区|特别行政区/.test(label)) || normalized[0] || '';
|
||||||
|
const city = normalized.filter(label => /市|州|盟/.test(label)).at(-1) || '';
|
||||||
|
const district = normalized.filter(label => /区|县|旗/.test(label)).at(-1) || '';
|
||||||
|
if (field.provinceName) field.model[field.provinceName] = province;
|
||||||
|
if (field.cityName) field.model[field.cityName] = city;
|
||||||
|
if (field.districtName) field.model[field.districtName] = district;
|
||||||
|
},
|
||||||
|
setAddressRegionNamesFromRow(field, row = {}) {
|
||||||
|
const labels = [row.provinceName, row.cityName, row.districtName].filter(Boolean);
|
||||||
|
if (labels.length) {
|
||||||
|
this.setAddressRegionNames(field, labels);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = String(row.regionName || '').replace(/\s+/g, '');
|
||||||
|
if (!text) return;
|
||||||
|
const provinceMatch = text.match(/^.*?(?:省|自治区|特别行政区)/);
|
||||||
|
const rest = provinceMatch ? text.slice(provinceMatch[0].length) : text;
|
||||||
|
const cityMatch = rest.match(/.+?(?:市|州|盟)/);
|
||||||
|
const districtMatch = cityMatch
|
||||||
|
? rest.slice(cityMatch[0].length).match(/^[^市州盟]*?(?:区|县|旗)/)
|
||||||
|
: rest.match(/.+?(?:区|县|旗)/);
|
||||||
|
this.setAddressRegionNames(field, [provinceMatch?.[0], cityMatch?.[0], districtMatch?.[0]]);
|
||||||
|
},
|
||||||
handleAddressRegionChange(target, value) {
|
handleAddressRegionChange(target, value) {
|
||||||
const field = this.addressModel(target);
|
const field = this.addressModel(target);
|
||||||
const path = Array.isArray(value) ? value.map(item => String(item)) : [];
|
const path = Array.isArray(value) ? value.map(item => String(item)) : [];
|
||||||
@@ -813,6 +885,8 @@ export default {
|
|||||||
const labels = this.regionLabels(path);
|
const labels = this.regionLabels(path);
|
||||||
field.model[field.name] = labels.join('');
|
field.model[field.name] = labels.join('');
|
||||||
field.model[field.regionCode] = path.at(-1) || '';
|
field.model[field.regionCode] = path.at(-1) || '';
|
||||||
|
field.model[field.siteCode] = '';
|
||||||
|
this.setAddressRegionNames(field, labels);
|
||||||
},
|
},
|
||||||
syncAddressRegionPath(target) {
|
syncAddressRegionPath(target) {
|
||||||
const field = this.addressModel(target);
|
const field = this.addressModel(target);
|
||||||
@@ -822,6 +896,7 @@ export default {
|
|||||||
? codePath
|
? codePath
|
||||||
: this.findRegionPathByName(this.regionOptions, field.model[field.name]);
|
: this.findRegionPathByName(this.regionOptions, field.model[field.name]);
|
||||||
this.addressRegionPaths = { ...this.addressRegionPaths, [target]: path };
|
this.addressRegionPaths = { ...this.addressRegionPaths, [target]: path };
|
||||||
|
if (path.length) this.setAddressRegionNames(field, this.regionLabels(path));
|
||||||
},
|
},
|
||||||
syncAddressRegionPaths() {
|
syncAddressRegionPaths() {
|
||||||
if (!this.regionOptions.length) return;
|
if (!this.regionOptions.length) return;
|
||||||
@@ -880,19 +955,27 @@ export default {
|
|||||||
const date = this.normalizeDateValue(value);
|
const date = this.normalizeDateValue(value);
|
||||||
return date ? `${date} ${endOfDay ? '23:59:59' : '00:00:00'}` : '';
|
return date ? `${date} ${endOfDay ? '23:59:59' : '00:00:00'}` : '';
|
||||||
},
|
},
|
||||||
getRegionDisplay(target, fallback) {
|
getRoadRegionDisplay(target, fallback) {
|
||||||
const labels = this.regionLabels(this.addressRegionPaths[target] || []);
|
const labels = this.regionLabels(this.addressRegionPaths[target] || []);
|
||||||
return labels.length ? labels.join('/') : fallback;
|
return labels.at(-1) || fallback;
|
||||||
|
},
|
||||||
|
getAddressDisplay(target, fallback) {
|
||||||
|
const field = this.addressModel(target);
|
||||||
|
const name = field?.model?.[field.name] || fallback;
|
||||||
|
return this.isNonRoadAddress(target) ? name : this.getRoadRegionDisplay(target, name);
|
||||||
},
|
},
|
||||||
getRouteTitle(index) {
|
getRouteTitle(index) {
|
||||||
const departureName =
|
const departureName =
|
||||||
index === 0
|
index === 0
|
||||||
? this.getRegionDisplay('departure', this.form.departureName || '发货地址')
|
? this.getAddressDisplay(
|
||||||
: this.getRegionDisplay(
|
'departure',
|
||||||
|
this.form.departureName || '发货地址'
|
||||||
|
)
|
||||||
|
: this.getAddressDisplay(
|
||||||
`route-${index - 1}`,
|
`route-${index - 1}`,
|
||||||
this.form.routes[index - 1].departureName || '途经地'
|
this.form.routes[index - 1].departureName || '途经地'
|
||||||
);
|
);
|
||||||
const arrivalName = this.getRegionDisplay(
|
const arrivalName = this.getAddressDisplay(
|
||||||
`route-${index}`,
|
`route-${index}`,
|
||||||
this.form.routes[index].departureName || '途经地'
|
this.form.routes[index].departureName || '途经地'
|
||||||
);
|
);
|
||||||
@@ -912,11 +995,14 @@ export default {
|
|||||||
return `${this.truncateRouteAddress(departure)}${separator}${this.truncateRouteAddress(arrival)}`;
|
return `${this.truncateRouteAddress(departure)}${separator}${this.truncateRouteAddress(arrival)}`;
|
||||||
},
|
},
|
||||||
getFinalRouteTitle() {
|
getFinalRouteTitle() {
|
||||||
const departureName = this.getRegionDisplay(
|
const departureName = this.getAddressDisplay(
|
||||||
`route-${this.form.routes.length - 1}`,
|
`route-${this.form.routes.length - 1}`,
|
||||||
this.lastRouteName
|
this.lastRouteName
|
||||||
);
|
);
|
||||||
const arrivalName = this.getRegionDisplay('arrival', this.form.arrivalName || '收货地址');
|
const arrivalName = this.getAddressDisplay(
|
||||||
|
'arrival',
|
||||||
|
this.form.arrivalName || '收货地址'
|
||||||
|
);
|
||||||
return `段${this.form.routes.length + 1}:${departureName} → ${arrivalName}`;
|
return `段${this.form.routes.length + 1}:${departureName} → ${arrivalName}`;
|
||||||
},
|
},
|
||||||
responseRecords(response) {
|
responseRecords(response) {
|
||||||
@@ -937,6 +1023,7 @@ export default {
|
|||||||
const first = this.contracts[0];
|
const first = this.contracts[0];
|
||||||
this.form.contractId = first?.id;
|
this.form.contractId = first?.id;
|
||||||
this.form.contractName = first?.contractName || '';
|
this.form.contractName = first?.contractName || '';
|
||||||
|
this.form.customerName = first?.partyA || '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async projectChange(projectId) {
|
async projectChange(projectId) {
|
||||||
@@ -950,6 +1037,7 @@ export default {
|
|||||||
contractChange(contractId) {
|
contractChange(contractId) {
|
||||||
const contract = this.contracts.find(item => String(item.id) === String(contractId));
|
const contract = this.contracts.find(item => String(item.id) === String(contractId));
|
||||||
this.form.contractName = contract?.contractName || '';
|
this.form.contractName = contract?.contractName || '';
|
||||||
|
this.form.customerName = contract?.partyA || '';
|
||||||
},
|
},
|
||||||
addressModel(target) {
|
addressModel(target) {
|
||||||
if (target.startsWith('route-')) {
|
if (target.startsWith('route-')) {
|
||||||
@@ -965,6 +1053,9 @@ export default {
|
|||||||
latitude: 'departureLatitude',
|
latitude: 'departureLatitude',
|
||||||
regionCode: 'departureRegionCode',
|
regionCode: 'departureRegionCode',
|
||||||
siteCode: 'departureSiteCode',
|
siteCode: 'departureSiteCode',
|
||||||
|
provinceName: 'departureProvinceName',
|
||||||
|
cityName: 'departureCityName',
|
||||||
|
districtName: 'departureDistrictName',
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
@@ -979,13 +1070,16 @@ export default {
|
|||||||
latitude: `${prefix}Latitude`,
|
latitude: `${prefix}Latitude`,
|
||||||
regionCode: `${prefix}RegionCode`,
|
regionCode: `${prefix}RegionCode`,
|
||||||
siteCode: `${prefix}SiteCode`,
|
siteCode: `${prefix}SiteCode`,
|
||||||
|
provinceName: `${prefix}ProvinceName`,
|
||||||
|
cityName: `${prefix}CityName`,
|
||||||
|
districtName: `${prefix}DistrictName`,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
addressTransportType(target) {
|
addressTransportType(target) {
|
||||||
if (target.startsWith('route-')) {
|
if (target.startsWith('route-')) {
|
||||||
return this.form.routes[Number(target.slice(6))]?.transportType || '';
|
return this.form.routes[Number(target.slice(6))]?.transportType || '';
|
||||||
}
|
}
|
||||||
return target === 'arrival' ? this.form.finalTransportType : '';
|
return target === 'arrival' ? this.form.finalTransportType : this.form.routes[0]?.transportType || '';
|
||||||
},
|
},
|
||||||
addressTransportMode(target) {
|
addressTransportMode(target) {
|
||||||
const type = this.addressTransportType(target);
|
const type = this.addressTransportType(target);
|
||||||
@@ -998,6 +1092,17 @@ export default {
|
|||||||
const value = String(type || '').trim().toLowerCase();
|
const value = String(type || '').trim().toLowerCase();
|
||||||
return value === 'road' || value.includes('公路');
|
return value === 'road' || value.includes('公路');
|
||||||
},
|
},
|
||||||
|
isNonRoadAddress(target) {
|
||||||
|
const field = this.addressModel(target);
|
||||||
|
if (!field) return false;
|
||||||
|
const siteCode = String(field.model[field.siteCode] || '').trim();
|
||||||
|
// 地址展示模式由实际选择的数据决定:有站点编码表示非公路站点名称,
|
||||||
|
// 没有站点编码则保留省市区级联,避免切换运输方式时改写已有地址。
|
||||||
|
return Boolean(siteCode && siteCode !== '/');
|
||||||
|
},
|
||||||
|
resolveAddressRegionCode(row = {}) {
|
||||||
|
return row.regionCode || row.districtCode || row.areaCode || row.adcode || '';
|
||||||
|
},
|
||||||
openCommonAddress(target) {
|
openCommonAddress(target) {
|
||||||
this.addressTarget = target;
|
this.addressTarget = target;
|
||||||
if (this.addressTransportMode(target) !== 'road') {
|
if (this.addressTransportMode(target) !== 'road') {
|
||||||
@@ -1054,14 +1159,15 @@ export default {
|
|||||||
const target = this.addressModel(this.addressTarget);
|
const target = this.addressModel(this.addressTarget);
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
const station = this.normalizeStation(row);
|
const station = this.normalizeStation(row);
|
||||||
target.model[target.name] = station.regionName || station.stationName;
|
target.model[target.name] = station.stationName || station.regionName;
|
||||||
target.model[target.address] = station.detailAddress;
|
target.model[target.address] = station.detailAddress;
|
||||||
target.model[target.longitude] = station.longitude || '';
|
target.model[target.longitude] = station.longitude || '';
|
||||||
target.model[target.latitude] = station.latitude || '';
|
target.model[target.latitude] = station.latitude || '';
|
||||||
target.model[target.regionCode] = station.regionCode || '';
|
target.model[target.regionCode] = this.resolveAddressRegionCode(station);
|
||||||
target.model[target.siteCode] = station.stationCode;
|
target.model[target.siteCode] = station.stationCode;
|
||||||
|
this.setAddressRegionNamesFromRow(target, station);
|
||||||
this.stationDialogVisible = false;
|
this.stationDialogVisible = false;
|
||||||
this.syncAddressRegionPath(this.addressTarget);
|
this.loadRegionOptions().then(() => this.syncAddressRegionPath(this.addressTarget));
|
||||||
},
|
},
|
||||||
async loadAddressList() {
|
async loadAddressList() {
|
||||||
this.addressLoading = true;
|
this.addressLoading = true;
|
||||||
@@ -1096,12 +1202,21 @@ export default {
|
|||||||
target.model[target.phone] = row.contactPhone || target.model[target.phone] || '';
|
target.model[target.phone] = row.contactPhone || target.model[target.phone] || '';
|
||||||
target.model[target.longitude] = row.longitude || '';
|
target.model[target.longitude] = row.longitude || '';
|
||||||
target.model[target.latitude] = row.latitude || '';
|
target.model[target.latitude] = row.latitude || '';
|
||||||
target.model[target.regionCode] = row.regionCode || '';
|
target.model[target.regionCode] = this.resolveAddressRegionCode(row);
|
||||||
|
target.model[target.siteCode] = '';
|
||||||
|
this.setAddressRegionNamesFromRow(target, row);
|
||||||
this.addressDialogVisible = false;
|
this.addressDialogVisible = false;
|
||||||
this.syncAddressRegionPath(this.addressTarget);
|
this.loadRegionOptions().then(() => this.syncAddressRegionPath(this.addressTarget));
|
||||||
|
},
|
||||||
|
handleAddressDetailClick(target) {
|
||||||
|
if (this.addressTransportMode(target) !== 'road') {
|
||||||
|
this.openCommonAddress(target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.openAddressMap(target);
|
||||||
},
|
},
|
||||||
openAddressMap(target) {
|
openAddressMap(target) {
|
||||||
if ((target.startsWith('route-') || target === 'arrival') && !this.isRoadTransportType(this.addressTransportType(target))) {
|
if (this.addressTransportMode(target) !== 'road') {
|
||||||
this.$message.info('非公路运输地址不能使用地图选择');
|
this.$message.info('非公路运输地址不能使用地图选择');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1170,12 +1285,24 @@ export default {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
const point = result?.geocodes?.[0]?.location;
|
const geocodes = result?.geocodes || [];
|
||||||
|
this.mapSearchResults = geocodes.map((item, index) => ({
|
||||||
|
id: item.id || index,
|
||||||
|
name: item.formattedAddress || this.mapKeyword,
|
||||||
|
address: item.formattedAddress || this.mapKeyword,
|
||||||
|
location: item.location,
|
||||||
|
}));
|
||||||
|
const point = geocodes[0]?.location;
|
||||||
if (!point) return this.$message.warning('地图搜索无匹配地址');
|
if (!point) return this.$message.warning('地图搜索无匹配地址');
|
||||||
this.pickAddressMap(point, this.mapKeyword);
|
this.pickAddressMap(point, this.mapKeyword);
|
||||||
})
|
})
|
||||||
.catch(() => this.$message.error('地图搜索失败'));
|
.catch(() => this.$message.error('地图搜索失败'));
|
||||||
},
|
},
|
||||||
|
selectAddressMapSearchResult(item) {
|
||||||
|
if (item && item.location) {
|
||||||
|
this.pickAddressMap(item.location, item.address || item.name || '');
|
||||||
|
}
|
||||||
|
},
|
||||||
ensureAddressGeocoder() {
|
ensureAddressGeocoder() {
|
||||||
if (this.addressMapGeocoder) return Promise.resolve(this.addressMapGeocoder);
|
if (this.addressMapGeocoder) return Promise.resolve(this.addressMapGeocoder);
|
||||||
return new Promise(resolve =>
|
return new Promise(resolve =>
|
||||||
@@ -1203,6 +1330,9 @@ export default {
|
|||||||
const component = result.regeocode?.addressComponent || {};
|
const component = result.regeocode?.addressComponent || {};
|
||||||
this.mapSelected.detailAddress =
|
this.mapSelected.detailAddress =
|
||||||
result.regeocode?.formattedAddress || this.mapSelected.detailAddress;
|
result.regeocode?.formattedAddress || this.mapSelected.detailAddress;
|
||||||
|
this.mapSelected.provinceName = component.province || '';
|
||||||
|
this.mapSelected.cityName = component.city || '';
|
||||||
|
this.mapSelected.districtName = component.district || '';
|
||||||
this.mapSelected.regionName = [component.province, component.city, component.district]
|
this.mapSelected.regionName = [component.province, component.city, component.district]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('');
|
.join('');
|
||||||
@@ -1228,6 +1358,8 @@ export default {
|
|||||||
target.model[target.longitude] = this.mapSelected.longitude;
|
target.model[target.longitude] = this.mapSelected.longitude;
|
||||||
target.model[target.latitude] = this.mapSelected.latitude;
|
target.model[target.latitude] = this.mapSelected.latitude;
|
||||||
target.model[target.regionCode] = this.mapSelected.regionCode || '';
|
target.model[target.regionCode] = this.mapSelected.regionCode || '';
|
||||||
|
target.model[target.siteCode] = '';
|
||||||
|
this.setAddressRegionNamesFromRow(target, this.mapSelected);
|
||||||
this.mapDialogVisible = false;
|
this.mapDialogVisible = false;
|
||||||
this.syncAddressRegionPath(this.mapTarget);
|
this.syncAddressRegionPath(this.mapTarget);
|
||||||
},
|
},
|
||||||
@@ -1445,6 +1577,8 @@ export default {
|
|||||||
'brand',
|
'brand',
|
||||||
'specification',
|
'specification',
|
||||||
'model',
|
'model',
|
||||||
|
'materialCode',
|
||||||
|
'deviceCode',
|
||||||
'remark',
|
'remark',
|
||||||
].some(key => firstGoods[key]);
|
].some(key => firstGoods[key]);
|
||||||
|
|
||||||
@@ -1489,6 +1623,8 @@ export default {
|
|||||||
brand: row.brand || '',
|
brand: row.brand || '',
|
||||||
specification: row.specification || row.spec || '',
|
specification: row.specification || row.spec || '',
|
||||||
model: row.model || '',
|
model: row.model || '',
|
||||||
|
materialCode: row.materialCode || '',
|
||||||
|
deviceCode: row.deviceCode || '',
|
||||||
remark: row.remark || row.descriptionOne || '',
|
remark: row.remark || row.descriptionOne || '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -1590,7 +1726,12 @@ export default {
|
|||||||
this.syncAddressRegionPaths();
|
this.syncAddressRegionPaths();
|
||||||
},
|
},
|
||||||
addGoods(index) {
|
addGoods(index) {
|
||||||
this.form.goods.splice(index + 1, 0, { quantityUnit: quantityUnitOptions[0] });
|
this.form.goods.splice(index + 1, 0, {
|
||||||
|
quantityUnit: quantityUnitOptions[0],
|
||||||
|
materialCode: '',
|
||||||
|
deviceCode: '',
|
||||||
|
remark: '',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
removeGoods(index) {
|
removeGoods(index) {
|
||||||
if (this.form.goods.length > 1) this.form.goods.splice(index, 1);
|
if (this.form.goods.length > 1) this.form.goods.splice(index, 1);
|
||||||
@@ -1707,13 +1848,20 @@ export default {
|
|||||||
await this.$refs.form.validate();
|
await this.$refs.form.validate();
|
||||||
if (!this.form.departureAddress || !this.form.arrivalAddress)
|
if (!this.form.departureAddress || !this.form.arrivalAddress)
|
||||||
return this.$message.warning('请填写收发货地址');
|
return this.$message.warning('请填写收发货地址');
|
||||||
if (!this.form.goods.some(item => item.cargoType && item.quantity > 0 && item.quantityUnit))
|
const invalidGoodsIndex = this.form.goods.findIndex(
|
||||||
return this.$message.warning('请至少填写一条有效货物');
|
item =>
|
||||||
if (
|
!String(item.cargoType || '').trim() ||
|
||||||
this.form.routes.some(item => !item.departureName || !item.transportType) ||
|
!(Number(item.quantity) > 0) ||
|
||||||
!this.form.finalTransportType
|
!String(item.quantityUnit || '').trim()
|
||||||
)
|
);
|
||||||
return this.$message.warning('请完整填写途经地及运输方式');
|
if (invalidGoodsIndex >= 0)
|
||||||
|
return this.$message.warning(`请完整填写第${invalidGoodsIndex + 1}条货物的类型、数量和数量单位`);
|
||||||
|
const invalidRouteIndex = this.form.routes.findIndex(
|
||||||
|
item => !item.departureName || !item.transportType
|
||||||
|
);
|
||||||
|
if (invalidRouteIndex >= 0)
|
||||||
|
return this.$message.warning(`请完整填写第${invalidRouteIndex + 1}段途经地及运输方式`);
|
||||||
|
if (!this.form.finalTransportType) return this.$message.warning('请填写最后一段运输方式');
|
||||||
}
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
...this.form,
|
...this.form,
|
||||||
@@ -1721,7 +1869,17 @@ export default {
|
|||||||
planEndTime: this.normalizeDateTimeValue(this.form.planEndTime, true),
|
planEndTime: this.normalizeDateTimeValue(this.form.planEndTime, true),
|
||||||
attachmentsJson: JSON.stringify(this.attachmentRows),
|
attachmentsJson: JSON.stringify(this.attachmentRows),
|
||||||
routes: [
|
routes: [
|
||||||
...this.form.routes.map((route, index) => ({ ...route, segmentNo: `段${index + 1}` })),
|
...this.form.routes.map((route, index) => ({
|
||||||
|
...route,
|
||||||
|
segmentNo: `段${index + 1}`,
|
||||||
|
...(index === 0
|
||||||
|
? {
|
||||||
|
departureProvinceName: this.form.departureProvinceName,
|
||||||
|
departureCityName: this.form.departureCityName,
|
||||||
|
departureDistrictName: this.form.departureDistrictName,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
})),
|
||||||
{
|
{
|
||||||
segmentNo: `段${this.form.routes.length + 1}`,
|
segmentNo: `段${this.form.routes.length + 1}`,
|
||||||
departureName: this.form.arrivalName,
|
departureName: this.form.arrivalName,
|
||||||
@@ -1732,6 +1890,9 @@ export default {
|
|||||||
departureLatitude: this.form.arrivalLatitude,
|
departureLatitude: this.form.arrivalLatitude,
|
||||||
departureRegionCode: this.form.arrivalRegionCode,
|
departureRegionCode: this.form.arrivalRegionCode,
|
||||||
departureSiteCode: this.form.arrivalSiteCode,
|
departureSiteCode: this.form.arrivalSiteCode,
|
||||||
|
departureProvinceName: this.form.arrivalProvinceName,
|
||||||
|
departureCityName: this.form.arrivalCityName,
|
||||||
|
departureDistrictName: this.form.arrivalDistrictName,
|
||||||
transportType: this.form.finalTransportType,
|
transportType: this.form.finalTransportType,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1766,6 +1927,10 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.master-editor {
|
||||||
|
padding-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
.master-editor section {
|
.master-editor section {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
@@ -1805,12 +1970,6 @@ export default {
|
|||||||
.master-editor__attachment {
|
.master-editor__attachment {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.master-editor__attachment-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.master-editor__attachment-upload {
|
.master-editor__attachment-upload {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
@@ -1883,57 +2042,59 @@ export default {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
.route-steps {
|
.route-flow {
|
||||||
position: relative;
|
position: relative;
|
||||||
.el-steps {
|
padding: 4px 0;
|
||||||
--el-text-color-placeholder: #a8abb2;
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 16px;
|
||||||
|
width: 2px;
|
||||||
|
background: #dcdfe6;
|
||||||
|
content: '';
|
||||||
}
|
}
|
||||||
:deep(.el-step__main) {
|
}
|
||||||
padding-bottom: 16px;
|
.route-flow-row {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 32px minmax(0, 1fr);
|
||||||
|
column-gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.route-flow-marker {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
&--middle {
|
||||||
|
background: #67c23a;
|
||||||
}
|
}
|
||||||
:deep(.el-step__title) {
|
&--end {
|
||||||
font-size: 16px;
|
background: #e6a23c;
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
:deep(.el-step__description) {
|
|
||||||
padding: 12px 0 0;
|
|
||||||
}
|
|
||||||
:deep(.el-step__line) {
|
|
||||||
background-color: #dcdfe6;
|
|
||||||
}
|
|
||||||
:deep(.el-step__icon) {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.route-step-icon {
|
|
||||||
display: inline-flex;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
min-width: 32px;
|
|
||||||
min-height: 32px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #409eff;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1;
|
|
||||||
&--middle {
|
|
||||||
background: #67c23a;
|
|
||||||
}
|
|
||||||
&--end {
|
|
||||||
background: #e6a23c;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.route-flow-body {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.route-flow-segment {
|
||||||
|
min-height: 48px;
|
||||||
|
padding-left: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
.route-node-form {
|
.route-node-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(420px, 1fr)) repeat(2, minmax(260px, 1fr));
|
grid-template-columns: 300px 360px repeat(3, 300px);
|
||||||
gap: 0 16px;
|
gap: 0 16px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
:deep(.el-form-item) {
|
:deep(.el-form-item) {
|
||||||
@@ -1959,9 +2120,24 @@ export default {
|
|||||||
.route-address-name {
|
.route-address-name {
|
||||||
flex: 0 0 220px;
|
flex: 0 0 220px;
|
||||||
}
|
}
|
||||||
.route-address-detail,
|
.route-address-detail {
|
||||||
|
flex: 0 0 300px;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
.route-contact-actions :deep(.el-input) {
|
.route-contact-actions :deep(.el-input) {
|
||||||
flex: 1;
|
flex: 0 0 200px;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
:deep(.route-contact-form-item .el-input),
|
||||||
|
:deep(.route-phone-form-item .el-input),
|
||||||
|
:deep(.route-date-form-item .el-date-editor) {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
:deep(.route-contact-actions) {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.address-picker-button {
|
.address-picker-button {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -1981,6 +2157,7 @@ export default {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
.route-segment-label {
|
.route-segment-label {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.route-title-path {
|
.route-title-path {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -2006,6 +2183,10 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.required-asterisk {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
.route-action-button {
|
.route-action-button {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,48 +1,203 @@
|
|||||||
<template>
|
<template>
|
||||||
<basic-container class="contract-change-page">
|
<basic-container class="contract-change-page">
|
||||||
|
<div class="archive-page-form__title">{{ pageTitle }}</div>
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="auto" class="contract-change-form">
|
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="auto" class="contract-change-form">
|
||||||
<section class="change-section contract-basic-section">
|
<section class="change-section contract-basic-section">
|
||||||
<div class="dialog-section-title">基本信息</div>
|
<div class="dialog-section-title">基本信息</div>
|
||||||
<div class="contract-basic-section__grid">
|
<div class="contract-basic-section__grid">
|
||||||
<el-form-item label="变更类型" class="contract-basic-section__change-type"><el-radio-group v-model="form.changeType"><el-radio label="合同信息变更" /><el-radio label="终止合同" /></el-radio-group></el-form-item>
|
<el-form-item label="变更类型" class="contract-basic-section__change-type">
|
||||||
<el-form-item label="合同编号"><el-input v-model="form.contractNo" disabled /></el-form-item>
|
<el-radio-group v-model="form.changeType">
|
||||||
<el-form-item label="合同名称" prop="contractName"><el-input v-model="form.contractName" /></el-form-item>
|
<el-radio label="合同信息变更" />
|
||||||
<el-form-item label="合同类型"><el-select v-model="form.contractCategory" disabled><el-option label="客户合同" value="客户合同" /><el-option label="承运商合同" value="承运商合同" /></el-select></el-form-item>
|
<el-radio label="终止合同" />
|
||||||
<el-form-item label="甲方"><el-input v-model="form.partyA" disabled /></el-form-item>
|
</el-radio-group>
|
||||||
<el-form-item label="乙方"><el-input v-model="form.partyB" disabled /></el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="所属项目"><el-input v-model="form.projectName" disabled /></el-form-item>
|
<el-form-item label="签约类型" prop="signType">
|
||||||
|
<el-select v-model="form.signType" placeholder="请选择签约类型" clearable>
|
||||||
|
<el-option
|
||||||
|
v-for="item in signTypeOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="合同编号">
|
||||||
|
<el-input v-model="form.contractNo" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="合同名称" prop="contractName">
|
||||||
|
<el-input
|
||||||
|
v-model="form.contractName"
|
||||||
|
maxlength="100"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入合同名称"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="合同类型">
|
||||||
|
<el-select v-model="form.contractCategory" disabled>
|
||||||
|
<el-option
|
||||||
|
v-for="item in contractCategoryOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="项目">
|
||||||
|
<el-input v-model="form.projectName" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="所属组织">
|
||||||
|
<el-input v-model="form.organizationName" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="甲方">
|
||||||
|
<el-input v-model="form.partyA" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="乙方">
|
||||||
|
<el-input v-model="form.partyB" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="签订日期">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.signDate"
|
||||||
|
type="date"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="合同期限">
|
<el-form-item label="合同期限">
|
||||||
<div class="contract-basic-section__date-range">
|
<div class="contract-basic-section__date-range">
|
||||||
<el-date-picker v-model="period[0]" type="date" format="YYYY-MM-DD" value-format="YYYY-MM-DD" placeholder="YYYY-MM-DD" />
|
<el-date-picker
|
||||||
|
v-model="period[0]"
|
||||||
|
type="date"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="YYYY-MM-DD"
|
||||||
|
/>
|
||||||
<span>至</span>
|
<span>至</span>
|
||||||
<el-date-picker v-model="period[1]" type="date" format="YYYY-MM-DD" value-format="YYYY-MM-DD" placeholder="YYYY-MM-DD" />
|
<el-date-picker
|
||||||
|
v-model="period[1]"
|
||||||
|
type="date"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="YYYY-MM-DD"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="所属组织"><el-input v-model="form.organizationName" disabled /></el-form-item>
|
<el-form-item label="回款账期">
|
||||||
<el-form-item label="签订日期"><el-date-picker v-model="form.signDate" type="date" value-format="YYYY-MM-DD" disabled /></el-form-item>
|
<el-input
|
||||||
<el-form-item label="合同格式"><el-select v-model="form.contractFormat"><el-option label="电子合同" value="电子合同" /><el-option label="纸质合同" value="纸质合同" /></el-select></el-form-item>
|
v-model="form.paymentDays"
|
||||||
<el-form-item label="结算方式"><el-input v-model="form.settlementMode" /></el-form-item>
|
placeholder="请输入"
|
||||||
<el-form-item label="是否需要加盖法人章"><el-select v-model="form.legalSealFlag"><el-option label="是" :value="1" /><el-option label="否" :value="0" /></el-select></el-form-item>
|
@input="value => positiveIntegerInput('paymentDays', value)"
|
||||||
<el-form-item label="一式(份)"><el-input v-model="form.copyCount" inputmode="numeric" maxlength="9" placeholder="请输入" @input="value => positiveIntegerInput('copyCount', value)" /></el-form-item>
|
>
|
||||||
<el-form-item label="回款账期(天)"><el-input v-model="form.paymentDays" inputmode="numeric" maxlength="9" placeholder="请输入" @input="value => positiveIntegerInput('paymentDays', value)" /></el-form-item>
|
<template #suffix>天</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="合同金额">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.contractAmount"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:controls="false"
|
||||||
|
placeholder="请输入"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否范本">
|
||||||
|
<el-select v-model="form.templateFlag" placeholder="请选择">
|
||||||
|
<el-option label="否" :value="0" />
|
||||||
|
<el-option label="是" :value="1" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="原件合同编号">
|
||||||
|
<el-input v-model="form.originalContractNo" maxlength="100" placeholder="请输入" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否电子章">
|
||||||
|
<el-select v-model="form.electronicSealFlag" placeholder="请选择">
|
||||||
|
<el-option label="否" :value="0" />
|
||||||
|
<el-option label="是" :value="1" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="开票周期">
|
||||||
|
<el-input
|
||||||
|
v-model="form.invoiceCycle"
|
||||||
|
placeholder="请输入"
|
||||||
|
@input="value => positiveIntegerInput('invoiceCycle', value)"
|
||||||
|
>
|
||||||
|
<template #suffix>天</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="结算币种" prop="settlementCurrency">
|
||||||
|
<el-select v-model="form.settlementCurrency" placeholder="请选择结算币种" clearable>
|
||||||
|
<el-option
|
||||||
|
v-for="item in settlementCurrencyOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="合同格式">
|
||||||
|
<el-select v-model="form.contractFormat" placeholder="请选择合同格式" clearable>
|
||||||
|
<el-option
|
||||||
|
v-for="item in contractFormatOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="一式">
|
||||||
|
<el-input
|
||||||
|
v-model="form.copyCount"
|
||||||
|
placeholder="请输入"
|
||||||
|
@input="value => positiveIntegerInput('copyCount', value)"
|
||||||
|
>
|
||||||
|
<template #suffix>份</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否需要加盖法人章">
|
||||||
|
<el-select v-model="form.legalSealFlag" placeholder="请选择">
|
||||||
|
<el-option label="否" :value="0" />
|
||||||
|
<el-option label="是" :value="1" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="结算方式">
|
||||||
|
<el-select v-model="form.settlementMode" placeholder="请选择结算方式" clearable>
|
||||||
|
<el-option
|
||||||
|
v-for="item in settlementModeOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="经办人">
|
||||||
|
<el-input v-model="form.handlerUserName" disabled />
|
||||||
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
<el-form-item label="备注" class="contract-basic-section__remark"><el-input v-model="form.remark" type="textarea" :rows="2" maxlength="2000" show-word-limit placeholder="请输入备注" /></el-form-item>
|
<el-form-item label="备注" class="contract-basic-section__remark">
|
||||||
|
<el-input
|
||||||
|
v-model="form.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入备注"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="change-section">
|
<contract-attachment-section
|
||||||
<div class="dialog-section-title">合同文件</div>
|
title="合同文件"
|
||||||
<div class="attachment-head">
|
description
|
||||||
<el-button type="primary" :disabled="!contractFileRows.length" @click="handleContractFileBatchDownload">批量下载</el-button>
|
attachment-type
|
||||||
</div>
|
use-upload-dialog
|
||||||
<el-table :data="contractFileRows" border class="change-table" @selection-change="selectedContractFiles = $event"><el-table-column type="selection" width="55" /><el-table-column type="index" label="序号" width="70" /><el-table-column label="文件名" min-width="240"><template #default="{ row }"><el-link type="primary" @click="previewAttachment(row, contractFileRows)">{{ row.originalName || row.name }}</el-link></template></el-table-column><el-table-column label="文件大小" width="120"><template #default="{ row }">{{ formatFileSize(row.size) }}</template></el-table-column><el-table-column prop="uploadUserName" label="上传人" width="140" /><el-table-column prop="uploadTime" label="上传时间" width="170" /><el-table-column label="操作" width="100"><template #default="{ $index }"><el-link type="danger" @click="removeContractFile($index)">删除</el-link></template></el-table-column></el-table>
|
:rows="contractFileRows"
|
||||||
<div class="attachment-upload">
|
:preview="previewAttachment"
|
||||||
<vehicle-attachment-upload v-model="contractFileRows" :readonly="false" :file-types="attachmentFileTypes" :max-size="500" :show-file-list="false" button-text="上传附件" @change="handleContractFileChange" />
|
@update:rows="contractFileRows = $event"
|
||||||
</div>
|
/>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="change-section">
|
<section class="change-section">
|
||||||
<div class="section-head"><div class="dialog-section-title">计费信息</div><el-button type="primary" plain @click="addPlan">添加</el-button></div>
|
<div class="section-head"><div class="dialog-section-title">计费信息</div><el-button type="primary" plain @click="addPlan">添加</el-button></div>
|
||||||
<el-radio-group v-model="feeGenerationMode"><el-radio label="system">系统生成</el-radio><el-radio label="manual">手动生成</el-radio></el-radio-group>
|
<el-radio-group v-model="feeGenerationMode"><el-radio label="system">系统生成</el-radio><el-radio label="manual">账单导入生成</el-radio></el-radio-group>
|
||||||
<el-table :data="plans" border class="change-table"><el-table-column type="index" label="序号" width="70" /><el-table-column prop="planName" label="方案名称" min-width="220" /><el-table-column label="默认方案" min-width="120"><template #default="{ row }">{{ row.defaultPlan ? '是' : '否' }}</template></el-table-column><el-table-column prop="remark" label="备注" min-width="260" /><el-table-column label="操作" width="180"><template #default="{ row, $index }"><el-link type="primary" @click="editPlan(row, $index)">编辑</el-link><el-link type="danger" @click="plans.splice($index, 1)">删除</el-link></template></el-table-column></el-table>
|
<el-table :data="plans" border class="change-table"><el-table-column type="index" label="序号" width="70" /><el-table-column prop="planName" label="方案名称" min-width="220" /><el-table-column label="默认方案" min-width="120"><template #default="{ row }">{{ row.defaultPlan ? '是' : '否' }}</template></el-table-column><el-table-column prop="remark" label="备注" min-width="260" /><el-table-column label="操作" width="180"><template #default="{ row, $index }"><el-link type="primary" @click="editPlan(row, $index)">编辑</el-link><el-link type="danger" @click="plans.splice($index, 1)">删除</el-link></template></el-table-column></el-table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -52,18 +207,39 @@
|
|||||||
<el-tab-pane label="预结算配置" name="pre" />
|
<el-tab-pane label="预结算配置" name="pre" />
|
||||||
<el-tab-pane label="正式结算配置" name="formal" />
|
<el-tab-pane label="正式结算配置" name="formal" />
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
<div class="settlement-switch"><span>自动生成结算单</span><el-radio-group v-model="settlementRule.autoGenerate"><el-radio :label="1">开启</el-radio><el-radio :label="0">关闭</el-radio></el-radio-group></div>
|
<div class="settlement-switch">
|
||||||
|
<span>自动生成结算单</span>
|
||||||
|
<el-radio-group v-model="settlementRule.autoGenerate">
|
||||||
|
<el-radio :label="1">开启</el-radio>
|
||||||
|
<el-tooltip content="开启时,系统根据配置规则归集运单,定时生成结算单" placement="top">
|
||||||
|
<el-icon class="settlement-switch-tip"><QuestionFilled /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-radio :label="0">关闭</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
<el-form v-if="settlementRule.autoGenerate === 1" :model="settlementRule" label-position="right" label-width="auto" class="settlement-form">
|
<el-form v-if="settlementRule.autoGenerate === 1" :model="settlementRule" label-position="right" label-width="auto" class="settlement-form">
|
||||||
<el-form-item label="账单起始日期" required><el-date-picker v-model="settlementRule.billStartDate" type="date" placeholder="请选择账单起始日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" /></el-form-item>
|
<el-form-item label="账单起始日期" required><el-date-picker v-model="settlementRule.billStartDate" type="date" placeholder="请选择账单起始日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" /></el-form-item>
|
||||||
<el-form-item label="结算类型" required><el-select v-model="settlementRule.settlementType" placeholder="请选择结算类型" @change="handleSettlementTypeChange"><el-option v-for="item in settlementTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
|
<el-form-item label="结算类型" required><el-select v-model="settlementRule.settlementType" placeholder="请选择结算类型" @change="handleSettlementTypeChange"><el-option v-for="item in settlementTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
|
||||||
<el-form-item v-if="showSettlementBillCycleType" label="账单周期类型" required><el-select v-model="settlementRule.billCycleType" placeholder="请选择账单周期类型" @change="handleCycleTypeChange"><el-option v-for="item in billCycleTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
|
<el-form-item v-if="showSettlementBillCycleType" label="结算周期" required><el-select v-model="settlementRule.billCycleType" placeholder="请选择结算周期" @change="handleCycleTypeChange"><el-option v-for="item in billCycleTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
|
||||||
<el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日" required><el-select v-model="settlementRule.billCutoffDay" placeholder="请选择账单截单日"><el-option v-for="item in billCutoffDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
|
<el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日" required><el-select v-model="settlementRule.billCutoffDay" placeholder="请选择账单截单日"><el-option v-for="item in billCutoffDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
|
||||||
<el-form-item v-if="showSettlementCycleDays" label="周期天数" required><el-select v-model="settlementRule.cycleDays" placeholder="请选择周期天数"><el-option v-for="item in cycleDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
|
<el-form-item v-if="showSettlementCycleDays" label="周期天数" required><el-select v-model="settlementRule.cycleDays" placeholder="请选择周期天数"><el-option v-for="item in cycleDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
<el-table v-if="settlementRule.autoGenerate === 1 && showSettlementCustomPeriods" :data="settlementRule.customPeriods || []" border class="change-table custom-periods-table">
|
||||||
|
<el-table-column type="index" label="序号" width="70" />
|
||||||
|
<el-table-column label="运单区间-开始日" min-width="180"><template #default="{ row, $index }"><el-select :model-value="row.startDay" placeholder="请选择" clearable @update:model-value="value => handleCustomPeriodStartDayChange($index, value)"><el-option v-for="item in billCutoffDayOptions" :key="`start-${item.value}`" :label="item.label" :value="item.value" /></el-select></template></el-table-column>
|
||||||
|
<el-table-column label="运单区间-结束日" min-width="200"><template #default="{ row, $index }"><el-select :model-value="row.endDay" placeholder="请选择" clearable @update:model-value="value => handleCustomPeriodEndDayChange($index, value)"><el-option v-for="item in customPeriodEndDayOptions(row)" :key="item.value" :label="item.label" :value="item.value" /></el-select></template></el-table-column>
|
||||||
|
<el-table-column label="操作" width="100"><template #default="{ $index }"><el-link v-if="$index === 0" type="primary" @click="addCustomPeriodRow">添加</el-link><el-link v-else type="danger" @click="removeCustomPeriodRow($index)">删除</el-link></template></el-table-column>
|
||||||
|
<template #empty><el-link type="primary" @click="addCustomPeriodRow">添加</el-link></template>
|
||||||
|
</el-table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="change-section">
|
<section class="change-section">
|
||||||
<div class="dialog-section-title">付款比例设置</div>
|
<div class="dialog-section-title">
|
||||||
|
付款比例设置
|
||||||
|
<el-tooltip content="非必填;配置付款比例时,合计必须等于100%。" placement="top">
|
||||||
|
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"><el-icon-question-filled /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
<el-table :data="paymentRatioRows" border class="change-table">
|
<el-table :data="paymentRatioRows" border class="change-table">
|
||||||
<el-table-column type="index" label="序号" width="70" />
|
<el-table-column type="index" label="序号" width="70" />
|
||||||
<el-table-column prop="paymentTerm" label="付款笔数" min-width="180" />
|
<el-table-column prop="paymentTerm" label="付款笔数" min-width="180" />
|
||||||
@@ -76,13 +252,41 @@
|
|||||||
|
|
||||||
<billing-plan-editor v-model="planDialogVisible" :value="planEditor" :index="planEditorIndex" @save="savePlan" />
|
<billing-plan-editor v-model="planDialogVisible" :value="planEditor" :index="planEditorIndex" @save="savePlan" />
|
||||||
|
|
||||||
<section class="change-section attachment-section">
|
<contract-attachment-section
|
||||||
<div class="section-head"><div class="dialog-section-title">其它附件</div><el-button type="primary" plain>批量下载</el-button></div>
|
title="其它附件"
|
||||||
<el-table :data="attachments" border class="change-table"><el-table-column type="index" label="序号" width="70" /><el-table-column label="文件名" min-width="240"><template #default="{ row }"><el-link type="primary" @click="previewAttachment(row, attachments)">{{ row.originalName || row.name }}</el-link></template></el-table-column><el-table-column prop="description" label="附件描述" min-width="240" /><el-table-column prop="size" label="文件大小" width="120" /><el-table-column prop="userName" label="上传人" width="140" /><el-table-column prop="uploadTime" label="上传时间" width="180" /><el-table-column label="操作" width="100"><template #default="{ $index }"><el-link type="danger" @click="attachments.splice($index, 1)">删除</el-link></template></el-table-column></el-table>
|
description
|
||||||
<el-upload action="#" :auto-upload="false" multiple :show-file-list="false" @change="handleAttachment"><el-button plain>上传附件</el-button></el-upload>
|
attachment-type
|
||||||
|
use-upload-dialog
|
||||||
|
attachment-location="其它附件"
|
||||||
|
:rows="attachments"
|
||||||
|
:preview="previewAttachment"
|
||||||
|
@update:rows="attachments = $event"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section class="change-section change-reason-section">
|
||||||
|
<div class="dialog-section-title">变更原因</div>
|
||||||
|
<el-form-item prop="changeReason" class="change-reason-section__input">
|
||||||
|
<el-input
|
||||||
|
v-model="form.changeReason"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入变更原因"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="change-section change-reason-section"><div class="dialog-section-title">变更原因</div><el-form-item prop="changeReason"><el-input v-model="form.changeReason" type="textarea" :rows="2" maxlength="2000" show-word-limit placeholder="请输入变更原因" /></el-form-item><div class="dialog-section-title">变更材料</div><el-upload action="#" :auto-upload="false" multiple :show-file-list="false" @change="handleChangeMaterial"><el-button plain>上传变更材料</el-button></el-upload></section>
|
<contract-attachment-section
|
||||||
|
title="变更材料"
|
||||||
|
description
|
||||||
|
attachment-type
|
||||||
|
use-upload-dialog
|
||||||
|
attachment-location="变更材料"
|
||||||
|
:rows="changeMaterials"
|
||||||
|
:preview="previewAttachment"
|
||||||
|
@update:rows="changeMaterials = $event"
|
||||||
|
/>
|
||||||
<div class="page-footer">
|
<div class="page-footer">
|
||||||
<el-button @click="$router.back()">取消</el-button>
|
<el-button @click="$router.back()">取消</el-button>
|
||||||
<el-button type="primary" @click="submit">提交</el-button>
|
<el-button type="primary" @click="submit">提交</el-button>
|
||||||
@@ -99,6 +303,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import * as api from '@/api/business/contract-manage';
|
import * as api from '@/api/business/contract-manage';
|
||||||
import BillingPlanEditor from './components/billing-plan-editor.vue';
|
import BillingPlanEditor from './components/billing-plan-editor.vue';
|
||||||
|
import ContractAttachmentSection, {
|
||||||
|
normalizeContractFileRows,
|
||||||
|
} from './components/contract-attachment-section.vue';
|
||||||
|
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||||
|
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||||||
|
import {
|
||||||
|
contractCategoryOptions as defaultContractCategoryOptions,
|
||||||
|
signTypeOptions as defaultSignTypeOptions,
|
||||||
|
} from '@/option/business/common';
|
||||||
import { ElImageViewer } from 'element-plus';
|
import { ElImageViewer } from 'element-plus';
|
||||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||||
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
|
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
|
||||||
@@ -112,31 +325,294 @@ const viewerPlugins = [
|
|||||||
textPlugin(),
|
textPlugin(),
|
||||||
fallbackPlugin(),
|
fallbackPlugin(),
|
||||||
];
|
];
|
||||||
|
const defaultSettlementModeOptions = [
|
||||||
|
{ label: '现结', value: '现结' },
|
||||||
|
{ label: '日结', value: '日结' },
|
||||||
|
{ label: '周结', value: '周结' },
|
||||||
|
{ label: '月结', value: '月结' },
|
||||||
|
{ label: '按固定天数周期', value: '按固定天数周期' },
|
||||||
|
];
|
||||||
|
const defaultContractFormatOptions = [
|
||||||
|
{ label: '电子合同', value: '电子合同' },
|
||||||
|
{ label: '纸质合同', value: '纸质合同' },
|
||||||
|
];
|
||||||
const normalizeOptionalPositiveInteger = value => {
|
const normalizeOptionalPositiveInteger = value => {
|
||||||
if (value === undefined || value === null || value === '') return null;
|
if (value === undefined || value === null || value === '') return null;
|
||||||
const number = Number(value);
|
const number = Number(value);
|
||||||
return Number.isInteger(number) && number > 0 ? number : null;
|
return Number.isInteger(number) && number > 0 ? number : null;
|
||||||
};
|
};
|
||||||
|
const normalizeOptionalAmount = value => {
|
||||||
|
if (value === undefined || value === null || value === '') return null;
|
||||||
|
const number = Number(value);
|
||||||
|
return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const normalizeCustomPeriods = (periods = []) =>
|
||||||
|
(periods || []).map(item => ({
|
||||||
|
startDay: Number(item?.startDay) > 0 ? Number(item.startDay) : '',
|
||||||
|
endDay: Number(item?.endDay) > 0 ? Number(item.endDay) : '',
|
||||||
|
}));
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { BillingPlanEditor, ElImageViewer, OpenFileViewer },
|
components: { BillingPlanEditor, ContractAttachmentSection, ElImageViewer, OpenFileViewer },
|
||||||
data() { return { form: {}, period: [], plans: [], attachments: [], contractFiles: [], contractFileRows: [], selectedContractFiles: [], paymentRatioRows: [], settlementConfigTab: 'pre', changeMaterials: [], imagePreviewVisible: false, imagePreviewUrls: [], imagePreviewIndex: 0, documentPreviewVisible: false, previewFile: {}, viewerPlugins, viewerToolbar: { zoom: true, rotate: true, download: true, fullscreen: true }, feeGenerationMode: 'system', settlementRule: { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: '' }, preSettlementConfig: {}, formalSettlementConfig: {}, settlementTypeOptions: ['月结','日结','周结','半月结','固定天数周期结算'], billCycleTypeOptions: ['固定截单日','自然月'], billCutoffDayOptions: Array.from({ length: 31 }, (_, index) => ({ label: `${index + 1}日`, value: index + 1 })), cycleDayOptions: [7,15,30,60].map(value => ({ label: `${value}天`, value })), planDialogVisible: false, planEditor: { planName: '', defaultPlan: false, remark: '', rules: [] }, planEditorIndex: -1, billingElements: ['按重量','按体积','按车辆','按里程','按吨·公里','固定金额(整单一口价)','按数量'], attachmentFileTypes: ['pdf','bmp','jpeg','png','jpg','doc','docx','ppt','pptx','xlsx','xls','eml','msg','zip'], rules: { contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }], changeReason: [{ required: true, message: '请输入变更原因', trigger: 'blur' }] } }; },
|
data() {
|
||||||
computed: { showSettlementBillCycleType() { return this.settlementRule.settlementType === '月结'; }, showSettlementBillCutoffDay() { return this.showSettlementBillCycleType && this.settlementRule.billCycleType === '固定截单日'; }, showSettlementCycleDays() { return this.settlementRule.settlementType === '固定天数周期结算'; } },
|
return {
|
||||||
mounted() { this.load(); },
|
form: {},
|
||||||
watch: { settlementConfigTab(tab, oldTab) { if (tab === oldTab) return; if (oldTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; this.settlementRule = { ...(tab === 'pre' ? this.preSettlementConfig : this.formalSettlementConfig) }; } },
|
period: [],
|
||||||
|
plans: [],
|
||||||
|
attachments: [],
|
||||||
|
contractFiles: [],
|
||||||
|
contractFileRows: [],
|
||||||
|
paymentRatioRows: [],
|
||||||
|
settlementConfigTab: 'pre',
|
||||||
|
changeMaterials: [],
|
||||||
|
imagePreviewVisible: false,
|
||||||
|
imagePreviewUrls: [],
|
||||||
|
imagePreviewIndex: 0,
|
||||||
|
documentPreviewVisible: false,
|
||||||
|
previewFile: {},
|
||||||
|
viewerPlugins,
|
||||||
|
viewerToolbar: { zoom: true, rotate: true, download: true, fullscreen: true },
|
||||||
|
feeGenerationMode: 'system',
|
||||||
|
settlementRule: {
|
||||||
|
autoGenerate: '',
|
||||||
|
billStartDate: '',
|
||||||
|
settlementType: '',
|
||||||
|
billCycleType: '',
|
||||||
|
billCutoffDay: '',
|
||||||
|
cycleDays: '',
|
||||||
|
customPeriods: [],
|
||||||
|
},
|
||||||
|
preSettlementConfig: {},
|
||||||
|
formalSettlementConfig: {},
|
||||||
|
settlementTypeOptions: ['月结', '日结', '周结', '半月结', '固定天数周期结算'],
|
||||||
|
billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'],
|
||||||
|
billCutoffDayOptions: Array.from({ length: 31 }, (_, index) => ({
|
||||||
|
label: `${index + 1}日`,
|
||||||
|
value: index + 1,
|
||||||
|
})),
|
||||||
|
cycleDayOptions: [7, 15, 30, 60].map(value => ({ label: `${value}天`, value })),
|
||||||
|
signTypeOptions: defaultSignTypeOptions,
|
||||||
|
contractCategoryDictOptions: [],
|
||||||
|
settlementCurrencyOptions: [],
|
||||||
|
settlementModeDictOptions: [],
|
||||||
|
contractFormatDictOptions: [],
|
||||||
|
planDialogVisible: false,
|
||||||
|
planEditor: { planName: '', defaultPlan: false, remark: '', rules: [] },
|
||||||
|
planEditorIndex: -1,
|
||||||
|
billingElements: [
|
||||||
|
'按重量',
|
||||||
|
'按体积',
|
||||||
|
'按车辆',
|
||||||
|
'按里程',
|
||||||
|
'按吨·公里',
|
||||||
|
'固定金额(整单一口价)',
|
||||||
|
'按数量',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
|
||||||
|
changeReason: [{ max: 500, message: '变更原因不能超过500个字符', trigger: 'blur' }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
pageTitle() {
|
||||||
|
return this.$route.query.name || '合同变更';
|
||||||
|
},
|
||||||
|
contractCategoryOptions() {
|
||||||
|
return this.contractCategoryDictOptions.length
|
||||||
|
? this.contractCategoryDictOptions
|
||||||
|
: defaultContractCategoryOptions;
|
||||||
|
},
|
||||||
|
settlementModeOptions() {
|
||||||
|
return this.settlementModeDictOptions.length
|
||||||
|
? this.settlementModeDictOptions
|
||||||
|
: defaultSettlementModeOptions;
|
||||||
|
},
|
||||||
|
contractFormatOptions() {
|
||||||
|
return this.contractFormatDictOptions.length
|
||||||
|
? this.contractFormatDictOptions
|
||||||
|
: defaultContractFormatOptions;
|
||||||
|
},
|
||||||
|
showSettlementBillCycleType() {
|
||||||
|
return this.settlementRule.settlementType === '月结';
|
||||||
|
},
|
||||||
|
showSettlementBillCutoffDay() {
|
||||||
|
return this.showSettlementBillCycleType && this.settlementRule.billCycleType === '固定截单日';
|
||||||
|
},
|
||||||
|
showSettlementCustomPeriods() {
|
||||||
|
return this.showSettlementBillCycleType && this.settlementRule.billCycleType === '自定义多周期';
|
||||||
|
},
|
||||||
|
showSettlementCycleDays() {
|
||||||
|
return this.settlementRule.settlementType === '固定天数周期结算';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadDictionaries();
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
settlementConfigTab(tab, oldTab) {
|
||||||
|
if (tab === oldTab) return;
|
||||||
|
if (oldTab === 'pre') this.preSettlementConfig = { ...this.settlementRule };
|
||||||
|
else this.formalSettlementConfig = { ...this.settlementRule };
|
||||||
|
this.settlementRule = {
|
||||||
|
...(tab === 'pre' ? this.preSettlementConfig : this.formalSettlementConfig),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async load() { const id = this.$route.query.id; if (!id) return; const res = await api.getDetail(id); const data = res.data?.data || res.data || {}; this.form = { ...data, copyCount: normalizeOptionalPositiveInteger(data.copyCount), paymentDays: normalizeOptionalPositiveInteger(data.paymentDays), changeType: '合同信息变更' }; this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : []; this.plans = this.parse(data.billingPlanJson); this.attachments = this.parse(data.attachmentsJson); this.contractFileRows = this.parse(data.contractFileJson); const rules = this.parseObject(data.settlementRuleJson); const pre = this.parseObject(data.preSettlementConfigJson); const formal = this.parseObject(data.formalSettlementConfigJson); const legacy = Object.keys(rules).some(key => !['preSettlementConfig', 'formalSettlementConfig'].includes(key)) ? rules : {}; this.preSettlementConfig = rules.preSettlementConfig || (Object.keys(pre).length ? pre : legacy); this.formalSettlementConfig = rules.formalSettlementConfig || (Object.keys(formal).length ? formal : legacy); this.settlementRule = { ...this.preSettlementConfig }; this.feeGenerationMode = data.feeGenerationMode || (Number(data.billingEnabled) === 0 ? 'manual' : 'system'); this.paymentRatioRows = this.parse(data.paymentRatioJson); },
|
loadDictionaries() {
|
||||||
parse(value) { try { const result = JSON.parse(value || '[]'); return Array.isArray(result) ? result : []; } catch { return []; } },
|
Promise.all([
|
||||||
parseObject(value) { try { return { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: 15, ...(JSON.parse(value || '{}') || {}) }; } catch { return { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: 15 }; } },
|
getSystemDictionary({ code: 'currency_type' }),
|
||||||
positiveIntegerInput(prop, value) { this.form[prop] = String(value ?? '').replace(/\D/g, '').replace(/^0+/, ''); },
|
getBizDictionary({ code: 'settle_method' }),
|
||||||
|
getBizDictionary({ code: 'contractFormat' }),
|
||||||
|
getBizDictionary({ code: 'contractCategory' }),
|
||||||
|
]).then(([currencyRes, methodRes, formatRes, categoryRes]) => {
|
||||||
|
const records = response => {
|
||||||
|
const data = response?.data?.data || response?.data || [];
|
||||||
|
return Array.isArray(data) ? data : data.records || [];
|
||||||
|
};
|
||||||
|
this.settlementCurrencyOptions = records(currencyRes).map(item => ({
|
||||||
|
label: item.dictValue,
|
||||||
|
value: item.dictKey,
|
||||||
|
}));
|
||||||
|
this.settlementModeDictOptions = records(methodRes).map(item => ({
|
||||||
|
label: item.dictValue,
|
||||||
|
value: item.dictKey,
|
||||||
|
}));
|
||||||
|
this.contractFormatDictOptions = records(formatRes).map(item => ({
|
||||||
|
label: item.dictValue,
|
||||||
|
value: item.dictKey,
|
||||||
|
}));
|
||||||
|
this.contractCategoryDictOptions = records(categoryRes).map(item => ({
|
||||||
|
label: item.dictValue,
|
||||||
|
value: item.dictKey,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async load() {
|
||||||
|
const id = this.$route.query.id;
|
||||||
|
if (!id) return;
|
||||||
|
const res = await api.getDetail(id);
|
||||||
|
const data = res.data?.data || res.data || {};
|
||||||
|
this.form = {
|
||||||
|
...data,
|
||||||
|
changeContent: '',
|
||||||
|
changeReason: '',
|
||||||
|
changeAttachmentsJson: '',
|
||||||
|
copyCount: normalizeOptionalPositiveInteger(data.copyCount),
|
||||||
|
invoiceCycle: normalizeOptionalPositiveInteger(data.invoiceCycle),
|
||||||
|
paymentDays: normalizeOptionalPositiveInteger(data.paymentDays),
|
||||||
|
contractAmount: normalizeOptionalAmount(data.contractAmount),
|
||||||
|
templateFlag: data.templateFlag ?? 0,
|
||||||
|
electronicSealFlag: data.electronicSealFlag ?? 0,
|
||||||
|
archiveStatus: data.archiveStatus || '未归档',
|
||||||
|
changeType: '合同信息变更',
|
||||||
|
};
|
||||||
|
this.changeMaterials = [];
|
||||||
|
this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : [];
|
||||||
|
this.plans = this.parse(data.billingPlanJson);
|
||||||
|
this.attachments = this.mergeChangeAttachments(
|
||||||
|
this.parse(data.attachmentsJson),
|
||||||
|
this.parse(data.changeAttachmentsJson)
|
||||||
|
);
|
||||||
|
this.contractFileRows = normalizeContractFileRows(this.parse(data.contractFileJson));
|
||||||
|
const rules = this.parseObject(data.settlementRuleJson);
|
||||||
|
const pre = this.parseObject(data.preSettlementConfigJson);
|
||||||
|
const formal = this.parseObject(data.formalSettlementConfigJson);
|
||||||
|
const legacy = Object.keys(rules).some(
|
||||||
|
key => !['preSettlementConfig', 'formalSettlementConfig'].includes(key)
|
||||||
|
)
|
||||||
|
? rules
|
||||||
|
: {};
|
||||||
|
const normalizeRule = rule => {
|
||||||
|
const next = {
|
||||||
|
autoGenerate: '',
|
||||||
|
billStartDate: '',
|
||||||
|
settlementType: '',
|
||||||
|
billCycleType: '',
|
||||||
|
billCutoffDay: '',
|
||||||
|
cycleDays: '',
|
||||||
|
customPeriods: [],
|
||||||
|
...(rule || {}),
|
||||||
|
};
|
||||||
|
if (next.settlementType === '月结' && next.billCycleType === '自定义多周期') {
|
||||||
|
next.customPeriods = normalizeCustomPeriods(
|
||||||
|
Array.isArray(next.customPeriods) ? next.customPeriods : []
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
next.customPeriods = [];
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
};
|
||||||
|
this.preSettlementConfig = normalizeRule(
|
||||||
|
rules.preSettlementConfig || (Object.keys(pre).length ? pre : legacy)
|
||||||
|
);
|
||||||
|
this.formalSettlementConfig = normalizeRule(
|
||||||
|
rules.formalSettlementConfig || (Object.keys(formal).length ? formal : legacy)
|
||||||
|
);
|
||||||
|
this.settlementRule = { ...this.preSettlementConfig };
|
||||||
|
this.feeGenerationMode =
|
||||||
|
data.feeGenerationMode || (Number(data.billingEnabled) === 0 ? 'manual' : 'system');
|
||||||
|
this.paymentRatioRows = this.parse(data.paymentRatioJson);
|
||||||
|
},
|
||||||
|
parse(value) {
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(value || '[]');
|
||||||
|
return Array.isArray(result) ? result : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 审核通过后上一次的变更材料归集到「其它附件」,按文件地址/文件名去重,避免重复展示
|
||||||
|
mergeChangeAttachments(attachments = [], changeAttachments = []) {
|
||||||
|
const key = item => this.attachmentUrl(item) || this.attachmentName(item);
|
||||||
|
const exists = new Set(attachments.map(key));
|
||||||
|
const merged = changeAttachments
|
||||||
|
.filter(item => !exists.has(key(item)))
|
||||||
|
.map(item => ({
|
||||||
|
...item,
|
||||||
|
size: /^\d+$/.test(String(item.size ?? '')) ? this.formatFileSize(item.size) : item.size,
|
||||||
|
}));
|
||||||
|
return [...attachments, ...merged];
|
||||||
|
},
|
||||||
|
parseObject(value) {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
autoGenerate: '',
|
||||||
|
billStartDate: '',
|
||||||
|
settlementType: '',
|
||||||
|
billCycleType: '',
|
||||||
|
billCutoffDay: '',
|
||||||
|
cycleDays: '',
|
||||||
|
customPeriods: [],
|
||||||
|
...(JSON.parse(value || '{}') || {}),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
autoGenerate: '',
|
||||||
|
billStartDate: '',
|
||||||
|
settlementType: '',
|
||||||
|
billCycleType: '',
|
||||||
|
billCutoffDay: '',
|
||||||
|
cycleDays: '',
|
||||||
|
customPeriods: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
positiveIntegerInput(prop, value) {
|
||||||
|
this.form[prop] = String(value ?? '')
|
||||||
|
.replace(/\D/g, '')
|
||||||
|
.replace(/^0+/, '');
|
||||||
|
},
|
||||||
addPlan() { this.planEditorIndex = -1; this.planEditor = { planName: `计费方案${this.plans.length + 1}`, defaultPlan: !this.plans.length, remark: '', rules: [{}] }; this.planDialogVisible = true; },
|
addPlan() { this.planEditorIndex = -1; this.planEditor = { planName: `计费方案${this.plans.length + 1}`, defaultPlan: !this.plans.length, remark: '', rules: [{}] }; this.planDialogVisible = true; },
|
||||||
editPlan(row, index) { this.planEditorIndex = index; this.planEditor = JSON.parse(JSON.stringify(row)); this.planDialogVisible = true; },
|
editPlan(row, index) { this.planEditorIndex = index; this.planEditor = JSON.parse(JSON.stringify(row)); this.planDialogVisible = true; },
|
||||||
toggleDefaultPlan(value) { if (value) this.plans.forEach(item => { item.defaultPlan = false; }); },
|
toggleDefaultPlan(value) { if (value) this.plans.forEach(item => { item.defaultPlan = false; }); },
|
||||||
savePlan(value, index) { if (index < 0) this.plans.push(value); else this.plans.splice(index, 1, value); if (value.defaultPlan) this.plans.forEach((item, current) => { if (current !== (index < 0 ? this.plans.length - 1 : index)) item.defaultPlan = false; }); },
|
savePlan(value, index) { if (index < 0) this.plans.push(value); else this.plans.splice(index, 1, value); if (value.defaultPlan) this.plans.forEach((item, current) => { if (current !== (index < 0 ? this.plans.length - 1 : index)) item.defaultPlan = false; }); },
|
||||||
handleSettlementTypeChange(value) { if (value !== '月结') { this.settlementRule.billCycleType = ''; this.settlementRule.billCutoffDay = ''; } else if (!this.settlementRule.billCycleType) this.settlementRule.billCycleType = '固定截单日'; if (value !== '固定天数周期结算') this.settlementRule.cycleDays = ''; },
|
handleSettlementTypeChange(value) { if (value !== '月结') { this.settlementRule.billCycleType = ''; this.settlementRule.billCutoffDay = ''; this.settlementRule.customPeriods = []; } else if (this.settlementRule.billCycleType === '自定义多周期') this.settlementRule.customPeriods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); else this.settlementRule.customPeriods = []; if (value !== '固定天数周期结算') this.settlementRule.cycleDays = ''; },
|
||||||
handleCycleTypeChange(value) { if (value === '固定截单日' && !this.settlementRule.billCutoffDay) this.settlementRule.billCutoffDay = 25; if (value !== '固定截单日') this.settlementRule.billCutoffDay = ''; },
|
handleCycleTypeChange(value) { if (value !== '固定截单日') this.settlementRule.billCutoffDay = ''; if (value === '自定义多周期') this.settlementRule.customPeriods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); else this.settlementRule.customPeriods = []; },
|
||||||
handleAttachment(event) { const raw = event.raw; if (raw) this.attachments.push({ name: raw.name, size: `${Math.ceil(raw.size / 1024)}KB`, uploadTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss') }); },
|
|
||||||
handleContractFileChange(list) { this.contractFileRows = (list || []).map(item => ({ ...item, uploadTime: item.uploadTime || this.$dayjs().format('YYYY-MM-DD HH:mm:ss') })); },
|
|
||||||
attachmentUrl(row = {}) { return row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || ''; },
|
attachmentUrl(row = {}) { return row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || ''; },
|
||||||
attachmentName(row = {}) { return row.originalName || row.name || row.fileName || '附件'; },
|
attachmentName(row = {}) { return row.originalName || row.name || row.fileName || '附件'; },
|
||||||
attachmentExtension(row = {}) { const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0]; const index = source.lastIndexOf('.'); return index > -1 ? source.slice(index + 1).toLowerCase() : ''; },
|
attachmentExtension(row = {}) { const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0]; const index = source.lastIndexOf('.'); return index > -1 ? source.slice(index + 1).toLowerCase() : ''; },
|
||||||
@@ -144,12 +620,16 @@ export default {
|
|||||||
previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; },
|
previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; },
|
||||||
handlePreviewUnsupported() { this.$message.warning('当前文件暂不支持在线预览'); },
|
handlePreviewUnsupported() { this.$message.warning('当前文件暂不支持在线预览'); },
|
||||||
handlePreviewError() { this.$message.error('附件预览失败'); },
|
handlePreviewError() { this.$message.error('附件预览失败'); },
|
||||||
removeContractFile(index) { this.contractFileRows.splice(index, 1); },
|
|
||||||
handleContractFileBatchDownload() { (this.selectedContractFiles.length ? this.selectedContractFiles : this.contractFileRows).forEach(row => { if (row.url) window.open(row.url, '_blank'); }); },
|
|
||||||
formatFileSize(value) { const size = Number(value || 0); return size > 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(2)}MB` : `${Math.max(1, Math.ceil(size / 1024))}KB`; },
|
formatFileSize(value) { const size = Number(value || 0); return size > 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(2)}MB` : `${Math.max(1, Math.ceil(size / 1024))}KB`; },
|
||||||
handleChangeMaterial(event) { if (event.raw) this.changeMaterials.push(event.raw); },
|
|
||||||
addPaymentRatioRow() { this.paymentRatioRows.push({ paymentTerm: `第${this.paymentRatioRows.length + 1}笔`, ratioLimit: '', remark: '' }); },
|
addPaymentRatioRow() { this.paymentRatioRows.push({ paymentTerm: `第${this.paymentRatioRows.length + 1}笔`, ratioLimit: '', remark: '' }); },
|
||||||
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; } const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), 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.changeReason, changeReason: this.form.changeReason }); this.$message.success('变更已提交'); this.$router.back(); },
|
customPeriodEndDayOptions(row) { const startDay = Number(row?.startDay); if (!Number.isFinite(startDay) || startDay < 1) return this.billCutoffDayOptions; return Array.from({ length: 31 - startDay + 1 }, (_, index) => ({ label: `${startDay + index}日`, value: startDay + index })); },
|
||||||
|
handleCustomPeriodStartDayChange(index, value) { const periods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); const row = periods[index]; if (!row) return; row.startDay = value || ''; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (Number.isFinite(startDay) && Number.isFinite(endDay) && (endDay < startDay || endDay > 31)) row.endDay = ''; this.settlementRule.customPeriods = periods; },
|
||||||
|
handleCustomPeriodEndDayChange(index, value) { const periods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); const row = periods[index]; if (!row) return; row.endDay = value || ''; this.settlementRule.customPeriods = periods; },
|
||||||
|
addCustomPeriodRow() { const periods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); periods.push({ startDay: '', endDay: '' }); this.settlementRule.customPeriods = 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; },
|
||||||
|
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) }); this.$message.success('变更已提交'); this.$router.back(); },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -179,13 +659,27 @@ export default {
|
|||||||
.change-table { margin: 16px 0; }
|
.change-table { margin: 16px 0; }
|
||||||
.ratio-tip { margin-bottom: 8px; color: #f56c6c; }
|
.ratio-tip { margin-bottom: 8px; color: #f56c6c; }
|
||||||
.unit { margin-left: 8px; }
|
.unit { margin-left: 8px; }
|
||||||
.attachment-head { display: flex; align-items: center; justify-content: flex-end; margin-bottom: 12px; }
|
|
||||||
.attachment-upload { display: flex; justify-content: flex-start; margin-top: 12px; }
|
.attachment-upload { display: flex; justify-content: flex-start; margin-top: 12px; }
|
||||||
.attachment-upload .vehicle-attachment-upload { width: auto; }
|
.attachment-upload .vehicle-attachment-upload { width: auto; }
|
||||||
.attachment-upload .el-upload { display: inline-flex; }
|
.attachment-upload .el-upload { display: inline-flex; }
|
||||||
.settlement-switch { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
.settlement-switch { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
||||||
|
// 问号提示图标紧靠「开启」一侧:清掉开启 radio 自带的右间距,图标与「关闭」之间留大间距
|
||||||
|
.settlement-switch :deep(.el-radio:first-child) { margin-right: 0; }
|
||||||
|
.settlement-switch-tip { margin: 0 32px 0 8px; color: #a8abb2; cursor: help; font-size: 14px; }
|
||||||
.settlement-form { display: grid; grid-template-columns: repeat(3, minmax(240px, 1fr)); gap: 8px 28px; }
|
.settlement-form { display: grid; grid-template-columns: repeat(3, minmax(240px, 1fr)); gap: 8px 28px; }
|
||||||
.change-reason-section { margin: 20px 0; }
|
.change-reason-section { margin: 20px 0; }
|
||||||
|
.change-reason-section__input {
|
||||||
|
width: 100%;
|
||||||
|
:deep(.el-textarea__inner) {
|
||||||
|
border-color: #f56c6c;
|
||||||
|
box-shadow: 0 0 0 1px #f56c6c inset;
|
||||||
|
}
|
||||||
|
:deep(.el-textarea__inner:hover),
|
||||||
|
:deep(.el-textarea__inner:focus) {
|
||||||
|
border-color: #f56c6c;
|
||||||
|
box-shadow: 0 0 0 1px #f56c6c inset;
|
||||||
|
}
|
||||||
|
}
|
||||||
.page-footer {
|
.page-footer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|||||||
+1199
-512
File diff suppressed because it is too large
Load Diff
+1450
-651
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<basic-container class="master-order-page">
|
<basic-container class="master-order-page">
|
||||||
<template v-if="mode === 'list'">
|
<template v-if="mode === 'list'">
|
||||||
<el-form :model="query" class="master-search" label-width="88px" @submit.prevent>
|
<el-form :model="query" class="master-search" label-width="160px" @submit.prevent>
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col v-for="field in primaryFields" :key="field.prop" :span="6"
|
<el-col v-for="field in primaryFields" :key="field.prop" :span="6"
|
||||||
><el-form-item :label="field.label"
|
><el-form-item :label="field.label"
|
||||||
@@ -262,7 +262,7 @@ export default {
|
|||||||
result.unshift({ text: '调度', run: () => this.goDispatch(row.id) });
|
result.unshift({ text: '调度', run: () => this.goDispatch(row.id) });
|
||||||
if (row.businessStatus !== 'draft')
|
if (row.businessStatus !== 'draft')
|
||||||
result.unshift({ text: '详情', run: () => this.goDetail(row.id) });
|
result.unshift({ text: '详情', run: () => this.goDetail(row.id) });
|
||||||
if (['dispatching', 'completed'].includes(row.businessStatus))
|
if (row.businessStatus === 'dispatching')
|
||||||
result.push({
|
result.push({
|
||||||
text: '关闭调度',
|
text: '关闭调度',
|
||||||
run: () =>
|
run: () =>
|
||||||
@@ -330,23 +330,100 @@ export default {
|
|||||||
const key = keys.find(item => route[item] !== undefined && route[item] !== null);
|
const key = keys.find(item => route[item] !== undefined && route[item] !== null);
|
||||||
return key ? route[key] : 0;
|
return key ? route[key] : 0;
|
||||||
},
|
},
|
||||||
|
isRoadTransportType(type) {
|
||||||
|
const value = String(type || '').trim().toLowerCase();
|
||||||
|
return value === 'road' || value.includes('公路');
|
||||||
|
},
|
||||||
|
formatRoadAddress(value) {
|
||||||
|
const text = String(value || '').replace(/\s+/g, '');
|
||||||
|
if (!text) return '-';
|
||||||
|
// 公路地址按“市 区县”展示,兼容省市区拼接及历史上只保存市/区县的值。
|
||||||
|
const withoutProvince = text.replace(/^.*?(?:省|自治区|特别行政区)/, '');
|
||||||
|
// “梅州市”同时包含“州”和“市”,避免误把城市截断为“梅州”。
|
||||||
|
const cityMatch = withoutProvince.match(/.+?(?:市|州(?!市)|盟(?!市))/);
|
||||||
|
if (!cityMatch) return text;
|
||||||
|
const city = cityMatch[0];
|
||||||
|
const districtSource = withoutProvince.slice(city.length);
|
||||||
|
const districtMatch = districtSource.match(/^[^市州盟]*?(?:区|县|旗)/);
|
||||||
|
if (!districtMatch) return city;
|
||||||
|
// 兼容“梅县区”这类名称,正则首次命中“县”后还需保留后面的“区”。
|
||||||
|
const district = `${districtMatch[0]}${
|
||||||
|
districtSource.slice(districtMatch[0].length).startsWith('区') ? '区' : ''
|
||||||
|
}`;
|
||||||
|
return `${city} ${district}`;
|
||||||
|
},
|
||||||
|
routeNodeName(value, address, transportType, region = {}) {
|
||||||
|
const text = String(value || '').trim();
|
||||||
|
if (!text) return '-';
|
||||||
|
const siteCode = String(region.siteCode || '').trim();
|
||||||
|
// 运输方式切换不应改变地址本身的展示模式:有站点编码时显示站点名称,
|
||||||
|
// 没有站点编码的区域地址统一按“市 区县”格式展示。
|
||||||
|
if (siteCode && siteCode !== '/') return text;
|
||||||
|
// 优先从完整的地址名称/详细地址解析,避免后端只返回“梅州”等不完整的市名称时
|
||||||
|
// 提前返回城市,导致区县被遗漏。
|
||||||
|
const parsedText = this.formatRoadAddress(text);
|
||||||
|
if (parsedText.includes(' ')) return parsedText;
|
||||||
|
const parsedAddress = this.formatRoadAddress(address);
|
||||||
|
if (parsedAddress.includes(' ')) return parsedAddress;
|
||||||
|
const city = String(region.cityName || '').trim();
|
||||||
|
const district = String(region.districtName || '').trim();
|
||||||
|
if (city && district) return `${city} ${district}`;
|
||||||
|
if (city) return city;
|
||||||
|
if (district && address) {
|
||||||
|
const formattedAddress = this.formatRoadAddress(address);
|
||||||
|
const addressCity = formattedAddress.split(' ')[0];
|
||||||
|
if (addressCity && addressCity !== formattedAddress) return `${addressCity} ${district}`;
|
||||||
|
}
|
||||||
|
const isRegional =
|
||||||
|
/省|自治区|特别行政区/.test(text) ||
|
||||||
|
/(?:市|州|盟).*?(?:区|县|旗)/.test(text) ||
|
||||||
|
/(?:区|县|旗)$/.test(text);
|
||||||
|
if (!isRegional) return text;
|
||||||
|
// 区县简称(如“西乡塘区”)从详细地址中补齐所属城市。
|
||||||
|
const source = /省|自治区|特别行政区|市|州|盟/.test(text) ? text : address || text;
|
||||||
|
const formatted = this.formatRoadAddress(source);
|
||||||
|
if (/(?:区|县|旗)$/.test(text) && !formatted.includes(text)) {
|
||||||
|
const city = formatted.split(' ')[0];
|
||||||
|
return city && city !== formatted ? `${city} ${text}` : formatted;
|
||||||
|
}
|
||||||
|
return formatted;
|
||||||
|
},
|
||||||
routeNodes(row = {}) {
|
routeNodes(row = {}) {
|
||||||
const routes = row.routeProgress || [];
|
const routes = row.routeProgress || [];
|
||||||
const total = this.routeNumber(row.totalQuantity);
|
const total = this.routeNumber(row.totalQuantity);
|
||||||
|
const firstTransportType = routes[0]?.transportType || row.routes?.[0]?.transportType || row.transportType || '';
|
||||||
if (!routes.length) {
|
if (!routes.length) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: 'start',
|
key: 'start',
|
||||||
type: 'start',
|
type: 'start',
|
||||||
text: '起',
|
text: '起',
|
||||||
name: row.departureName || row.departureAddress || '-',
|
name: this.routeNodeName(
|
||||||
|
row.departureName || row.departureAddress,
|
||||||
|
row.departureAddress,
|
||||||
|
firstTransportType,
|
||||||
|
{
|
||||||
|
cityName: row.departureCityName,
|
||||||
|
districtName: row.departureDistrictName,
|
||||||
|
siteCode: row.departureSiteCode,
|
||||||
|
}
|
||||||
|
),
|
||||||
lines: [`已调度0/${total}吨`],
|
lines: [`已调度0/${total}吨`],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'end',
|
key: 'end',
|
||||||
type: 'end',
|
type: 'end',
|
||||||
text: '终',
|
text: '终',
|
||||||
name: row.arrivalName || row.arrivalAddress || '-',
|
name: this.routeNodeName(
|
||||||
|
row.arrivalName || row.arrivalAddress,
|
||||||
|
row.arrivalAddress,
|
||||||
|
row.finalTransportType || firstTransportType,
|
||||||
|
{
|
||||||
|
cityName: row.arrivalCityName,
|
||||||
|
districtName: row.arrivalDistrictName,
|
||||||
|
siteCode: row.arrivalSiteCode,
|
||||||
|
}
|
||||||
|
),
|
||||||
lines: [`到达0/${total}吨`],
|
lines: [`到达0/${total}吨`],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -356,7 +433,16 @@ export default {
|
|||||||
key: 'start',
|
key: 'start',
|
||||||
type: 'start',
|
type: 'start',
|
||||||
text: '起',
|
text: '起',
|
||||||
name: row.departureName || row.departureAddress || '-',
|
name: this.routeNodeName(
|
||||||
|
row.departureName || row.departureAddress,
|
||||||
|
row.departureAddress,
|
||||||
|
firstTransportType,
|
||||||
|
{
|
||||||
|
cityName: row.departureCityName,
|
||||||
|
districtName: row.departureDistrictName,
|
||||||
|
siteCode: row.departureSiteCode,
|
||||||
|
}
|
||||||
|
),
|
||||||
lines: [`已调度${this.routeNumber(routes[0]?.dispatchedQuantity)}/${total}吨`],
|
lines: [`已调度${this.routeNumber(routes[0]?.dispatchedQuantity)}/${total}吨`],
|
||||||
},
|
},
|
||||||
...routes.map((route, index) => {
|
...routes.map((route, index) => {
|
||||||
@@ -367,7 +453,16 @@ export default {
|
|||||||
key: route.segmentNo || `route-${index}`,
|
key: route.segmentNo || `route-${index}`,
|
||||||
type: isEnd ? 'end' : 'middle',
|
type: isEnd ? 'end' : 'middle',
|
||||||
text: isEnd ? '终' : '经',
|
text: isEnd ? '终' : '经',
|
||||||
name: route.arrivalName || route.departureName || row.arrivalName || '-',
|
name: this.routeNodeName(
|
||||||
|
route.arrivalName || route.departureName || row.arrivalName,
|
||||||
|
route.arrivalAddress || route.departureAddress,
|
||||||
|
route.transportType,
|
||||||
|
{
|
||||||
|
cityName: route.arrivalCityName || route.departureCityName,
|
||||||
|
districtName: route.arrivalDistrictName || route.departureDistrictName,
|
||||||
|
siteCode: route.arrivalSiteCode || route.departureSiteCode,
|
||||||
|
}
|
||||||
|
),
|
||||||
lines: isEnd
|
lines: isEnd
|
||||||
? [`到达${arrived}/${total}吨`]
|
? [`到达${arrived}/${total}吨`]
|
||||||
: [`到达 ${arrived}/${total}吨`, `已调度 ${dispatched}/到达${arrived}/${total}吨`],
|
: [`到达 ${arrived}/${total}吨`, `已调度 ${dispatched}/到达${arrived}/${total}吨`],
|
||||||
@@ -412,6 +507,9 @@ export default {
|
|||||||
.el-date-editor {
|
.el-date-editor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.search-actions {
|
.search-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
@@ -227,7 +227,7 @@
|
|||||||
<el-radio-group
|
<el-radio-group
|
||||||
v-model="row.confirmMode"
|
v-model="row.confirmMode"
|
||||||
:disabled="dialogReadonly || !row.enabled"
|
:disabled="dialogReadonly || !row.enabled"
|
||||||
@change="syncNodeFields"
|
@change="handleAcceptConfirmChange(row)"
|
||||||
>
|
>
|
||||||
<el-radio label="yes">是</el-radio>
|
<el-radio label="yes">是</el-radio>
|
||||||
<el-radio label="no_confirm_accept">无需确认接单</el-radio>
|
<el-radio label="no_confirm_accept">无需确认接单</el-radio>
|
||||||
@@ -235,12 +235,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="process-config-form__require-line">
|
<div class="process-config-form__require-line">
|
||||||
<span class="process-config-form__require-label">确认接单人</span>
|
<span class="process-config-form__require-label">确认接单人</span>
|
||||||
<el-checkbox
|
<el-checkbox v-model="row.confirmDriver" :disabled="true">司机</el-checkbox>
|
||||||
v-model="row.confirmDriver"
|
|
||||||
:disabled="dialogReadonly || !row.enabled || row.confirmMode !== 'yes'"
|
|
||||||
@change="syncNodeFields"
|
|
||||||
>司机</el-checkbox
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -551,10 +546,19 @@ export default {
|
|||||||
hasPermission(code) {
|
hasPermission(code) {
|
||||||
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
|
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
|
||||||
},
|
},
|
||||||
|
handleAcceptConfirmChange(row) {
|
||||||
|
row.confirmDriver = row.confirmMode === 'yes';
|
||||||
|
this.syncNodeFields();
|
||||||
|
},
|
||||||
handleReturnConfirmChange(row) {
|
handleReturnConfirmChange(row) {
|
||||||
row.confirmInternal = row.confirmMode === 'yes';
|
row.confirmInternal = row.confirmMode === 'yes';
|
||||||
this.syncNodeFields();
|
this.syncNodeFields();
|
||||||
},
|
},
|
||||||
|
enforceAcceptDriver(node) {
|
||||||
|
if (!node || (node.key !== 'accept' && node.name !== '接单')) return node;
|
||||||
|
node.confirmDriver = node.confirmMode === 'yes';
|
||||||
|
return node;
|
||||||
|
},
|
||||||
initDeptOptions() {
|
initDeptOptions() {
|
||||||
getDeptTree(this.userInfo.tenantId).then(res => {
|
getDeptTree(this.userInfo.tenantId).then(res => {
|
||||||
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
|
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
|
||||||
@@ -610,7 +614,7 @@ export default {
|
|||||||
return 'warning';
|
return 'warning';
|
||||||
},
|
},
|
||||||
canEdit(row) {
|
canEdit(row) {
|
||||||
return this.hasPermission('process_config_edit') && !row.readonly;
|
return this.hasPermission('process_config_edit') && !row.readonly && !row.hasRelatedWaybill;
|
||||||
},
|
},
|
||||||
canDelete(row) {
|
canDelete(row) {
|
||||||
return (
|
return (
|
||||||
@@ -626,7 +630,7 @@ export default {
|
|||||||
return this.hasPermission('process_config_disable') && !row.readonly && row.status === 1;
|
return this.hasPermission('process_config_disable') && !row.readonly && row.status === 1;
|
||||||
},
|
},
|
||||||
cloneNode(template) {
|
cloneNode(template) {
|
||||||
return {
|
const node = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
punch: true,
|
punch: true,
|
||||||
location: true,
|
location: true,
|
||||||
@@ -641,10 +645,16 @@ export default {
|
|||||||
supportVoucher: false,
|
supportVoucher: false,
|
||||||
voucherOptions: [],
|
voucherOptions: [],
|
||||||
confirmMode: 'yes',
|
confirmMode: 'yes',
|
||||||
confirmDriver: false,
|
confirmDriver: true,
|
||||||
confirmInternal: false,
|
confirmInternal: false,
|
||||||
...template,
|
...template,
|
||||||
};
|
};
|
||||||
|
if ((node.key === 'accept' || node.name === '接单') && node.confirmMode === 'yes') {
|
||||||
|
node.confirmDriver = true;
|
||||||
|
} else if ((node.key === 'accept' || node.name === '接单') && node.confirmMode !== 'yes') {
|
||||||
|
node.confirmDriver = false;
|
||||||
|
}
|
||||||
|
return node;
|
||||||
},
|
},
|
||||||
resetNodeRows() {
|
resetNodeRows() {
|
||||||
this.nodeRows = NODE_TEMPLATES.map(item => this.cloneNode(item));
|
this.nodeRows = NODE_TEMPLATES.map(item => this.cloneNode(item));
|
||||||
@@ -769,20 +779,28 @@ export default {
|
|||||||
this.syncNodeFields();
|
this.syncNodeFields();
|
||||||
},
|
},
|
||||||
syncNodeFields() {
|
syncNodeFields() {
|
||||||
|
this.nodeRows.forEach(item => this.enforceAcceptDriver(item));
|
||||||
const enabledNodes = this.nodeRows.filter(item => item.enabled);
|
const enabledNodes = this.nodeRows.filter(item => item.enabled);
|
||||||
this.form.includedNodes = enabledNodes.map(item => item.name).join(',');
|
this.form.includedNodes = enabledNodes.map(item => item.name).join(',');
|
||||||
this.form.nodeConfigJson = JSON.stringify(
|
this.form.nodeConfigJson = JSON.stringify(
|
||||||
this.nodeRows.map(item => ({
|
this.nodeRows.map(item => ({
|
||||||
key: item.key,
|
key: item.key,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
|
type: item.type,
|
||||||
enabled: item.enabled,
|
enabled: item.enabled,
|
||||||
confirmMode: item.confirmMode,
|
confirmMode: item.confirmMode,
|
||||||
confirmDriver: item.confirmDriver,
|
confirmDriver: item.confirmDriver,
|
||||||
confirmInternal: item.confirmInternal,
|
confirmInternal: item.confirmInternal,
|
||||||
punch: item.punch,
|
punch: item.punch,
|
||||||
location: item.location,
|
location: item.location,
|
||||||
uploadCargo: item.uploadCargo,
|
// 在途不支持货量;其它节点按配置
|
||||||
cargoTypes: item.uploadCargo ? item.cargoTypes : [],
|
uploadCargo: item.type === 'transit' || item.key === 'transit' ? false : item.uploadCargo,
|
||||||
|
cargoTypes:
|
||||||
|
item.type === 'transit' || item.key === 'transit'
|
||||||
|
? []
|
||||||
|
: item.uploadCargo
|
||||||
|
? item.cargoTypes
|
||||||
|
: [],
|
||||||
frequencyDays: item.frequencyDays,
|
frequencyDays: item.frequencyDays,
|
||||||
timeStart: item.timeStart,
|
timeStart: item.timeStart,
|
||||||
timeEnd: item.timeEnd,
|
timeEnd: item.timeEnd,
|
||||||
@@ -935,7 +953,7 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
openConfig(row, readonly = false) {
|
openConfig(row, readonly = false) {
|
||||||
this.dialogReadonly = readonly;
|
this.dialogReadonly = readonly || !!(row && row.hasRelatedWaybill);
|
||||||
if (!row || !row.id) {
|
if (!row || !row.id) {
|
||||||
this.form = {
|
this.form = {
|
||||||
configName: '',
|
configName: '',
|
||||||
@@ -952,6 +970,7 @@ export default {
|
|||||||
}
|
}
|
||||||
api.getDetail(row.id).then(res => {
|
api.getDetail(row.id).then(res => {
|
||||||
const detail = res.data.data || {};
|
const detail = res.data.data || {};
|
||||||
|
this.dialogReadonly = readonly || !!detail.hasRelatedWaybill;
|
||||||
this.form = {
|
this.form = {
|
||||||
...detail,
|
...detail,
|
||||||
defaultFinishDays:
|
defaultFinishDays:
|
||||||
@@ -969,6 +988,10 @@ export default {
|
|||||||
},
|
},
|
||||||
submitConfig(mode) {
|
submitConfig(mode) {
|
||||||
if (this.submitting) return;
|
if (this.submitting) return;
|
||||||
|
if (this.form.id && this.form.hasRelatedWaybill) {
|
||||||
|
this.$message.warning('该项目已有运单,过程配置不可修改');
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.syncNodeFields();
|
this.syncNodeFields();
|
||||||
const submitRow = this.normalizeRow({
|
const submitRow = this.normalizeRow({
|
||||||
...this.form,
|
...this.form,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<business-crud-page :api="api" :config="config" :crud-option="option" :menu-width="270" standalone-form-page />
|
<shipping-template-page
|
||||||
|
:api="api"
|
||||||
|
:config="config"
|
||||||
|
:crud-option="option"
|
||||||
|
:menu-width="270"
|
||||||
|
standalone-form-page
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import BusinessCrudPage from './components/business-crud-page.vue';
|
import ShippingTemplatePage from './components/shipping-template-page.vue';
|
||||||
import * as api from '@/api/business/shipping-template';
|
import * as api from '@/api/business/shipping-template';
|
||||||
import { config, option } from '@/option/business/shipping-template';
|
import { config, option } from '@/option/business/shipping-template';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { BusinessCrudPage },
|
components: { ShippingTemplatePage },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
api,
|
api,
|
||||||
|
|||||||
@@ -50,11 +50,19 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #projectQuotaInfoTitle-form>
|
||||||
|
<div class="dialog-section-title">项目额度信息</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #temporaryCreditInfoTitle-form>
|
||||||
|
<div class="dialog-section-title">临时额度信息</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #projectName-form>
|
<template #projectName-form>
|
||||||
<el-select
|
<el-select
|
||||||
v-model="selectedProjectId"
|
v-model="selectedProjectId"
|
||||||
class="temporary-credit-limit-page__field"
|
class="temporary-credit-limit-page__field"
|
||||||
placeholder="请选择项目"
|
placeholder="请选择"
|
||||||
filterable
|
filterable
|
||||||
clearable
|
clearable
|
||||||
:loading="projectLoading"
|
:loading="projectLoading"
|
||||||
@@ -75,7 +83,7 @@
|
|||||||
<el-input
|
<el-input
|
||||||
v-model="form.applyLimit"
|
v-model="form.applyLimit"
|
||||||
class="temporary-credit-limit-page__field"
|
class="temporary-credit-limit-page__field"
|
||||||
placeholder="请输入申请临时额度"
|
placeholder="请输入"
|
||||||
:disabled="dialogReadonly"
|
:disabled="dialogReadonly"
|
||||||
@input="handleAmountInput"
|
@input="handleAmountInput"
|
||||||
/>
|
/>
|
||||||
@@ -117,10 +125,10 @@
|
|||||||
v-model="attachmentRows"
|
v-model="attachmentRows"
|
||||||
:readonly="dialogReadonly"
|
:readonly="dialogReadonly"
|
||||||
:file-types="attachmentFileTypes"
|
:file-types="attachmentFileTypes"
|
||||||
:max-size="500"
|
:max-size="50"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
button-text="上传附件"
|
button-text="上传附件"
|
||||||
tip="支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过500M"
|
tip="支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过50M"
|
||||||
@change="handleAttachmentChange"
|
@change="handleAttachmentChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,22 +136,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #menu-form-before>
|
<template #menu-form-before>
|
||||||
<el-button
|
<template v-if="!dialogReadonly">
|
||||||
v-if="!dialogReadonly"
|
<el-button @click="closeFormDialog">取消</el-button>
|
||||||
type="primary"
|
<el-button type="primary" plain :loading="draftLoading" @click="saveDraft">
|
||||||
plain
|
保存
|
||||||
:loading="draftLoading"
|
</el-button>
|
||||||
@click="saveDraft"
|
</template>
|
||||||
>
|
|
||||||
保存
|
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #menu="{ row }">
|
<template #menu="{ row }">
|
||||||
<el-link
|
<el-link
|
||||||
v-if="hasPermission(`${config.permission}_view`)"
|
v-if="hasPermission(`${config.permission}_view`)"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="$refs.crud.rowView(row)"
|
@click="openDetail(row)"
|
||||||
>
|
>
|
||||||
查看
|
查看
|
||||||
</el-link>
|
</el-link>
|
||||||
@@ -162,6 +167,74 @@
|
|||||||
</template>
|
</template>
|
||||||
</avue-crud>
|
</avue-crud>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailVisible"
|
||||||
|
title="查看临时额度申请"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
width="96%"
|
||||||
|
class="temporary-credit-limit-dialog temporary-credit-limit-detail-dialog"
|
||||||
|
>
|
||||||
|
<div v-loading="detailLoading" class="business-crud-page__detail-content">
|
||||||
|
<section
|
||||||
|
v-for="section in detailSections"
|
||||||
|
:key="section.title"
|
||||||
|
class="temporary-credit-limit-detail-dialog__section"
|
||||||
|
>
|
||||||
|
<div class="dialog-section-title">{{ section.title }}</div>
|
||||||
|
<el-descriptions :column="4" class="temporary-credit-limit-detail-dialog__descriptions">
|
||||||
|
<el-descriptions-item
|
||||||
|
v-for="field in section.fields"
|
||||||
|
:key="field[0]"
|
||||||
|
:label="field[1]"
|
||||||
|
:span="field[2] || 1"
|
||||||
|
>
|
||||||
|
{{ formatDetailValue(field[0]) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<div
|
||||||
|
v-if="section.showAttachment"
|
||||||
|
class="temporary-credit-limit-detail-dialog__attachment"
|
||||||
|
>
|
||||||
|
<div class="temporary-credit-limit-page__attachment-head">
|
||||||
|
<span class="temporary-credit-limit-detail-dialog__attachment-label">附件</span>
|
||||||
|
<el-button type="primary" :disabled="!attachmentRows.length" @click="batchDownload">
|
||||||
|
批量下载
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="attachmentRows" empty-text="暂无附件">
|
||||||
|
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||||
|
<el-table-column label="文件名" min-width="240" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link type="primary" @click="previewAttachment(row)">
|
||||||
|
{{ attachmentName(row) }}
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="文件大小" width="120" align="center">
|
||||||
|
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
||||||
|
<el-table-column
|
||||||
|
prop="uploadTime"
|
||||||
|
label="上传时间"
|
||||||
|
:width="config.detailAttachmentUploadTimeWidth || 180"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link type="primary" @click="downloadAttachment(row)">下载</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="detailVisible = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<empty-pagination
|
<empty-pagination
|
||||||
:page="page"
|
:page="page"
|
||||||
@size-change="sizeChange"
|
@size-change="sizeChange"
|
||||||
@@ -311,6 +384,9 @@ export default {
|
|||||||
zoom: true,
|
zoom: true,
|
||||||
},
|
},
|
||||||
attachmentFileTypes,
|
attachmentFileTypes,
|
||||||
|
detailVisible: false,
|
||||||
|
detailLoading: false,
|
||||||
|
detailRow: {},
|
||||||
flowBox: false,
|
flowBox: false,
|
||||||
flowUrl: '',
|
flowUrl: '',
|
||||||
processInstanceId: '',
|
processInstanceId: '',
|
||||||
@@ -321,6 +397,9 @@ export default {
|
|||||||
permissionList() {
|
permissionList() {
|
||||||
return { addBtn: this.canCreate };
|
return { addBtn: this.canCreate };
|
||||||
},
|
},
|
||||||
|
detailSections() {
|
||||||
|
return this.config.detailSections || [];
|
||||||
|
},
|
||||||
isAdmin() {
|
isAdmin() {
|
||||||
const authority = this.userInfo?.authority;
|
const authority = this.userInfo?.authority;
|
||||||
return Array.isArray(authority)
|
return Array.isArray(authority)
|
||||||
@@ -339,14 +418,54 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
buildTableOption() {
|
buildTableOption() {
|
||||||
|
const columns = (option.column || []).map(column => ({ ...column }));
|
||||||
|
const sectionTitleProps = ['projectQuotaInfoTitle', 'temporaryCreditInfoTitle'];
|
||||||
|
const formGroups = [];
|
||||||
|
let current = [];
|
||||||
|
columns.forEach(col => {
|
||||||
|
if (sectionTitleProps.includes(col.prop)) {
|
||||||
|
if (current.length) formGroups.push(current);
|
||||||
|
current = [col];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!current.length) return;
|
||||||
|
// 列表/搜索专用字段不进入表单分组白卡
|
||||||
|
if (col.addDisplay === false && col.editDisplay === false) return;
|
||||||
|
current.push(col);
|
||||||
|
});
|
||||||
|
if (current.length) formGroups.push(current);
|
||||||
|
|
||||||
|
const formSectionProps = new Set();
|
||||||
|
formGroups.forEach(group => {
|
||||||
|
group.forEach(col => {
|
||||||
|
if (col.prop) formSectionProps.add(col.prop);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表单字段全部进 group 拆白卡;Avue 会额外生成空的主列 group,由样式隐藏去掉顶部白横条
|
||||||
return {
|
return {
|
||||||
...option,
|
...option,
|
||||||
addBtn: false,
|
addBtn: false,
|
||||||
viewBtn: false,
|
viewBtn: false,
|
||||||
editBtn: false,
|
editBtn: false,
|
||||||
delBtn: false,
|
delBtn: false,
|
||||||
|
// 隐藏 Avue 默认取消按钮,改由 menu-form-before 按「取消、保存、提交」顺序自定义
|
||||||
|
cancelBtn: false,
|
||||||
menuWidth: 320,
|
menuWidth: 320,
|
||||||
column: (option.column || []).map(column => ({ ...column })),
|
column: columns.map(col => {
|
||||||
|
if (!formSectionProps.has(col.prop)) return col;
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
viewDisplay: false,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
group: formGroups.map(group => ({
|
||||||
|
label: '',
|
||||||
|
arrow: false,
|
||||||
|
column: group,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
hasPermission(code) {
|
hasPermission(code) {
|
||||||
@@ -449,6 +568,32 @@ export default {
|
|||||||
.then(res => this.applyDetail(res.data?.data || {}))
|
.then(res => this.applyDetail(res.data?.data || {}))
|
||||||
.finally(done);
|
.finally(done);
|
||||||
},
|
},
|
||||||
|
openDetail(row) {
|
||||||
|
this.detailVisible = true;
|
||||||
|
this.detailLoading = true;
|
||||||
|
this.detailRow = { ...row };
|
||||||
|
this.attachmentRows = [];
|
||||||
|
this.selectedAttachments = [];
|
||||||
|
this.api
|
||||||
|
.getDetail(row.id)
|
||||||
|
.then(res => {
|
||||||
|
const detail = res.data?.data || {};
|
||||||
|
this.detailRow = { ...detail };
|
||||||
|
this.attachmentRows = this.parseJsonArray(detail.attachmentsJson);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.detailLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
formatDetailValue(prop) {
|
||||||
|
const row = this.detailRow || {};
|
||||||
|
if (prop === 'approvalStatus') {
|
||||||
|
return this.displayStatus(row, 'approvalStatus');
|
||||||
|
}
|
||||||
|
const value = row[prop];
|
||||||
|
if (value === undefined || value === null || value === '') return '-';
|
||||||
|
return value;
|
||||||
|
},
|
||||||
applyDetail(detail) {
|
applyDetail(detail) {
|
||||||
this.form = { ...detail };
|
this.form = { ...detail };
|
||||||
this.selectedProjectId = detail.projectId || '';
|
this.selectedProjectId = detail.projectId || '';
|
||||||
@@ -457,9 +602,10 @@ export default {
|
|||||||
this.syncCurrentProjectOption();
|
this.syncCurrentProjectOption();
|
||||||
},
|
},
|
||||||
normalizeForm(row = this.form) {
|
normalizeForm(row = this.form) {
|
||||||
|
const { projectQuotaInfoTitle, temporaryCreditInfoTitle, ...payload } = row || {};
|
||||||
return {
|
return {
|
||||||
...row,
|
...payload,
|
||||||
applyLimit: row.applyLimit === '' ? '' : Number(row.applyLimit),
|
applyLimit: payload.applyLimit === '' ? '' : Number(payload.applyLimit),
|
||||||
attachmentsJson: JSON.stringify(this.attachmentRows),
|
attachmentsJson: JSON.stringify(this.attachmentRows),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -490,13 +636,20 @@ export default {
|
|||||||
.saveDraft(this.normalizeForm())
|
.saveDraft(this.normalizeForm())
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$message.success('保存成功');
|
this.$message.success('保存成功');
|
||||||
this.$refs.crud?.closeDialog();
|
this.closeFormDialog();
|
||||||
this.onLoad(this.page, this.query);
|
this.onLoad(this.page, this.query);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.draftLoading = false;
|
this.draftLoading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
closeFormDialog() {
|
||||||
|
if (typeof this.$refs.crud?.closeDialog === 'function') {
|
||||||
|
this.$refs.crud.closeDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$refs.crud?.$refs?.dialogForm?.hide?.();
|
||||||
|
},
|
||||||
rowDel(row) {
|
rowDel(row) {
|
||||||
this.removeRows([row]);
|
this.removeRows([row]);
|
||||||
},
|
},
|
||||||
@@ -822,7 +975,6 @@ export default {
|
|||||||
.el-form-item__label {
|
.el-form-item__label {
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
padding-top: 7px; // 视觉补偿:让单行 label 仍接近 input 中线
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 强制必填星号垂直居中于行盒,与第一行汉字字符中心同基线(关键)
|
// 强制必填星号垂直居中于行盒,与第一行汉字字符中心同基线(关键)
|
||||||
@@ -830,15 +982,257 @@ export default {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
vertical-align: middle; // 关键:相对 baseline 上移,与汉字字符中心对齐
|
vertical-align: middle;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 「项目额度信息 / 临时额度信息」小标题:对齐项目管理「项目基本信息」样式
|
||||||
|
.avue-form__group:has(.dialog-section-title) {
|
||||||
|
.dialog-section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 16px 4px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avue-form__group--flex:has(.dialog-section-title),
|
||||||
|
.el-form-item:has(.dialog-section-title) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item:has(.dialog-section-title) .el-form-item__content {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分组白卡:Avue 实际结构为 .avue-form > .el-form > .el-row > .avue-group
|
||||||
|
.avue-form {
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avue-form > .el-form > .el-row {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无分区标题的空分组(Avue 主列残留)隐藏,去掉顶部白横条
|
||||||
|
.avue-form > .el-form > .el-row > .avue-group:not(:has(.dialog-section-title)) {
|
||||||
|
display: none !important;
|
||||||
|
height: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有分区标题的分组渲染为独立白卡
|
||||||
|
.avue-form > .el-form > .el-row > .avue-group:has(.dialog-section-title) {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 0 0 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avue-form > .el-form > .el-row > .avue-group:has(.dialog-section-title):last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avue-group .el-collapse {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avue-group .el-collapse-item__header {
|
||||||
|
display: none !important;
|
||||||
|
height: 0 !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
line-height: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avue-group .avue-group__header {
|
||||||
|
display: none !important;
|
||||||
|
height: 0 !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avue-group .el-collapse-item__wrap {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avue-group .el-collapse-item__content {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡片视觉由外层 avue-group 承担,内层去白底,避免叠卡/顶条
|
||||||
|
.avue-group .avue-form__group {
|
||||||
|
display: flex !important;
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.temporary-credit-limit-dialog .business-crud-page__detail-content .el-descriptions__label {
|
.temporary-credit-limit-dialog .business-crud-page__detail-content .el-descriptions__label {
|
||||||
width: 180px !important;
|
width: 180px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 新增/编辑/查看:高度随内容自适应,超出时再滚动;去掉强制全屏导致的底部大片空白
|
||||||
|
.temporary-credit-limit-dialog.el-dialog {
|
||||||
|
margin-top: 20px !important;
|
||||||
|
margin-bottom: 20px !important;
|
||||||
|
height: auto !important;
|
||||||
|
max-height: calc(100vh - 40px) !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temporary-credit-limit-dialog .el-dialog__header {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temporary-credit-limit-dialog .el-dialog__body {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: calc(100vh - 140px) !important;
|
||||||
|
display: block;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 覆盖全局 .el-dialog .avue-form 白底内边距,避免顶部出现白横条
|
||||||
|
.temporary-credit-limit-dialog:not(.temporary-credit-limit-detail-dialog) .avue-form {
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temporary-credit-limit-dialog:not(.temporary-credit-limit-detail-dialog) .avue-dialog__footer,
|
||||||
|
.temporary-credit-limit-detail-dialog .el-dialog__footer {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 5;
|
||||||
|
flex: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
padding: 12px 20px !important;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid var(--el-border-color-lighter, #ebeef5) !important;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
.el-button + .el-button {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.temporary-credit-limit-detail-dialog {
|
||||||
|
.business-crud-page__detail-content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__section {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 0 0 4px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__descriptions {
|
||||||
|
padding: 0 16px 8px;
|
||||||
|
|
||||||
|
// 取消 Descriptions 表格边框样式,改为纯文本描述列表
|
||||||
|
.el-descriptions__body {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-descriptions__table {
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-descriptions__cell {
|
||||||
|
border: none !important;
|
||||||
|
background: transparent !important;
|
||||||
|
padding: 8px 12px 8px 0;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-descriptions__label {
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 400;
|
||||||
|
white-space: normal;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-descriptions__content {
|
||||||
|
color: #303133;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 16px 4px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__attachment {
|
||||||
|
padding: 8px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__attachment-label {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temporary-credit-limit-page__attachment-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.el-form-item--default .el-form-item__label {
|
.el-form-item--default .el-form-item__label {
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,717 @@
|
|||||||
|
<template>
|
||||||
|
<basic-container class="transport-plan-dispatch-page">
|
||||||
|
<div v-loading="loading" class="transport-plan-dispatch-page__shell">
|
||||||
|
<!-- 顶部信息区 -->
|
||||||
|
<div class="transport-plan-dispatch-page__top">
|
||||||
|
<div class="transport-plan-dispatch-page__heading">
|
||||||
|
<div class="transport-plan-dispatch-page__heading-title">
|
||||||
|
<el-button icon="el-icon-arrow-left" text @click="handleBack">返回</el-button>
|
||||||
|
<span class="transport-plan-dispatch-page__heading-name">计划调度</span>
|
||||||
|
<span class="transport-plan-dispatch-page__heading-no">
|
||||||
|
{{ planData.planNo || planData.loadingNo || '-' }}
|
||||||
|
</span>
|
||||||
|
<el-tag :type="getStatusTagType(planData.businessStatus)" effect="light" class="status-text">
|
||||||
|
{{ planData.businessStatusName || '-' }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag type="primary" effect="light">
|
||||||
|
{{ planData.transportTypeName || '公路整车' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="transport-plan-dispatch-page__summary">
|
||||||
|
<section-card title="基本信息" class="transport-plan-dispatch-page__summary-card">
|
||||||
|
<div class="transport-plan-dispatch-page__summary-grid">
|
||||||
|
<div class="transport-plan-dispatch-page__summary-item">
|
||||||
|
<div class="transport-plan-dispatch-page__summary-label">客户</div>
|
||||||
|
<div class="transport-plan-dispatch-page__summary-value">
|
||||||
|
{{ planData.customerName || '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="transport-plan-dispatch-page__summary-item">
|
||||||
|
<div class="transport-plan-dispatch-page__summary-label">项目</div>
|
||||||
|
<div class="transport-plan-dispatch-page__summary-value">
|
||||||
|
{{ planData.projectName || '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="transport-plan-dispatch-page__summary-item">
|
||||||
|
<div class="transport-plan-dispatch-page__summary-label">合同编号</div>
|
||||||
|
<div class="transport-plan-dispatch-page__summary-value">
|
||||||
|
{{ planData.contractNo || '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="transport-plan-dispatch-page__summary-item">
|
||||||
|
<div class="transport-plan-dispatch-page__summary-label">计划执行时间</div>
|
||||||
|
<div class="transport-plan-dispatch-page__summary-value">
|
||||||
|
{{ formatDateRange(planData.planStartDate, planData.planEndDate) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="transport-plan-dispatch-page__summary-item is-wide">
|
||||||
|
<div class="transport-plan-dispatch-page__summary-label">货物信息</div>
|
||||||
|
<div class="transport-plan-dispatch-page__summary-value">
|
||||||
|
{{ planData.goodsInfo || formatGoodsInfo(planData) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="transport-plan-dispatch-page__summary-item">
|
||||||
|
<div class="transport-plan-dispatch-page__summary-label">附件</div>
|
||||||
|
<div class="transport-plan-dispatch-page__summary-value">
|
||||||
|
<template v-if="attachmentList.length">
|
||||||
|
<el-link
|
||||||
|
v-for="(item, index) in attachmentList"
|
||||||
|
:key="index"
|
||||||
|
type="primary"
|
||||||
|
:href="item.url"
|
||||||
|
:underline="false"
|
||||||
|
target="_blank"
|
||||||
|
class="transport-plan-dispatch-page__attachment-link"
|
||||||
|
>
|
||||||
|
{{ item.name || item.originalName || '附件' }}
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section-card>
|
||||||
|
|
||||||
|
<section-card title="收发货路线" class="transport-plan-dispatch-page__route-card">
|
||||||
|
<div class="transport-plan-dispatch-page__route">
|
||||||
|
<div class="transport-plan-dispatch-page__route-item">
|
||||||
|
<span class="transport-plan-dispatch-page__route-badge is-start">起</span>
|
||||||
|
<div class="transport-plan-dispatch-page__route-body">
|
||||||
|
<div class="transport-plan-dispatch-page__route-title">
|
||||||
|
{{ planData.departureName || '-' }}
|
||||||
|
</div>
|
||||||
|
<div class="transport-plan-dispatch-page__route-address">
|
||||||
|
{{ formatAddress(planData.departureAddress) }}
|
||||||
|
</div>
|
||||||
|
<div class="transport-plan-dispatch-page__route-contact">
|
||||||
|
{{ formatContact(planData.departureContact, planData.departurePhone) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="transport-plan-dispatch-page__route-vehicle">
|
||||||
|
<el-icon><Van /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="transport-plan-dispatch-page__route-item">
|
||||||
|
<span class="transport-plan-dispatch-page__route-badge is-end">终</span>
|
||||||
|
<div class="transport-plan-dispatch-page__route-body">
|
||||||
|
<div class="transport-plan-dispatch-page__route-title">
|
||||||
|
{{ planData.arrivalName || '-' }}
|
||||||
|
</div>
|
||||||
|
<div class="transport-plan-dispatch-page__route-address">
|
||||||
|
{{ formatAddress(planData.arrivalAddress) }}
|
||||||
|
</div>
|
||||||
|
<div class="transport-plan-dispatch-page__route-contact">
|
||||||
|
{{ formatContact(planData.arrivalContact, planData.arrivalPhone) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 调度列表 -->
|
||||||
|
<section-card class="transport-plan-dispatch-page__list-card">
|
||||||
|
<template #title>
|
||||||
|
<span>待调度列表</span>
|
||||||
|
<span class="transport-plan-dispatch-page__list-summary">
|
||||||
|
{{ summaryText }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<el-button type="primary" :disabled="!canAddDispatch" @click="handleAdd">
|
||||||
|
新增
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<el-table
|
||||||
|
:data="dispatchList"
|
||||||
|
border
|
||||||
|
height="100%"
|
||||||
|
row-key="id"
|
||||||
|
class="transport-plan-dispatch-page__table"
|
||||||
|
>
|
||||||
|
<el-table-column type="index" label="序号" width="70" align="center" fixed="left" />
|
||||||
|
<el-table-column label="运输方式" min-width="180" align="center" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.transportTypeName || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="carrierType" label="承运类型" min-width="150" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="carrierName" label="承运商" min-width="150" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="driverName" label="司机" min-width="260" align="center" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ formatDriver(row) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="vehicleNo" label="车牌号" min-width="150" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="cargoType" label="货物类型" min-width="170" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="cargoInfo" label="货物信息" min-width="280" align="center" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.cargoInfo || formatCargoInfo(row) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="发货地址" min-width="220" align="center" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatAddress(row.departureAddress) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="收货地址" min-width="220" align="center" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatAddress(row.arrivalAddress) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="quantity" label="数量" min-width="120" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.quantity ? `${row.quantity}${row.quantityUnit || '吨'}` : '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="unitPrice" label="单价" min-width="120" align="center" />
|
||||||
|
<el-table-column prop="freight" label="运费" min-width="120" align="center" />
|
||||||
|
<el-table-column prop="otherFeeTotal" label="其他费用" min-width="120" align="center" />
|
||||||
|
<el-table-column prop="freightTotal" label="运费合计" min-width="120" align="center" />
|
||||||
|
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||||
|
<template #default="{ row, $index }">
|
||||||
|
<div class="transport-plan-dispatch-page__actions">
|
||||||
|
<el-link type="primary" @click="handleEdit(row, $index)">编辑</el-link>
|
||||||
|
<el-link type="danger" @click="handleDelete($index)">删除</el-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</section-card>
|
||||||
|
|
||||||
|
<!-- 底部按钮 -->
|
||||||
|
<div class="transport-plan-dispatch-page__footer">
|
||||||
|
<el-button @click="handleBack">关闭</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="handleSubmit">
|
||||||
|
确认生成
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 调度明细编辑弹窗 - 将使用原组件的弹窗 -->
|
||||||
|
<transport-plan-page
|
||||||
|
v-if="transportPlanPageVisible"
|
||||||
|
ref="transportPlanPageRef"
|
||||||
|
:api="api"
|
||||||
|
:config="config"
|
||||||
|
:crud-option="option"
|
||||||
|
style="display: none"
|
||||||
|
/>
|
||||||
|
</basic-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Van } from '@element-plus/icons-vue';
|
||||||
|
import { getDetail, dispatch } from '@/api/business/transport-plan';
|
||||||
|
import SectionCard from '@/components/section-card/main.vue';
|
||||||
|
import TransportPlanPage from './components/transport-plan-page.vue';
|
||||||
|
import * as api from '@/api/business/transport-plan';
|
||||||
|
import { config, option } from '@/option/business/transport-plan';
|
||||||
|
|
||||||
|
const TRANSPORT_PLAN_QUANTITY_UNIT = '吨';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TransportPlanDispatch',
|
||||||
|
components: {
|
||||||
|
Van,
|
||||||
|
SectionCard,
|
||||||
|
TransportPlanPage,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
saving: false,
|
||||||
|
planData: {},
|
||||||
|
dispatchList: [],
|
||||||
|
attachmentList: [],
|
||||||
|
transportPlanPageVisible: false,
|
||||||
|
api,
|
||||||
|
config,
|
||||||
|
option,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
planId() {
|
||||||
|
return this.$route.query.planId;
|
||||||
|
},
|
||||||
|
summaryText() {
|
||||||
|
const goodsList = this.parseGoodsList(this.planData.goodsJson);
|
||||||
|
if (!goodsList.length) {
|
||||||
|
const total = Number(this.planData.totalQuantity || 0);
|
||||||
|
const dispatched = this.calculateDispatchedQuantity();
|
||||||
|
const remaining = Math.max(total - dispatched, 0);
|
||||||
|
return `共${total}吨 | 已调度 ${dispatched.toFixed(2)}吨,剩余${remaining.toFixed(2)}吨`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = {};
|
||||||
|
goodsList.forEach(goods => {
|
||||||
|
const unit = goods.quantityUnit || TRANSPORT_PLAN_QUANTITY_UNIT;
|
||||||
|
if (!summary[unit]) {
|
||||||
|
summary[unit] = { total: 0, dispatched: 0 };
|
||||||
|
}
|
||||||
|
summary[unit].total += Number(goods.quantity || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dispatchList.forEach(item => {
|
||||||
|
const unit = item.quantityUnit || TRANSPORT_PLAN_QUANTITY_UNIT;
|
||||||
|
if (!summary[unit]) {
|
||||||
|
summary[unit] = { total: 0, dispatched: 0 };
|
||||||
|
}
|
||||||
|
summary[unit].dispatched += Number(item.quantity || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const parts = Object.entries(summary).map(([unit, data]) => {
|
||||||
|
const remaining = Math.max(data.total - data.dispatched, 0);
|
||||||
|
return `${data.total}${unit} | 已调度 ${data.dispatched.toFixed(2)}${unit},剩余${remaining.toFixed(
|
||||||
|
2
|
||||||
|
)}${unit}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return parts.join(' | ');
|
||||||
|
},
|
||||||
|
canAddDispatch() {
|
||||||
|
const goodsList = this.parseGoodsList(this.planData.goodsJson);
|
||||||
|
if (!goodsList.length) {
|
||||||
|
const total = Number(this.planData.totalQuantity || 0);
|
||||||
|
const dispatched = this.calculateDispatchedQuantity();
|
||||||
|
return dispatched < total;
|
||||||
|
}
|
||||||
|
|
||||||
|
return goodsList.some(goods => {
|
||||||
|
const unit = goods.quantityUnit || TRANSPORT_PLAN_QUANTITY_UNIT;
|
||||||
|
const totalQty = Number(goods.quantity || 0);
|
||||||
|
const dispatchedQty = this.dispatchList
|
||||||
|
.filter(item => item.quantityUnit === unit)
|
||||||
|
.reduce((sum, item) => sum + Number(item.quantity || 0), 0);
|
||||||
|
return dispatchedQty < totalQty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadPlanData();
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// 初始化 transport-plan-page 组件用于打开弹窗
|
||||||
|
this.transportPlanPageVisible = true;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadPlanData() {
|
||||||
|
if (!this.planId) {
|
||||||
|
this.$message.error('缺少计划ID参数');
|
||||||
|
this.handleBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
getDetail(this.planId)
|
||||||
|
.then(res => {
|
||||||
|
if (res.data?.success) {
|
||||||
|
this.planData = res.data.data || {};
|
||||||
|
this.attachmentList = this.parseAttachments(this.planData.attachmentsJson);
|
||||||
|
this.dispatchList = this.parseDispatchList(this.planData);
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.data?.msg || '加载数据失败');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('加载计划数据失败:', err);
|
||||||
|
this.$message.error('加载数据失败');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
parseAttachments(json) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(json || '[]');
|
||||||
|
return Array.isArray(data) ? data : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseDispatchList(planData) {
|
||||||
|
try {
|
||||||
|
const sources = [
|
||||||
|
planData.waybillList,
|
||||||
|
planData.dispatchRows,
|
||||||
|
planData.dispatchRecordList,
|
||||||
|
planData.dispatchList,
|
||||||
|
];
|
||||||
|
for (const source of sources) {
|
||||||
|
if (Array.isArray(source) && source.length) {
|
||||||
|
return source.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
id: item.id || `temp-${index}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseGoodsList(json) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(json || '[]');
|
||||||
|
return Array.isArray(data) ? data : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
calculateDispatchedQuantity() {
|
||||||
|
return this.dispatchList.reduce((sum, item) => sum + Number(item.quantity || 0), 0);
|
||||||
|
},
|
||||||
|
getStatusTagType(status) {
|
||||||
|
const statusMap = {
|
||||||
|
0: 'info',
|
||||||
|
1: 'warning',
|
||||||
|
2: 'success',
|
||||||
|
3: 'danger',
|
||||||
|
};
|
||||||
|
return statusMap[status] || 'info';
|
||||||
|
},
|
||||||
|
formatDateRange(startDate, endDate) {
|
||||||
|
const dates = [startDate, endDate].filter(Boolean);
|
||||||
|
return dates.length ? dates.join(' ~ ') : '-';
|
||||||
|
},
|
||||||
|
formatAddress(address) {
|
||||||
|
if (!address) return '-';
|
||||||
|
const match = address.match(/^(.*?省)?(.*?市)?(.*?区|.*?县)?/);
|
||||||
|
return match ? match[0] || address : address;
|
||||||
|
},
|
||||||
|
formatContact(contact, phone) {
|
||||||
|
const parts = [contact, phone].filter(Boolean);
|
||||||
|
return parts.length ? parts.join(' / ') : '-';
|
||||||
|
},
|
||||||
|
formatDriver(row) {
|
||||||
|
const parts = [row.driverName, row.driverPhone].filter(Boolean);
|
||||||
|
return parts.length ? parts.join(' / ') : '-';
|
||||||
|
},
|
||||||
|
formatGoodsInfo(data) {
|
||||||
|
const goodsList = this.parseGoodsList(data.goodsJson);
|
||||||
|
if (!goodsList.length) {
|
||||||
|
return data.cargoName || '-';
|
||||||
|
}
|
||||||
|
return goodsList
|
||||||
|
.map(goods => {
|
||||||
|
const parts = [goods.cargoName || goods.goodsName];
|
||||||
|
if (goods.quantity) {
|
||||||
|
parts.push(`${goods.quantity}${goods.quantityUnit || TRANSPORT_PLAN_QUANTITY_UNIT}`);
|
||||||
|
}
|
||||||
|
return parts.join(' ');
|
||||||
|
})
|
||||||
|
.join(';');
|
||||||
|
},
|
||||||
|
formatCargoInfo(row) {
|
||||||
|
const parts = [row.cargoName];
|
||||||
|
if (row.specification) parts.push(row.specification);
|
||||||
|
if (row.model) parts.push(row.model);
|
||||||
|
return parts.filter(Boolean).join(' / ') || '-';
|
||||||
|
},
|
||||||
|
handleBack() {
|
||||||
|
this.$router.back();
|
||||||
|
},
|
||||||
|
handleAdd() {
|
||||||
|
if (!this.canAddDispatch) {
|
||||||
|
this.$message.warning('待调度列表货物总量已达到计划总量,不能新增调度明细');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用 transport-plan-page 组件的方法打开弹窗
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const component = this.$refs.transportPlanPageRef;
|
||||||
|
if (component) {
|
||||||
|
// 设置调度数据
|
||||||
|
component.dispatchRow = this.planData;
|
||||||
|
component.dispatchRows = [...this.dispatchList];
|
||||||
|
// 打开新增弹窗
|
||||||
|
component.openDispatchItemDialog(-1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleEdit(row, index) {
|
||||||
|
// 调用 transport-plan-page 组件的方法打开弹窗
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const component = this.$refs.transportPlanPageRef;
|
||||||
|
if (component) {
|
||||||
|
// 设置调度数据
|
||||||
|
component.dispatchRow = this.planData;
|
||||||
|
component.dispatchRows = [...this.dispatchList];
|
||||||
|
// 打开编辑弹窗
|
||||||
|
component.openDispatchItemDialog(index, row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleDelete(index) {
|
||||||
|
this.$confirm('确定删除该调度明细?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}).then(() => {
|
||||||
|
this.dispatchList.splice(index, 1);
|
||||||
|
this.$message.success('删除成功');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSubmit() {
|
||||||
|
if (!this.dispatchList.length) {
|
||||||
|
this.$message.warning('请先添加调度明细');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [index, row] of this.dispatchList.entries()) {
|
||||||
|
if (!row.carrierName) {
|
||||||
|
this.$message.warning(`第${index + 1}条调度明细缺少承运商信息`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!row.driverName) {
|
||||||
|
this.$message.warning(`第${index + 1}条调度明细缺少司机信息`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!row.vehicleNo) {
|
||||||
|
this.$message.warning(`第${index + 1}条调度明细缺少车牌号`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$confirm(
|
||||||
|
`请确认调度信息,将生成 <span style="color: #409eff; font-size: 20px; padding: 0 4px;">${this.dispatchList.length}</span> 条运单,是否继续?`,
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
this.submitDispatch();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
submitDispatch() {
|
||||||
|
this.saving = true;
|
||||||
|
const payload = {
|
||||||
|
id: this.planId,
|
||||||
|
mode: 'submit',
|
||||||
|
waybills: this.dispatchList.map(row => ({
|
||||||
|
...row,
|
||||||
|
taskEntryMode: row.taskEntryMode || 'simple',
|
||||||
|
taskRemark: row.remark || '',
|
||||||
|
estimatedStartTime: row.estimatedStartTime || this.planData.planStartDate || '',
|
||||||
|
estimatedEndTime: row.estimatedEndTime || this.planData.planEndDate || '',
|
||||||
|
goodsJson:
|
||||||
|
row.goodsJson ||
|
||||||
|
JSON.stringify([
|
||||||
|
{
|
||||||
|
cargoName: row.cargoName,
|
||||||
|
cargoType: row.cargoType,
|
||||||
|
quantity: row.quantity,
|
||||||
|
quantityUnit: row.quantityUnit,
|
||||||
|
specification: row.specification,
|
||||||
|
model: row.model,
|
||||||
|
packageType: row.packageType,
|
||||||
|
brand: row.brand,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(payload)
|
||||||
|
.then(res => {
|
||||||
|
if (res.data?.success) {
|
||||||
|
this.$message.success('调度成功,运单已生成');
|
||||||
|
this.handleBack();
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.data?.msg || '调度失败');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('调度失败:', err);
|
||||||
|
this.$message.error('调度失败');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.saving = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.transport-plan-dispatch-page {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&__shell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__top {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__heading {
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-name {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-no {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__summary {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
&-card {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&.is-wide {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__route-card {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__route {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-badge {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.is-start {
|
||||||
|
background: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-end {
|
||||||
|
background: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-address,
|
||||||
|
&-contact {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-vehicle {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list-card {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list-summary {
|
||||||
|
margin-left: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__table {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__attachment-link {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,732 @@
|
|||||||
|
<template>
|
||||||
|
<basic-container class="transport-plan-import-page">
|
||||||
|
<div class="transport-plan-import-page__form-wrapper">
|
||||||
|
<div class="transport-plan-import-page__title">导入运输计划</div>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
ref="importFormRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
label-position="right"
|
||||||
|
label-width="auto"
|
||||||
|
class="transport-plan-import-page__form"
|
||||||
|
>
|
||||||
|
<el-row :gutter="24">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="项目" prop="projectId" required>
|
||||||
|
<el-select
|
||||||
|
v-model="form.projectId"
|
||||||
|
placeholder="模糊查询后选择"
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
:loading="projectLoading"
|
||||||
|
@visible-change="visible => visible && loadProjectOptions()"
|
||||||
|
@change="handleProjectChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in projectOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.projectName"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="客户" prop="customerName" required>
|
||||||
|
<el-input v-model="form.customerName" disabled placeholder="选择项目后带出" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="客户合同" prop="contractId" required>
|
||||||
|
<el-select
|
||||||
|
v-model="form.contractId"
|
||||||
|
placeholder="选择项目后选择"
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
:loading="contractLoading"
|
||||||
|
:disabled="!form.projectId"
|
||||||
|
@change="handleContractChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in contractOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.contractName"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item
|
||||||
|
label="计划明细表"
|
||||||
|
prop="file"
|
||||||
|
required
|
||||||
|
class="transport-plan-import-page__file-item"
|
||||||
|
>
|
||||||
|
<el-upload
|
||||||
|
action="#"
|
||||||
|
accept=".xls,.xlsx"
|
||||||
|
:auto-upload="false"
|
||||||
|
:limit="1"
|
||||||
|
:file-list="files"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
:on-remove="handleFileRemove"
|
||||||
|
>
|
||||||
|
<el-button type="primary">添加附件</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<span class="transport-plan-import-page__file-tip">
|
||||||
|
请上传计划明细表,仅支持 Excel 格式
|
||||||
|
</span>
|
||||||
|
<el-link type="primary" @click="handleTemplateDownload">下载模板</el-link>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section v-if="rows.length" class="transport-plan-import-page__preview">
|
||||||
|
<div class="transport-plan-import-page__preview-head">
|
||||||
|
<div class="dialog-section-title">数据明细</div>
|
||||||
|
<el-button type="danger" plain :disabled="!selection.length" @click="removeSelectedRows">
|
||||||
|
批量删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-tabs v-model="activeTab" type="border-card" class="transport-plan-import-page__tabs">
|
||||||
|
<el-tab-pane :label="`计划单数(${rows.length})`" name="all" />
|
||||||
|
<el-tab-pane :label="`疑似重复(${duplicateCount})`" name="duplicate" />
|
||||||
|
</el-tabs>
|
||||||
|
<div class="transport-plan-import-page__table">
|
||||||
|
<el-table
|
||||||
|
:data="visibleRows"
|
||||||
|
border
|
||||||
|
row-key="_key"
|
||||||
|
empty-text="暂无明细数据"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="52" fixed="left" />
|
||||||
|
<el-table-column type="index" label="序号" width="70" fixed="left" />
|
||||||
|
<el-table-column label="计划单号" min-width="150">
|
||||||
|
<template #default>待生成</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
v-for="column in previewColumns"
|
||||||
|
:key="column.prop"
|
||||||
|
:label="column.label"
|
||||||
|
:min-width="column.width"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-select
|
||||||
|
v-if="isEditingRow(row) && column.prop === 'quantityUnit'"
|
||||||
|
v-model="row[column.prop]"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in quantityUnitOptions"
|
||||||
|
:key="item"
|
||||||
|
:label="item"
|
||||||
|
:value="item"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-input
|
||||||
|
v-else-if="isEditingRow(row)"
|
||||||
|
v-model="row[column.prop]"
|
||||||
|
:maxlength="column.prop === 'remark' ? 200 : undefined"
|
||||||
|
:show-word-limit="column.prop === 'remark'"
|
||||||
|
/>
|
||||||
|
<span v-else>{{ row[column.prop] || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="170" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="transport-plan-import-page__actions">
|
||||||
|
<template v-if="isEditingRow(row)">
|
||||||
|
<el-link type="primary" @click="saveRow">保存</el-link>
|
||||||
|
<el-link type="primary" @click="cancelEditRow(row)">取消</el-link>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-link type="primary" @click="editRow(row)">编辑</el-link>
|
||||||
|
<el-link type="danger" @click="removeRow(row)">删除</el-link>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="transport-plan-import-page__footer">
|
||||||
|
<el-button :disabled="loading" @click="handleCancel">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="loading" @click="handleSubmit">提交</el-button>
|
||||||
|
</div>
|
||||||
|
</basic-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { exportBlob } from '@/api/common';
|
||||||
|
import { getList as getContractList } from '@/api/business/contract-manage';
|
||||||
|
import { getList as getProjectList } from '@/api/business/project-apply';
|
||||||
|
import { importTransportPlan, validateTransportPlan } from '@/api/business/transport-plan';
|
||||||
|
import { config } from '@/option/business/transport-plan';
|
||||||
|
import { downloadXls } from '@/utils/util';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const importColumns = [
|
||||||
|
['计划名称', 'planName'],
|
||||||
|
['运输类型', 'transportType'],
|
||||||
|
['发货地址', 'departureAddress'],
|
||||||
|
['发货联系人', 'departureContact'],
|
||||||
|
['发货联系人电话', 'departurePhone'],
|
||||||
|
['到货地址', 'arrivalAddress'],
|
||||||
|
['收货联系人', 'arrivalContact'],
|
||||||
|
['收货联系人电话', 'arrivalPhone'],
|
||||||
|
['货物名称', 'cargoName'],
|
||||||
|
['货物类型', 'cargoType'],
|
||||||
|
['数量', 'quantity'],
|
||||||
|
['计量单位', 'quantityUnit'],
|
||||||
|
['包装', 'packageType'],
|
||||||
|
['规格', 'specification'],
|
||||||
|
['型号', 'model'],
|
||||||
|
['里程(km)', 'mileage'],
|
||||||
|
['计划开始时间', 'planStartDate'],
|
||||||
|
['计划结束时间', 'planEndDate'],
|
||||||
|
['备注', 'remark'],
|
||||||
|
['同一计划标识号', 'planGroupId'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const previewColumns = [
|
||||||
|
{ label: '计划名称', prop: 'planName', width: 180 },
|
||||||
|
{ label: '运输类型', prop: 'transportType', width: 120 },
|
||||||
|
{ label: '发货地址', prop: 'departureAddress', width: 260 },
|
||||||
|
{ label: '发货联系人', prop: 'departureContact', width: 120 },
|
||||||
|
{ label: '发货联系人电话', prop: 'departurePhone', width: 140 },
|
||||||
|
{ label: '到货地址', prop: 'arrivalAddress', width: 260 },
|
||||||
|
{ label: '收货联系人', prop: 'arrivalContact', width: 120 },
|
||||||
|
{ label: '收货联系人电话', prop: 'arrivalPhone', width: 140 },
|
||||||
|
{ label: '货物名称', prop: 'cargoName', width: 140 },
|
||||||
|
{ label: '货物类型', prop: 'cargoType', width: 120 },
|
||||||
|
{ label: '数量', prop: 'quantity', width: 100 },
|
||||||
|
{ label: '计量单位', prop: 'quantityUnit', width: 110 },
|
||||||
|
{ label: '包装', prop: 'packageType', width: 100 },
|
||||||
|
{ label: '规格', prop: 'specification', width: 120 },
|
||||||
|
{ label: '型号', prop: 'model', width: 120 },
|
||||||
|
{ label: '里程(km)', prop: 'mileage', width: 120 },
|
||||||
|
{ label: '计划开始时间', prop: 'planStartDate', width: 140 },
|
||||||
|
{ label: '计划结束时间', prop: 'planEndDate', width: 140 },
|
||||||
|
{ label: '备注', prop: 'remark', width: 180 },
|
||||||
|
{ label: '同一计划标识号', prop: 'planGroupId', width: 150 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const extractRecords = res => {
|
||||||
|
const data = res?.data?.data || res?.data || res;
|
||||||
|
if (Array.isArray(data)) return data;
|
||||||
|
if (Array.isArray(data?.records)) return data.records;
|
||||||
|
if (Array.isArray(data?.data)) return data.data;
|
||||||
|
if (Array.isArray(data?.data?.records)) return data.data.records;
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultForm = () => ({
|
||||||
|
projectId: '',
|
||||||
|
projectName: '',
|
||||||
|
customerName: '',
|
||||||
|
contractId: '',
|
||||||
|
contractName: '',
|
||||||
|
file: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TransportPlanImport',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: defaultForm(),
|
||||||
|
rules: {
|
||||||
|
projectId: [{ required: true, message: '请选择项目', trigger: 'change' }],
|
||||||
|
customerName: [{ required: true, message: '请选择项目后带出客户', trigger: 'change' }],
|
||||||
|
contractId: [{ required: true, message: '请选择项目后带出客户合同', trigger: 'change' }],
|
||||||
|
file: [{ required: true, message: '请上传计划明细表', trigger: 'change' }],
|
||||||
|
},
|
||||||
|
projectOptions: [],
|
||||||
|
projectLoading: false,
|
||||||
|
contractOptions: [],
|
||||||
|
contractLoading: false,
|
||||||
|
files: [],
|
||||||
|
sourceFile: null,
|
||||||
|
rows: [],
|
||||||
|
selection: [],
|
||||||
|
activeTab: 'all',
|
||||||
|
editingKey: '',
|
||||||
|
editSnapshot: null,
|
||||||
|
loading: false,
|
||||||
|
previewColumns,
|
||||||
|
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
duplicateCount() {
|
||||||
|
return this.rows.filter(row => row._duplicate).length;
|
||||||
|
},
|
||||||
|
visibleRows() {
|
||||||
|
return this.activeTab === 'duplicate' ? this.rows.filter(row => row._duplicate) : this.rows;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadProjectOptions();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadProjectOptions() {
|
||||||
|
if (this.projectLoading) return;
|
||||||
|
this.projectLoading = true;
|
||||||
|
getProjectList(1, 9999, config.projectQueryParams || {})
|
||||||
|
.then(res => {
|
||||||
|
this.projectOptions = extractRecords(res);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.projectLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleProjectChange(projectId) {
|
||||||
|
const project = this.projectOptions.find(item => String(item.id) === String(projectId));
|
||||||
|
this.form.customerName =
|
||||||
|
project?.customerName || project?.customerNames || project?.customer || '';
|
||||||
|
this.form.projectName = project?.projectName || '';
|
||||||
|
this.form.contractId = '';
|
||||||
|
this.form.contractName = '';
|
||||||
|
this.contractOptions = [];
|
||||||
|
if (!projectId) return;
|
||||||
|
this.contractLoading = true;
|
||||||
|
getContractList(1, 9999, {
|
||||||
|
projectId,
|
||||||
|
projectName: project?.projectName,
|
||||||
|
...(config.contractQueryParams || {}),
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
this.contractOptions = extractRecords(res);
|
||||||
|
const contract = this.contractOptions[0];
|
||||||
|
this.form.customerName =
|
||||||
|
this.form.customerName || contract?.customerName || contract?.partyA || '';
|
||||||
|
this.form.contractId = contract?.id || '';
|
||||||
|
this.form.contractName = contract?.contractName || '';
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.contractLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleContractChange(contractId) {
|
||||||
|
const contract = this.contractOptions.find(item => String(item.id) === String(contractId));
|
||||||
|
this.form.contractName = contract?.contractName || '';
|
||||||
|
this.form.customerName =
|
||||||
|
contract?.customerName || contract?.partyA || this.form.customerName || '';
|
||||||
|
},
|
||||||
|
async handleFileChange(file, fileList) {
|
||||||
|
if (!/\.(xls|xlsx)$/i.test(file.name || '')) {
|
||||||
|
this.$message.error('请上传 .xls 或 .xlsx 格式文件');
|
||||||
|
this.files = [];
|
||||||
|
this.form.file = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const XLSX = await import('xlsx');
|
||||||
|
const workbook = XLSX.read(await file.raw.arrayBuffer(), {
|
||||||
|
type: 'array',
|
||||||
|
cellDates: false,
|
||||||
|
});
|
||||||
|
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||||
|
const values = XLSX.utils.sheet_to_json(worksheet, {
|
||||||
|
header: 1,
|
||||||
|
defval: '',
|
||||||
|
raw: false,
|
||||||
|
blankrows: false,
|
||||||
|
});
|
||||||
|
const headers = values[0] || [];
|
||||||
|
const headerMap = headers.reduce((result, label, index) => {
|
||||||
|
const normalized = String(label || '')
|
||||||
|
.replace(/^\*/, '')
|
||||||
|
.trim();
|
||||||
|
if (normalized) result[normalized] = index;
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
const rows = values.slice(1).reduce((result, cells, index) => {
|
||||||
|
const row = { _key: `${Date.now()}-${index}-${Math.random().toString(36).slice(2)}` };
|
||||||
|
importColumns.forEach(([label, prop]) => {
|
||||||
|
row[prop] = String(cells[headerMap[label]] ?? '').trim();
|
||||||
|
});
|
||||||
|
if (importColumns.some(([, prop]) => row[prop])) result.push(row);
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
|
if (!rows.length) {
|
||||||
|
this.$message.warning('未读取到计划明细数据');
|
||||||
|
this.handleFileRemove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.files = fileList.slice(-1);
|
||||||
|
this.sourceFile = file.raw;
|
||||||
|
this.form.file = file.raw;
|
||||||
|
this.rows = rows;
|
||||||
|
this.markDuplicates();
|
||||||
|
this.$refs.importFormRef?.validateField('file');
|
||||||
|
this.$message.success(`已读取${rows.length}条计划明细`);
|
||||||
|
|
||||||
|
// 调用后端校验接口
|
||||||
|
await this.performValidation();
|
||||||
|
} catch (error) {
|
||||||
|
this.handleFileRemove();
|
||||||
|
this.$message.error('计划明细表读取失败,请检查文件格式');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleFileRemove() {
|
||||||
|
this.files = [];
|
||||||
|
this.sourceFile = null;
|
||||||
|
this.form.file = null;
|
||||||
|
this.rows = [];
|
||||||
|
this.selection = [];
|
||||||
|
this.editingKey = '';
|
||||||
|
this.editSnapshot = null;
|
||||||
|
},
|
||||||
|
async performValidation() {
|
||||||
|
if (!this.rows.length || !this.form.file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 校验表单必填项
|
||||||
|
await this.$refs.importFormRef?.validate();
|
||||||
|
|
||||||
|
const response = await validateTransportPlan({
|
||||||
|
file: this.form.file,
|
||||||
|
projectId: this.form.projectId,
|
||||||
|
projectName: this.form.projectName,
|
||||||
|
customerName: this.form.customerName,
|
||||||
|
contractId: this.form.contractId,
|
||||||
|
contractName: this.form.contractName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查响应类型
|
||||||
|
const blob = response.data || response;
|
||||||
|
|
||||||
|
// 如果是 Blob,需要检查其 MIME 类型
|
||||||
|
if (blob instanceof Blob) {
|
||||||
|
// Excel 文件的 MIME 类型
|
||||||
|
if (blob.type.includes('application/vnd.ms-excel') ||
|
||||||
|
blob.type.includes('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')) {
|
||||||
|
// 校验失败,下载错误明细
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `运输计划导入失败明细_${dayjs().format('YYYYMMDDHHmmss')}.xlsx`;
|
||||||
|
link.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
this.$message.warning('数据校验失败,已自动下载错误明细表,请修正后重新上传');
|
||||||
|
} else if (blob.type.includes('application/json')) {
|
||||||
|
// 可能是 JSON 响应被当作 Blob,需要解析
|
||||||
|
const text = await blob.text();
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
if (json.success) {
|
||||||
|
this.$message.success('数据校验通过');
|
||||||
|
} else {
|
||||||
|
this.$message.error(json.msg || '校验失败');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 其他类型,默认认为是校验通过
|
||||||
|
this.$message.success('数据校验通过');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 不是 Blob,直接判断
|
||||||
|
this.$message.success('数据校验通过');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('校验失败:', error);
|
||||||
|
// 表单校验失败时不提示错误,让表单自己显示错误信息
|
||||||
|
if (error && error.message && !error.message.includes('validate')) {
|
||||||
|
this.$message.error('校验接口调用失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
duplicateKey(row) {
|
||||||
|
return importColumns.map(([, prop]) => String(row[prop] || '').trim()).join('\u0001');
|
||||||
|
},
|
||||||
|
markDuplicates() {
|
||||||
|
const countMap = this.rows.reduce((result, row) => {
|
||||||
|
const key = this.duplicateKey(row);
|
||||||
|
result[key] = (result[key] || 0) + 1;
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
this.rows.forEach(row => {
|
||||||
|
row._duplicate = countMap[this.duplicateKey(row)] > 1;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSelectionChange(rows) {
|
||||||
|
this.selection = rows;
|
||||||
|
},
|
||||||
|
isEditingRow(row) {
|
||||||
|
return this.editingKey === row._key;
|
||||||
|
},
|
||||||
|
editRow(row) {
|
||||||
|
if (this.editingKey && this.editingKey !== row._key) {
|
||||||
|
this.$message.warning('请先保存或取消当前编辑');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.editingKey = row._key;
|
||||||
|
this.editSnapshot = { ...row };
|
||||||
|
},
|
||||||
|
saveRow() {
|
||||||
|
this.editingKey = '';
|
||||||
|
this.editSnapshot = null;
|
||||||
|
this.markDuplicates();
|
||||||
|
},
|
||||||
|
cancelEditRow(row) {
|
||||||
|
Object.assign(row, this.editSnapshot || {});
|
||||||
|
this.editingKey = '';
|
||||||
|
this.editSnapshot = null;
|
||||||
|
this.markDuplicates();
|
||||||
|
},
|
||||||
|
removeRow(row) {
|
||||||
|
this.rows = this.rows.filter(item => item._key !== row._key);
|
||||||
|
this.selection = this.selection.filter(item => item._key !== row._key);
|
||||||
|
this.markDuplicates();
|
||||||
|
},
|
||||||
|
removeSelectedRows() {
|
||||||
|
const selectedKeys = new Set(this.selection.map(row => row._key));
|
||||||
|
this.rows = this.rows.filter(row => !selectedKeys.has(row._key));
|
||||||
|
this.selection = [];
|
||||||
|
this.markDuplicates();
|
||||||
|
},
|
||||||
|
validateRows() {
|
||||||
|
if (!this.rows.length) {
|
||||||
|
this.$message.warning('请保留至少一条计划明细');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const requiredFields = [
|
||||||
|
['planName', '计划名称'],
|
||||||
|
['transportType', '运输类型'],
|
||||||
|
['departureAddress', '发货地址'],
|
||||||
|
['arrivalAddress', '到货地址'],
|
||||||
|
['cargoType', '货物类型'],
|
||||||
|
];
|
||||||
|
const isValidDate = value => {
|
||||||
|
const match = String(value || '').match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) return false;
|
||||||
|
const [, year, month, day] = match;
|
||||||
|
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
||||||
|
return (
|
||||||
|
date.getFullYear() === Number(year) &&
|
||||||
|
date.getMonth() === Number(month) - 1 &&
|
||||||
|
date.getDate() === Number(day)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
for (const [index, row] of this.rows.entries()) {
|
||||||
|
const emptyField = requiredFields.find(([prop]) => !String(row[prop] || '').trim());
|
||||||
|
if (emptyField) {
|
||||||
|
this.$message.warning(`第${index + 1}行${emptyField[1]}不能为空`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (row.planStartDate && !isValidDate(row.planStartDate)) {
|
||||||
|
this.$message.warning(`第${index + 1}行计划开始时间格式必须为 YYYY-MM-DD`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (row.planEndDate && !isValidDate(row.planEndDate)) {
|
||||||
|
this.$message.warning(`第${index + 1}行计划结束时间格式必须为 YYYY-MM-DD`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (row.planStartDate && row.planEndDate && row.planEndDate < row.planStartDate) {
|
||||||
|
this.$message.warning(`第${index + 1}行计划结束时间不能早于计划开始时间`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
row.quantity !== '' &&
|
||||||
|
(!Number.isFinite(Number(row.quantity)) || Number(row.quantity) < 0)
|
||||||
|
) {
|
||||||
|
this.$message.warning(`第${index + 1}行数量必须为非负数`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
row.mileage !== '' &&
|
||||||
|
(!Number.isFinite(Number(row.mileage)) || Number(row.mileage) < 0)
|
||||||
|
) {
|
||||||
|
this.$message.warning(`第${index + 1}行里程必须为非负数`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (String(row.remark || '').length > 200) {
|
||||||
|
this.$message.warning(`第${index + 1}行备注不能超过200个字`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
async buildImportFile() {
|
||||||
|
const XLSX = await import('xlsx');
|
||||||
|
const requiredLabels = [
|
||||||
|
'计划名称',
|
||||||
|
'运输类型',
|
||||||
|
'发货地址',
|
||||||
|
'到货地址',
|
||||||
|
'货物类型',
|
||||||
|
];
|
||||||
|
const values = [
|
||||||
|
importColumns.map(([label]) => (requiredLabels.includes(label) ? `*${label}` : label)),
|
||||||
|
...this.rows.map(row => importColumns.map(([, prop]) => String(row[prop] ?? ''))),
|
||||||
|
];
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(values), '运输计划导入模板');
|
||||||
|
const binary = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
||||||
|
const fileName = (this.sourceFile?.name || '运输计划导入模板.xlsx').replace(
|
||||||
|
/\.xls$/i,
|
||||||
|
'.xlsx'
|
||||||
|
);
|
||||||
|
return new File([binary], fileName, {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleTemplateDownload() {
|
||||||
|
exportBlob(config.transportPlanTemplateUrl, {}, { feedback: true }).then(res => {
|
||||||
|
downloadXls(res.data, '运输计划导入模板.xlsx');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async handleSubmit() {
|
||||||
|
const valid = await this.$refs.importFormRef?.validate().catch(() => false);
|
||||||
|
if (!valid || this.loading || !this.validateRows()) return;
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const file = await this.buildImportFile();
|
||||||
|
const res = await importTransportPlan({ ...this.form, file });
|
||||||
|
const contentType = res.headers?.['content-type'] || res.data?.type || '';
|
||||||
|
if (
|
||||||
|
contentType.includes('application/vnd.ms-excel') ||
|
||||||
|
contentType.includes('spreadsheetml')
|
||||||
|
) {
|
||||||
|
downloadXls(
|
||||||
|
res.data,
|
||||||
|
`运输计划导入失败明细${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`
|
||||||
|
);
|
||||||
|
this.$message.warning('部分数据导入失败,已下载失败明细');
|
||||||
|
} else {
|
||||||
|
const text = await res.data.text();
|
||||||
|
const result = text ? JSON.parse(text) : {};
|
||||||
|
if (result.code !== 200) throw new Error(result.msg || '导入失败');
|
||||||
|
this.$message.success('导入完成');
|
||||||
|
}
|
||||||
|
this.$router.push('/business/transport-plan');
|
||||||
|
} catch (error) {
|
||||||
|
this.$message.error(error.message || '导入失败');
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleCancel() {
|
||||||
|
this.$router.push('/business/transport-plan');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.transport-plan-import-page {
|
||||||
|
min-height: calc(100vh - 112px);
|
||||||
|
|
||||||
|
&__form-wrapper {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 18px 24px;
|
||||||
|
border-bottom: 1px solid #eff1f7;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: 4px;
|
||||||
|
height: 18px;
|
||||||
|
background: #409eff;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__form {
|
||||||
|
padding: 24px 24px 4px;
|
||||||
|
|
||||||
|
.el-select,
|
||||||
|
.el-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__file-item {
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
:deep(.el-form-item__content) {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__file-tip {
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__preview {
|
||||||
|
margin: 8px 24px 0;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #eff1f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__preview-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.dialog-section-title {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tabs {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__table {
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
min-width: 2400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__cell) {
|
||||||
|
border-color: #eff1f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row:nth-child(even) td) {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
left: 230px;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-top: 1px solid #eff1f7;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.avue--collapse .transport-plan-import-page__footer) {
|
||||||
|
left: 60px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user