Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18aeb08c90 | |||
| cdd30b8211 | |||
| 6762cf7b83 | |||
| a87b5e4b95 | |||
| 1181aea3b9 | |||
| 68f4b2c35e | |||
| 6296c8f6f4 | |||
| 6955784e93 | |||
| bc7755a160 | |||
| 18a02a1969 | |||
| dcaa3127a0 | |||
| 959c1821c1 | |||
| a4c2987c5f | |||
| 2377a2e8d4 | |||
| f455003014 | |||
| ea797356ce | |||
| eaece0209d | |||
| cd92c36dab | |||
| a1da644c49 | |||
| 2500540933 | |||
| 570fbeb537 | |||
| d1f804056e | |||
| 3320155416 | |||
| 8ad53894b9 | |||
| 360147e46a | |||
| b00e12c9bb | |||
| 7543a8c06b | |||
| 1699ae873f | |||
| 462f9bbea2 | |||
| d62d3da60e | |||
| 50b8abf85c | |||
| 509f5e163c | |||
| 0e853ea751 | |||
| d67240bc3b | |||
| d0770c8e83 | |||
| cc0b889fef | |||
| 3d27e981e1 | |||
| 2994539de4 | |||
| 785ad08b7e | |||
| 8de51e568d | |||
| d22fe4777d | |||
| 7751a735c5 | |||
| 069d0e92ff | |||
| ce54d21344 | |||
| 715c0dc00c | |||
| b8d74de4a4 | |||
| 71a6f7c4f8 | |||
| 7c6ac2e5db | |||
| 0b81001ce7 | |||
| 50c1afe9c8 | |||
| e5a7ad616c | |||
| 2210e33643 | |||
| 1ed524f80f | |||
| 4418836fcd | |||
| 4641d0a5dd | |||
| 8b9c908237 | |||
| b5043575e4 | |||
| b92625528c | |||
| f3a33b1a04 | |||
| 98fbf123f5 | |||
| c268d2e64d | |||
| 079be78c2c | |||
| ee7160e393 | |||
| aa26beda28 | |||
| a919781d4f | |||
| 694fe1a433 | |||
| ae1f2fa09f | |||
| fbfb563254 | |||
| 5352d799af | |||
| e2e2eb9a2f | |||
| 6212d68a77 | |||
| ce1ce02551 | |||
| 3a8442e522 | |||
| ac87208ba9 |
+3
-2
@@ -3,8 +3,9 @@
|
|||||||
VITE_APP_ENV = 'development'
|
VITE_APP_ENV = 'development'
|
||||||
|
|
||||||
#接口地址
|
#接口地址
|
||||||
# 开发环境建议填 [/api]:由下方 vite.config.mjs 的 proxy 同源转发到后端(172.16.203.228:8000),避免跨域(CORS)被浏览器拦截
|
# 开发环境建议填 [/api]:由 vite.config.mjs 的 proxy 同源转发到本地网关 http://localhost(80)
|
||||||
# 如需直连线上绝对地址(如 http://172.16.203.228:8000/api),必须让后端 CORS 把 Allow-Origin 改为具体前端域名,不能用 `*`(配合 credentials 会被浏览器拒绝)
|
# 本地需先启动:Nacos + blade-gateway(80) + blade-auth + blade-system 等;8080 端口一般是 Nacos 控制台不是网关
|
||||||
|
# 如需临时联调远程,把 vite proxy target 改为 http://172.16.203.228:8000 并去掉 rewrite
|
||||||
VITE_APP_API=/api
|
VITE_APP_API=/api
|
||||||
|
|
||||||
#调试参数
|
#调试参数
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 2026-09-30
|
||||||
|
|
||||||
|
## /wel/index 首页重构为运营数据看板(按蓝湖效果图)
|
||||||
|
- 新增依赖:echarts ^6.1.0(corepack pnpm add echarts)。
|
||||||
|
- 重构 `src/views/wel/index.vue`:改为「核心经营指标 / 运输结构 / 业务单元情况 / 项目情况 / 财务情况 / 车队与线路 / 车务管理 / 底部占比与趋势」8 大板块 + 版权页脚,删除原欢迎横幅/快捷入口。
|
||||||
|
- 新增文件:
|
||||||
|
- `src/views/wel/chart-options.js`:排行条形/盈亏双向/环形/分组柱/面积折线/堆叠柱 6 类 option 生成器(fmt 千分位、红涨绿跌 rich label)。
|
||||||
|
- `src/views/wel/dashboard.scss`:看板公共样式,各组件 scoped `@import '../dashboard.scss'`(子目录注意路径)。
|
||||||
|
- `src/views/wel/components/`:wel-chart.vue(ECharts 封装:ResizeObserver + activated resize + dispose)、wel-filters.vue(统计时间/条件筛选 mock)、rank-panel.vue(排行/盈亏通用面板)、section-kpi / section-structure / section-business / section-project / section-finance / section-fleet / section-vehicle / section-summary.vue。
|
||||||
|
- 数据全部为 Mock,组件内已注释标注对接点;蓝湖红字标注的指标口径未定,待后端统计接口(估 12~18 个)确定后替换。
|
||||||
|
- 自检:dev server(2889) 下所有新文件及 `?vue&type=style&...&lang.scss` 均 200;`/node_modules/.vite/deps/echarts.js` 200。vite build 因 facelogin.vue 沙箱中断不可用,未跑。
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# 2026-10-13
|
||||||
|
|
||||||
|
## 车辆调度弹窗:使用部门级联选择器宽度对齐
|
||||||
|
|
||||||
|
- 现象:`/transportCapacity/vehicle-dispatch` 新增/编辑弹窗中,使用部门 `el-cascader` 明显窄于同行车牌号 `el-input`。
|
||||||
|
- 根因:`el-cascader` 是 `display: inline-block` 宽度按内容收缩(不像 `el-autocomplete` / `el-input` 有 `--el-input-width: 100%` 兜底);而页面里写的 `.vehicle-dispatch-page :deep(.vehicle-dispatch-form-control){width:100%}` 命中不到 —— avue-crud 弹窗默认 `append-to-body: true`(`append-to-body": validData(crud.tableOption.dialogAppendToBody, true)`),DOM Teleport 到 body,已不在 `.vehicle-dispatch-page` 之下。
|
||||||
|
- 修法(走项目既有约定):
|
||||||
|
1. `src/option/transportCapacity/vehicle-dispatch.js` 增加 `dialogCustomClass: 'vehicle-dispatch-dialog'`;
|
||||||
|
2. `src/styles/element-ui.scss` 增加全局规则 `.vehicle-dispatch-dialog .el-form-item__content > .el-cascader { width:100%; max-width:100% }`。
|
||||||
|
- 自检:dev(2889) `curl /src/styles/element-ui.scss` 与 `/src/option/transportCapacity/vehicle-dispatch.js` 均 200,编译产物含新规则。
|
||||||
|
- 遗留观察:同文件里 `.vehicle-dispatch-page :deep(.avue--detail ...)` 那一批查看态禁用样式,同样因 Teleport 属死规则(未改,待确认是否需要迁到 `.vehicle-dispatch-dialog` 作用域)。
|
||||||
|
|
||||||
|
## 追加修正(同日 20:xx):第一次改完用户反馈「还是没对齐」
|
||||||
|
|
||||||
|
- 原因一:选择器写成了 `.el-form-item__content > .el-cascader`(直接子级)。**avue 表单项在 `el-form-item__content` 与控件之间还有两层包裹 div**(`packages/element-plus/form/index3.js`:content > div > [div|elTooltip] > 控件),必须用后代选择器。已改成 `.vehicle-dispatch-dialog .el-form-item__content .el-cascader`。
|
||||||
|
- 原因二(更隐蔽):option 用 `labelWidth: 'auto'`,**EP 的 auto 是逐项计算**(实测:3 字 54/64px、2 字 40px、4 字无星号 68px、4 字带星号 78px),label 越宽控件可用宽度越小 → 车牌号 input 486px、使用部门 cascader 472px,仍差 14px。已加 `.vehicle-dispatch-dialog .el-form-item__label { flex: 0 0 calc(4em + 24px) !important; width: calc(4em + 24px) !important }`(本弹窗左右两列最长 label 均为 4 汉字)。
|
||||||
|
- 验证方式(可复用):临时在项目根建 `cascader-width-check.html`,Vite 直接服务 → `import './src/option/...'` 用真实 option + `import './src/styles/element-ui.scss'`,挂 avue-crud 并 `mounted(){ this.$refs.crud.rowAdd() }` 弹出真实弹窗;用 agent-browser(`/Users/gxwebsoft/.workbuddy/binaries/node/workspace/node_modules/.bin/agent-browser`)`open` + `eval` 读 DOM 链路与 `getBoundingClientRect`。修完已删除该临时文件。
|
||||||
|
- 结果:全部 label 80px,车牌号 input 与使用部门 cascader 均为 left=732 / width=470 / right=1202,完全一致。
|
||||||
@@ -12,6 +12,14 @@
|
|||||||
|
|
||||||
**坑二:宿主 view 直绑全局 `$route.query.*` 给子组件 prop。** 缓存实例被灌入当前页 id → 拿错 id 查后端(批量弹「运单管理不存在」),live computed 还会误 push 标签。修法:`routePathLocked` 锁路径 + watcher / push 前判 `isOwnedRouteActive` + 宿主绑定加路径守卫。⚠️ `routePathLocked` **必须放 `data()`**(Options API 顺序 data → computed → watch(immediate) → created;放 created 会读到空值、挡掉首次加载)。已修 waybill-manage-page / transport-plan-page + 三个宿主 view。
|
**坑二:宿主 view 直绑全局 `$route.query.*` 给子组件 prop。** 缓存实例被灌入当前页 id → 拿错 id 查后端(批量弹「运单管理不存在」),live computed 还会误 push 标签。修法:`routePathLocked` 锁路径 + watcher / push 前判 `isOwnedRouteActive` + 宿主绑定加路径守卫。⚠️ `routePathLocked` **必须放 `data()`**(Options API 顺序 data → computed → watch(immediate) → created;放 created 会读到空值、挡掉首次加载)。已修 waybill-manage-page / transport-plan-page + 三个宿主 view。
|
||||||
|
|
||||||
|
**坑四:avue-crud 弹窗 `append-to-body` 默认 true(`dialog-form3.js`:`validData(tableOption.dialogAppendToBody, true)`),DOM Teleport 到 body。** 于是页面里以页面容器为祖先的 scoped 选择器(`.xxx-page :deep(.yyy)`)**命中不到弹窗内容**,尤其 `el-cascader`(inline-block、宽度按内容收缩,不像 `el-input`/`el-autocomplete` 有 `--el-input-width:100%` 兜底)会明显比同行 input 窄。修法:option 里配 `dialogCustomClass: 'xxx-dialog'`,规则写到 `src/styles/element-ui.scss` 的 `.xxx-dialog ...`(项目既有约定,10+ 处同款);或把基准类放在 **Teleport 内部元素**上(如 ship.vue 的 `.ship-form` 挂在 el-form 上,自身带 scope 属性,照样生效)。
|
||||||
|
|
||||||
|
**坑五:avue 表单项控件不是 `.el-form-item__content` 的直接子级。** `form/index3.js` 渲染为 `content > div > [div|elTooltip] > 控件`,所以 `.el-form-item__content > .el-cascader` 这类**直接子级选择器对 avue 表单全部失效**(手写 `<el-form-item>` 才生效),一律用后代选择器。
|
||||||
|
|
||||||
|
**坑六:`labelWidth: 'auto'` 在 Element Plus 里逐项计算,不是全表单共享。** 实测同一弹窗:3 字 54/64px、2 字 40px、4 字 68px、4 字带必填星号 78px;label 越宽 → 控件可用宽度越小,同级控件宽度就不一致(车牌号 input 486 vs 使用部门 cascader 472)。要让同一行控件等宽必须统一 label 宽度:`.xxx-dialog .el-form-item__label { flex: 0 0 calc(Nem + 24px) !important; width: calc(Nem + 24px) !important }`(N=本列最长汉字数,与 ship.vue、`.dialog-form--carded` 同款)。
|
||||||
|
|
||||||
|
**验证 avue 弹窗 DOM/宽度的可复用办法:** 项目根建临时 html(Vite 直接服务)→ 引真实 option 与 `src/styles/element-ui.scss`,挂 avue-crud 并 `mounted(){ this.$refs.crud.rowAdd() }` 弹出真弹窗;用 `agent-browser`(`/Users/gxwebsoft/.workbuddy/binaries/node/workspace/node_modules/.bin/agent-browser`)`open` + `eval` 读 DOM 链路与 `getBoundingClientRect`,用完删文件。
|
||||||
|
|
||||||
**坑三:路由表重复定义 → 内容区整页空白(2026-09-18)。** `/business/contract-manage/detail` 在 `router/views/index.js` 定义两次、子路由同名 `合同详情`。重名 `addRoute` 会 `removeRoute(name)` 移除旧子记录,但**不从父记录 children 里摘除**,第一条 Layout 壳记录留在 matcher 且靠前 → `resolve()` 命中它 → `$route.matched.length === 1` → 二级 router-view 无匹配 → 侧栏/标签正常但内容空白。修法:删重复定义。排查:扫「顶层 path + 首个子路由 name」重复项。
|
**坑三:路由表重复定义 → 内容区整页空白(2026-09-18)。** `/business/contract-manage/detail` 在 `router/views/index.js` 定义两次、子路由同名 `合同详情`。重名 `addRoute` 会 `removeRoute(name)` 移除旧子记录,但**不从父记录 children 里摘除**,第一条 Layout 壳记录留在 matcher 且靠前 → `resolve()` 命中它 → `$route.matched.length === 1` → 二级 router-view 无匹配 → 侧栏/标签正常但内容空白。修法:删重复定义。排查:扫「顶层 path + 首个子路由 name」重复项。
|
||||||
|
|
||||||
## 样式硬规则
|
## 样式硬规则
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"dayjs": "^1.10.6",
|
"dayjs": "^1.10.6",
|
||||||
"disable-devtool": "^0.3.8",
|
"disable-devtool": "^0.3.8",
|
||||||
|
"echarts": "^6.1.0",
|
||||||
"element-plus": "^2.14.2",
|
"element-plus": "^2.14.2",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
|
|||||||
Generated
+18
-44
@@ -4,11 +4,6 @@ settings:
|
|||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
patchedDependencies:
|
|
||||||
'@smallwei/avue':
|
|
||||||
hash: 5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db
|
|
||||||
path: patches/@smallwei__avue.patch
|
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
@@ -33,7 +28,7 @@ importers:
|
|||||||
version: 1.7.0
|
version: 1.7.0
|
||||||
'@smallwei/avue':
|
'@smallwei/avue':
|
||||||
specifier: ~3.9.2
|
specifier: ~3.9.2
|
||||||
version: 3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)
|
version: 3.9.2(@element-plus/icons-vue@2.3.2(vue@3.5.34))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)
|
||||||
animate.css:
|
animate.css:
|
||||||
specifier: ^4.1.1
|
specifier: ^4.1.1
|
||||||
version: 4.1.1
|
version: 4.1.1
|
||||||
@@ -55,6 +50,9 @@ importers:
|
|||||||
disable-devtool:
|
disable-devtool:
|
||||||
specifier: ^0.3.8
|
specifier: ^0.3.8
|
||||||
version: 0.3.9
|
version: 0.3.9
|
||||||
|
echarts:
|
||||||
|
specifier: ^6.1.0
|
||||||
|
version: 6.1.0
|
||||||
element-plus:
|
element-plus:
|
||||||
specifier: ^2.14.2
|
specifier: ^2.14.2
|
||||||
version: 2.14.2(vue@3.5.34)
|
version: 2.14.2(vue@3.5.34)
|
||||||
@@ -63,13 +61,13 @@ importers:
|
|||||||
version: 4.4.0
|
version: 4.4.0
|
||||||
highlight.js:
|
highlight.js:
|
||||||
specifier: ^11.9.0
|
specifier: ^11.9.0
|
||||||
version: 11.11.1
|
version: 11.12.0
|
||||||
js-base64:
|
js-base64:
|
||||||
specifier: ^3.7.4
|
specifier: ^3.7.4
|
||||||
version: 3.7.8
|
version: 3.9.4
|
||||||
js-cookie:
|
js-cookie:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.5
|
version: 3.0.8
|
||||||
js-md5:
|
js-md5:
|
||||||
specifier: ^0.7.3
|
specifier: ^0.7.3
|
||||||
version: 0.7.3
|
version: 0.7.3
|
||||||
@@ -406,35 +404,30 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-musl@1.0.2':
|
'@napi-rs/canvas-linux-arm64-musl@1.0.2':
|
||||||
resolution: {integrity: sha512-qwROoDIC9upfvDoRLuPn2aNg9CGW1x0Ygr4k2Or+8paA9d0qBLwk87U+g8KQpoOviKoPoiwl97kvBYuYD7qZoA==}
|
resolution: {integrity: sha512-qwROoDIC9upfvDoRLuPn2aNg9CGW1x0Ygr4k2Or+8paA9d0qBLwk87U+g8KQpoOviKoPoiwl97kvBYuYD7qZoA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu@1.0.2':
|
'@napi-rs/canvas-linux-riscv64-gnu@1.0.2':
|
||||||
resolution: {integrity: sha512-fXRjnPihdnbO6qy1QQOgxAonb68A0TCEG7rj1x7v7rxNElsE8EVIKIEUTvyDtU+sthYSbX+8e7g3oZiLGnOmxw==}
|
resolution: {integrity: sha512-fXRjnPihdnbO6qy1QQOgxAonb68A0TCEG7rj1x7v7rxNElsE8EVIKIEUTvyDtU+sthYSbX+8e7g3oZiLGnOmxw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-gnu@1.0.2':
|
'@napi-rs/canvas-linux-x64-gnu@1.0.2':
|
||||||
resolution: {integrity: sha512-nPR97DXhbWIAy7yazF3jc06kEPMqYMLmPzFOVNlwKPfIoSChnI+x7dc0hTLaihz3jxrjL6j4BbA7earxfx4X3g==}
|
resolution: {integrity: sha512-nPR97DXhbWIAy7yazF3jc06kEPMqYMLmPzFOVNlwKPfIoSChnI+x7dc0hTLaihz3jxrjL6j4BbA7earxfx4X3g==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-musl@1.0.2':
|
'@napi-rs/canvas-linux-x64-musl@1.0.2':
|
||||||
resolution: {integrity: sha512-l7zZY5+jL5qnBZtDz7CoBtY6p7EkHu422g/0zWwrOrzIwWyWxZFRfZZORY1UG7YApymPLx+UbOkN206xXn/c1Q==}
|
resolution: {integrity: sha512-l7zZY5+jL5qnBZtDz7CoBtY6p7EkHu422g/0zWwrOrzIwWyWxZFRfZZORY1UG7YApymPLx+UbOkN206xXn/c1Q==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-arm64-msvc@1.0.2':
|
'@napi-rs/canvas-win32-arm64-msvc@1.0.2':
|
||||||
resolution: {integrity: sha512-yE0koHCFF4PIbMc2o2SEALhnipz7WBISh5glLvQiomtIoCcW0np3H4Lw93ceJAfJttTTeIIWFbwH84F7EVzjMQ==}
|
resolution: {integrity: sha512-yE0koHCFF4PIbMc2o2SEALhnipz7WBISh5glLvQiomtIoCcW0np3H4Lw93ceJAfJttTTeIIWFbwH84F7EVzjMQ==}
|
||||||
@@ -510,42 +503,36 @@ packages:
|
|||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@parcel/watcher-linux-arm-musl@2.5.6':
|
'@parcel/watcher-linux-arm-musl@2.5.6':
|
||||||
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
|
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@parcel/watcher-linux-arm64-glibc@2.5.6':
|
'@parcel/watcher-linux-arm64-glibc@2.5.6':
|
||||||
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
|
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@parcel/watcher-linux-arm64-musl@2.5.6':
|
'@parcel/watcher-linux-arm64-musl@2.5.6':
|
||||||
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
|
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@parcel/watcher-linux-x64-glibc@2.5.6':
|
'@parcel/watcher-linux-x64-glibc@2.5.6':
|
||||||
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
|
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@parcel/watcher-linux-x64-musl@2.5.6':
|
'@parcel/watcher-linux-x64-musl@2.5.6':
|
||||||
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
|
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@parcel/watcher-win32-arm64@2.5.6':
|
'@parcel/watcher-win32-arm64@2.5.6':
|
||||||
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
|
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
|
||||||
@@ -612,79 +599,66 @@ packages:
|
|||||||
resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==}
|
resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-musleabihf@4.60.3':
|
'@rollup/rollup-linux-arm-musleabihf@4.60.3':
|
||||||
resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==}
|
resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-gnu@4.60.3':
|
'@rollup/rollup-linux-arm64-gnu@4.60.3':
|
||||||
resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==}
|
resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-musl@4.60.3':
|
'@rollup/rollup-linux-arm64-musl@4.60.3':
|
||||||
resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==}
|
resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-gnu@4.60.3':
|
'@rollup/rollup-linux-loong64-gnu@4.60.3':
|
||||||
resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==}
|
resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-musl@4.60.3':
|
'@rollup/rollup-linux-loong64-musl@4.60.3':
|
||||||
resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==}
|
resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-gnu@4.60.3':
|
'@rollup/rollup-linux-ppc64-gnu@4.60.3':
|
||||||
resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==}
|
resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-musl@4.60.3':
|
'@rollup/rollup-linux-ppc64-musl@4.60.3':
|
||||||
resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==}
|
resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-gnu@4.60.3':
|
'@rollup/rollup-linux-riscv64-gnu@4.60.3':
|
||||||
resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==}
|
resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-musl@4.60.3':
|
'@rollup/rollup-linux-riscv64-musl@4.60.3':
|
||||||
resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==}
|
resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-s390x-gnu@4.60.3':
|
'@rollup/rollup-linux-s390x-gnu@4.60.3':
|
||||||
resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==}
|
resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.60.3':
|
'@rollup/rollup-linux-x64-gnu@4.60.3':
|
||||||
resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==}
|
resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-musl@4.60.3':
|
'@rollup/rollup-linux-x64-musl@4.60.3':
|
||||||
resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==}
|
resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-openbsd-x64@4.60.3':
|
'@rollup/rollup-openbsd-x64@4.60.3':
|
||||||
resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==}
|
resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==}
|
||||||
@@ -1208,6 +1182,7 @@ packages:
|
|||||||
|
|
||||||
crypto-js@4.2.0:
|
crypto-js@4.2.0:
|
||||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||||
|
deprecated: Active development of CryptoJS has been discontinued. This library is no longer maintained.
|
||||||
|
|
||||||
csstype@3.2.3:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
@@ -1453,8 +1428,8 @@ packages:
|
|||||||
heic2any@0.0.4:
|
heic2any@0.0.4:
|
||||||
resolution: {integrity: sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA==}
|
resolution: {integrity: sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA==}
|
||||||
|
|
||||||
highlight.js@11.11.1:
|
highlight.js@11.12.0:
|
||||||
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
resolution: {integrity: sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
hls.js@1.6.16:
|
hls.js@1.6.16:
|
||||||
@@ -1532,12 +1507,11 @@ packages:
|
|||||||
isarray@1.0.0:
|
isarray@1.0.0:
|
||||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||||
|
|
||||||
js-base64@3.7.8:
|
js-base64@3.9.4:
|
||||||
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
|
resolution: {integrity: sha512-PtOMXpEGuP0RRiRXsjzHzl44dMHxSu2CPvAhinupR1tBa88me+1DPqsobl7eupn5UukjLkln1ZnAOAOXOrYG0Q==}
|
||||||
|
|
||||||
js-cookie@3.0.5:
|
js-cookie@3.0.8:
|
||||||
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==}
|
||||||
engines: {node: '>=14'}
|
|
||||||
|
|
||||||
js-md5@0.7.3:
|
js-md5@0.7.3:
|
||||||
resolution: {integrity: sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==}
|
resolution: {integrity: sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==}
|
||||||
@@ -2653,7 +2627,7 @@ snapshots:
|
|||||||
countup.js: 2.10.0
|
countup.js: 2.10.0
|
||||||
signature_pad: 5.1.3
|
signature_pad: 5.1.3
|
||||||
|
|
||||||
'@smallwei/avue@3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)':
|
'@smallwei/avue@3.9.2(@element-plus/icons-vue@2.3.2(vue@3.5.34))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@element-plus/icons-vue': 2.3.2(vue@3.5.34)
|
'@element-plus/icons-vue': 2.3.2(vue@3.5.34)
|
||||||
axios: 0.21.4
|
axios: 0.21.4
|
||||||
@@ -3546,7 +3520,7 @@ snapshots:
|
|||||||
|
|
||||||
heic2any@0.0.4: {}
|
heic2any@0.0.4: {}
|
||||||
|
|
||||||
highlight.js@11.11.1: {}
|
highlight.js@11.12.0: {}
|
||||||
|
|
||||||
hls.js@1.6.16: {}
|
hls.js@1.6.16: {}
|
||||||
|
|
||||||
@@ -3605,9 +3579,9 @@ snapshots:
|
|||||||
|
|
||||||
isarray@1.0.0: {}
|
isarray@1.0.0: {}
|
||||||
|
|
||||||
js-base64@3.7.8: {}
|
js-base64@3.9.4: {}
|
||||||
|
|
||||||
js-cookie@3.0.5: {}
|
js-cookie@3.0.8: {}
|
||||||
|
|
||||||
js-md5@0.7.3: {}
|
js-md5@0.7.3: {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
packages:
|
packages:
|
||||||
- '.'
|
- '.'
|
||||||
|
|
||||||
|
allowBuilds:
|
||||||
|
'@parcel/watcher': true
|
||||||
|
es5-ext: true
|
||||||
|
esbuild: true
|
||||||
|
|
||||||
patchedDependencies:
|
patchedDependencies:
|
||||||
'@smallwei/avue': patches/@smallwei__avue.patch
|
'@smallwei/avue': patches/@smallwei__avue.patch
|
||||||
|
|||||||
@@ -90,3 +90,11 @@ export const expireStats = params =>
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 按合同负责部门预览下一合同编号 */
|
||||||
|
export const nextContractNo = organizationId =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/next-contract-no`,
|
||||||
|
method: 'get',
|
||||||
|
params: { organizationId },
|
||||||
|
});
|
||||||
|
|||||||
@@ -22,6 +22,18 @@ export const getVoucherImages = (waybillId, params = {}) =>
|
|||||||
...params,
|
...params,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
export const getVoucherFolders = waybillId =>
|
||||||
|
request({
|
||||||
|
url: '/blade-transport/process-config/voucher-folders',
|
||||||
|
method: 'get',
|
||||||
|
params: { waybillId },
|
||||||
|
});
|
||||||
|
export const getVoucherFolderDetail = (voucherId, plateNo) =>
|
||||||
|
request({
|
||||||
|
url: '/blade-transport/process-config/voucher-folder-detail',
|
||||||
|
method: 'get',
|
||||||
|
params: { voucherId, plateNo },
|
||||||
|
});
|
||||||
export const submit = api.submit;
|
export const submit = api.submit;
|
||||||
export const remove = api.remove;
|
export const remove = api.remove;
|
||||||
export const copy = api.copy;
|
export const copy = api.copy;
|
||||||
|
|||||||
@@ -52,6 +52,17 @@ export const createFileTask = data =>
|
|||||||
request({ url: `${fileTaskBaseUrl}/createFileTask`, method: 'post', data });
|
request({ url: `${fileTaskBaseUrl}/createFileTask`, method: 'post', data });
|
||||||
export const getPartUploadUrl = (id, partNumber) =>
|
export const getPartUploadUrl = (id, partNumber) =>
|
||||||
request({ url: `${fileTaskBaseUrl}/getUploadUrl`, method: 'get', params: { id, partNumber } });
|
request({ url: `${fileTaskBaseUrl}/getUploadUrl`, method: 'get', params: { id, partNumber } });
|
||||||
|
export const uploadPartByProxy = (id, partNumber, partBlob) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', partBlob, `part-${partNumber}.bin`);
|
||||||
|
return request({
|
||||||
|
url: `${fileTaskBaseUrl}/uploadPart`,
|
||||||
|
method: 'post',
|
||||||
|
params: { id, partNumber },
|
||||||
|
data: formData,
|
||||||
|
timeout: 600000,
|
||||||
|
});
|
||||||
|
};
|
||||||
export const updateFileTask = data =>
|
export const updateFileTask = data =>
|
||||||
request({ url: `${fileTaskBaseUrl}/updateFileTask`, method: 'post', data });
|
request({ url: `${fileTaskBaseUrl}/updateFileTask`, method: 'post', data });
|
||||||
export const pauseFileTask = ids =>
|
export const pauseFileTask = ids =>
|
||||||
|
|||||||
@@ -34,6 +34,14 @@ export const getPunchRecords = waybillId =>
|
|||||||
params: { waybillId },
|
params: { waybillId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 编辑打卡记录(含打卡状态;punched=false 表示取消打卡) */
|
||||||
|
export const savePunchRecord = data =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/punch-record/save`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
/** 运单车辆实时定位 */
|
/** 运单车辆实时定位 */
|
||||||
export const locateVehicle = id =>
|
export const locateVehicle = id =>
|
||||||
request({
|
request({
|
||||||
@@ -67,6 +75,16 @@ export const getImportDetails = params => request({ url: `${baseUrl}/import-batc
|
|||||||
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, timeout: 120000 });
|
||||||
export const validateImport = data => request({ url: `${baseUrl}/import-batch/validate`, method: 'post', data, responseType: 'blob' });
|
export const validateImport = data =>
|
||||||
export const confirmImport = data => request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data });
|
request({ url: `${baseUrl}/import-batch/validate`, method: 'post', data, timeout: 120000 });
|
||||||
|
export const exportImportFailures = data =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/import-batch/export-failures`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
responseType: 'blob',
|
||||||
|
timeout: 120000,
|
||||||
|
});
|
||||||
|
export const confirmImport = data =>
|
||||||
|
request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data, timeout: 120000 });
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const syncKingdeeBatch = ids =>
|
|||||||
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 approvalStatusOptions = [
|
export const approvalStatusOptions = [
|
||||||
{ label: '草稿', value: 'draft' },
|
{ label: '草稿', value: 'draft' },
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import request from '@/axios';
|
|||||||
* @param {string} data.templateCode 模板编码
|
* @param {string} data.templateCode 模板编码
|
||||||
* @param {string} data.submitIdentity 提交人(手机号)
|
* @param {string} data.submitIdentity 提交人(手机号)
|
||||||
* @param {string} data.loginName 登录账号(手机号)
|
* @param {string} data.loginName 登录账号(手机号)
|
||||||
* @param {string} data.formInstanceId 业务表单实例 id
|
* @param {string} data.bizId 业务主键(后端生成/复用 formInstanceId)
|
||||||
* @param {string} [data.subject] 流程标题
|
* @param {string} [data.subject] 流程标题
|
||||||
|
* @param {string} [data.bizType] 业务类型
|
||||||
*/
|
*/
|
||||||
export const processSubmit = data => {
|
export const processSubmit = data => {
|
||||||
return request({
|
return request({
|
||||||
@@ -20,7 +21,7 @@ export const processSubmit = data => {
|
|||||||
/**
|
/**
|
||||||
* 调用 MK processDelete 删除审核流(驳回后重新提交前使用)
|
* 调用 MK processDelete 删除审核流(驳回后重新提交前使用)
|
||||||
* @param {Object} data
|
* @param {Object} data
|
||||||
* @param {string} data.formInstanceId 业务表单实例 id
|
* @param {string} data.bizId 业务主键
|
||||||
*/
|
*/
|
||||||
export const processDelete = data => {
|
export const processDelete = data => {
|
||||||
return request({
|
return request({
|
||||||
@@ -29,3 +30,30 @@ export const processDelete = data => {
|
|||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 MK 流程实例是否仍存在(有数据则无需删除/重新提交)
|
||||||
|
* @param {Object} params
|
||||||
|
* @param {string} params.bizId 业务主键
|
||||||
|
*/
|
||||||
|
export const hasMkProcessInstance = params => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-system/businessProcess/hasMkProcessInstance',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 MK 审批页跳转链接(后端按 thirdParty.mk 配置拼接,含 SSO)
|
||||||
|
* @param {Object} params
|
||||||
|
* @param {string} [params.bizId] 业务 id(如客商 id)
|
||||||
|
* @param {string} [params.processInstanceId] 流程实例 id
|
||||||
|
*/
|
||||||
|
export const getMKApprovalUrl = params => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-system/businessProcess/getMKApprovalUrl',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
+21
-2
@@ -48,26 +48,45 @@ export const syncIamOrganizations = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const syncOaCompany = (current = 1, size = 20, signal) => {
|
export const clearNonTopDept = signal => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-system/dept/clear-non-top',
|
||||||
|
method: 'post',
|
||||||
|
timeout: 60000,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const syncOaCompany = (current = 1, size = 20, signal, incremental = false, modifiedSince) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/blade-system/dept/sync-oa-company',
|
url: '/blade-system/dept/sync-oa-company',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
params: {
|
params: {
|
||||||
current,
|
current,
|
||||||
size,
|
size,
|
||||||
|
incremental: !!incremental,
|
||||||
|
modifiedSince: modifiedSince || undefined,
|
||||||
},
|
},
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const syncOaDepartment = (current = 1, size = 20, signal) => {
|
export const syncOaDepartment = (
|
||||||
|
current = 1,
|
||||||
|
size = 20,
|
||||||
|
signal,
|
||||||
|
incremental = false,
|
||||||
|
modifiedSince
|
||||||
|
) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/blade-system/dept/sync-oa-department',
|
url: '/blade-system/dept/sync-oa-department',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
params: {
|
params: {
|
||||||
current,
|
current,
|
||||||
size,
|
size,
|
||||||
|
incremental: !!incremental,
|
||||||
|
modifiedSince: modifiedSince || undefined,
|
||||||
},
|
},
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
signal,
|
signal,
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import request from '@/axios';
|
||||||
|
|
||||||
|
export const getList = (current, size, params) => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-system/mp-user/list',
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
current,
|
||||||
|
size,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDetail = id => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-system/mp-user/detail',
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const submit = row => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-system/mp-user/submit',
|
||||||
|
method: 'post',
|
||||||
|
data: row,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const remove = ids => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-system/mp-user/remove',
|
||||||
|
method: 'post',
|
||||||
|
params: {
|
||||||
|
ids,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const changeStatus = (id, status) => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-system/mp-user/change-status',
|
||||||
|
method: 'post',
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetPassword = userIds => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-system/mp-user/reset-password',
|
||||||
|
method: 'post',
|
||||||
|
params: {
|
||||||
|
userIds,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
+14
-1
@@ -31,13 +31,15 @@ export const add = row => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const syncIamAccounts = (current = 1, size = 50, signal) => {
|
export const syncIamAccounts = (current = 1, size = 50, signal, incremental = false, modifiedSince) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/blade-system/user/sync-iam-accounts',
|
url: '/blade-system/user/sync-iam-accounts',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
params: {
|
params: {
|
||||||
current,
|
current,
|
||||||
size,
|
size,
|
||||||
|
incremental: !!incremental,
|
||||||
|
modifiedSince: modifiedSince || undefined,
|
||||||
},
|
},
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
signal,
|
signal,
|
||||||
@@ -52,6 +54,17 @@ export const update = row => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const changeStatus = (id, status) => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-system/user/change-status',
|
||||||
|
method: 'post',
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const updatePlatform = (userId, userType, userExt) => {
|
export const updatePlatform = (userId, userType, userExt) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/blade-system/user/update-platform',
|
url: '/blade-system/user/update-platform',
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get'
|
|||||||
|
|
||||||
export const follow = data => request({ url: `${baseUrl}/follow`, method: 'post', data });
|
export const follow = data => request({ url: `${baseUrl}/follow`, method: 'post', data });
|
||||||
|
|
||||||
|
export const update = data => request({ url: `${baseUrl}/update`, method: 'post', data });
|
||||||
|
|
||||||
export const complete = data => request({ url: `${baseUrl}/complete`, method: 'post', data });
|
export const complete = data => request({ url: `${baseUrl}/complete`, method: 'post', data });
|
||||||
|
|
||||||
export const batchComplete = ids =>
|
export const batchComplete = ids =>
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import request from '@/axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证记录列表:按审核状态分页,按提交时间倒序
|
||||||
|
* 审核对象是**一条认证记录**,不是一辆车或一名司机,故必须带 bizType 区分
|
||||||
|
* status 为审核状态(0审核中 1已通过 2已驳回 3已作废);不传时后端按 0 处理
|
||||||
|
*/
|
||||||
|
export const getPendingPage = (bizType, current, size, params) => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-transport/certification-audit/pending-page',
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
bizType,
|
||||||
|
current,
|
||||||
|
size,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审核详情:同时返回本次待审快照与该对象当前生效快照,供前端做字段级对比
|
||||||
|
* 从未认证通过时 effectiveSnapshotJson 为空串
|
||||||
|
*/
|
||||||
|
export const getAuditDetail = id => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-transport/certification-audit/detail',
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审核一条认证记录
|
||||||
|
* 通过时不传驳回原因;驳回时必须传,且原因不超过 200 字
|
||||||
|
*/
|
||||||
|
export const auditCertification = (id, pass, rejectReason) => {
|
||||||
|
return request({
|
||||||
|
url: '/blade-transport/certification-audit/audit',
|
||||||
|
method: 'post',
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
pass,
|
||||||
|
...(rejectReason ? { rejectReason } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -51,14 +51,6 @@ export const changeStatus = (id, status) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const auditCertification = (id, certificationStatus, rejectReason) => {
|
|
||||||
return request({
|
|
||||||
url: '/blade-transport/transport-vehicle/audit-certification',
|
|
||||||
method: 'post',
|
|
||||||
params: { id, certificationStatus, rejectReason },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getExpiryStat = params => {
|
export const getExpiryStat = params => {
|
||||||
return request({
|
return request({
|
||||||
url: '/blade-transport/transport-vehicle/expiry-stat',
|
url: '/blade-transport/transport-vehicle/expiry-stat',
|
||||||
|
|||||||
+8
-1
@@ -189,7 +189,14 @@ axios.interceptors.response.use(
|
|||||||
// 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。
|
// 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。
|
||||||
if (isJsonContentType(res) && !isPlainObject(res.data)) {
|
if (isJsonContentType(res) && !isPlainObject(res.data)) {
|
||||||
try {
|
try {
|
||||||
const text = res.data instanceof Blob ? await res.data.text() : res.data;
|
let text = '';
|
||||||
|
if (typeof res.data?.text === 'function') {
|
||||||
|
text = await res.data.text();
|
||||||
|
} else if (typeof res.data === 'string') {
|
||||||
|
text = res.data;
|
||||||
|
} else if (typeof res.data?.arrayBuffer === 'function') {
|
||||||
|
text = new TextDecoder().decode(await res.data.arrayBuffer());
|
||||||
|
}
|
||||||
const parsed = typeof text === 'string' && text ? JSON.parse(text) : null;
|
const parsed = typeof text === 'string' && text ? JSON.parse(text) : null;
|
||||||
if (parsed && typeof parsed === 'object') {
|
if (parsed && typeof parsed === 'object') {
|
||||||
res.data = parsed;
|
res.data = parsed;
|
||||||
|
|||||||
@@ -10,17 +10,17 @@
|
|||||||
<el-button type="primary" :loading="searching" @click="searchKeyword">搜索</el-button>
|
<el-button type="primary" :loading="searching" @click="searchKeyword">搜索</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="address-map-picker__content">
|
<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 ref="map" class="address-map-picker__map"></div>
|
||||||
|
<map-search-results :results="searchResults" @select="selectSearchResult" />
|
||||||
</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>
|
||||||
<el-button type="primary" :disabled="!selected.longitude || resolving" @click="confirm">
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="!hasCoordinateValue(selected.longitude) || !hasCoordinateValue(selected.latitude) || resolving"
|
||||||
|
@click="confirm"
|
||||||
|
>
|
||||||
确定
|
确定
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -28,8 +28,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
|
||||||
|
const AMAP_KEY = '13dcb15bc29bfe9c432d17ecd808b812';
|
||||||
|
const AMAP_SECURITY_CODE = 'f9d036ef29e376e15d309e0de5c0f7ab';
|
||||||
let amapLoader;
|
let amapLoader;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -42,6 +44,14 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
longitude: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
latitude: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue', 'confirm'],
|
emits: ['update:modelValue', 'confirm'],
|
||||||
data() {
|
data() {
|
||||||
@@ -66,9 +76,11 @@ export default {
|
|||||||
this.visible = value;
|
this.visible = value;
|
||||||
if (value) {
|
if (value) {
|
||||||
this.keyword = this.address || '';
|
this.keyword = this.address || '';
|
||||||
this.selected = {};
|
|
||||||
this.status = '可搜索地址或点击地图选点';
|
|
||||||
this.searchResults = [];
|
this.searchResults = [];
|
||||||
|
this.selected = this.buildInitialSelection();
|
||||||
|
this.status = this.selected.longitude
|
||||||
|
? this.selected.address || '已选点,可确认回填'
|
||||||
|
: '可搜索地址或点击地图选点';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -82,6 +94,32 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
hasCoordinateValue(value) {
|
||||||
|
return value !== '' && value !== null && value !== undefined;
|
||||||
|
},
|
||||||
|
buildInitialSelection() {
|
||||||
|
// Number('') === 0,空字符串不能当有效坐标,否则会落到 [0,0](非洲附近)
|
||||||
|
if (!this.hasCoordinateValue(this.longitude) || !this.hasCoordinateValue(this.latitude)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const longitude = Number(this.longitude);
|
||||||
|
const latitude = Number(this.latitude);
|
||||||
|
if (
|
||||||
|
!Number.isFinite(longitude) ||
|
||||||
|
!Number.isFinite(latitude) ||
|
||||||
|
longitude < -180 ||
|
||||||
|
longitude > 180 ||
|
||||||
|
latitude < -90 ||
|
||||||
|
latitude > 90
|
||||||
|
) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
longitude,
|
||||||
|
latitude,
|
||||||
|
address: this.address || '',
|
||||||
|
};
|
||||||
|
},
|
||||||
loadAmap() {
|
loadAmap() {
|
||||||
if (window.AMap && window.AMap.Map) {
|
if (window.AMap && window.AMap.Map) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@@ -110,15 +148,29 @@ export default {
|
|||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (!this.$refs.map) return resolve(null);
|
if (!this.$refs.map) return resolve(null);
|
||||||
|
const initial = this.buildInitialSelection();
|
||||||
|
const hasPoint =
|
||||||
|
Number.isFinite(initial.longitude) && Number.isFinite(initial.latitude);
|
||||||
|
const center = hasPoint
|
||||||
|
? [initial.longitude, initial.latitude]
|
||||||
|
: [116.40769, 39.89945];
|
||||||
|
const zoom = hasPoint ? 14 : 11;
|
||||||
if (!this.amap) {
|
if (!this.amap) {
|
||||||
this.amap = new window.AMap.Map(this.$refs.map, {
|
this.amap = new window.AMap.Map(this.$refs.map, {
|
||||||
center: [116.40769, 39.89945],
|
center,
|
||||||
zoom: 11,
|
zoom,
|
||||||
});
|
});
|
||||||
this.amap.on('click', event => this.pickPoint(event.lnglat));
|
this.amap.on('click', event => this.pickPoint(event.lnglat));
|
||||||
} else {
|
} else {
|
||||||
this.amap.resize();
|
this.amap.resize();
|
||||||
this.clearMarker();
|
this.clearMarker();
|
||||||
|
this.amap.setZoomAndCenter(zoom, center);
|
||||||
|
}
|
||||||
|
if (hasPoint) {
|
||||||
|
const point = new window.AMap.LngLat(initial.longitude, initial.latitude);
|
||||||
|
this.renderMarker(point);
|
||||||
|
this.selected = { ...initial };
|
||||||
|
this.status = initial.address || '已选点,可确认回填';
|
||||||
}
|
}
|
||||||
resolve(this.amap);
|
resolve(this.amap);
|
||||||
});
|
});
|
||||||
@@ -176,23 +228,17 @@ export default {
|
|||||||
this.searchResults = [];
|
this.searchResults = [];
|
||||||
this.searching = true;
|
this.searching = true;
|
||||||
this.initMap()
|
this.initMap()
|
||||||
.then(map => (map ? this.runGeocode('location', keyword) : null))
|
.then(map => (map ? searchAmapPlaceText(keyword) : null))
|
||||||
.then(result => {
|
.then(pois => {
|
||||||
if (!result) return;
|
if (!pois) return;
|
||||||
const geocodes = result.geocodes || (result.location ? [result] : []);
|
this.searchResults = normalizeMapSearchResults(pois, keyword);
|
||||||
this.searchResults = geocodes.map((item, index) => ({
|
const first = this.searchResults[0];
|
||||||
id: item.id || index,
|
if (!first?.location) {
|
||||||
name: item.formattedAddress || item.address || keyword,
|
|
||||||
address: item.formattedAddress || item.address || keyword,
|
|
||||||
location: item.location,
|
|
||||||
}));
|
|
||||||
const point = geocodes[0]?.location || result.location;
|
|
||||||
if (!point) {
|
|
||||||
throw new Error('未找到匹配地址');
|
throw new Error('未找到匹配地址');
|
||||||
}
|
}
|
||||||
return this.$nextTick().then(() => {
|
return this.$nextTick().then(() => {
|
||||||
this.amap?.resize();
|
this.amap?.resize();
|
||||||
this.pickPoint(point, geocodes[0]?.formattedAddress || keyword);
|
this.pickPoint(first.location, first.address || first.name || keyword);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -209,8 +255,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
pickPoint(point, fallbackAddress = '') {
|
pickPoint(point, fallbackAddress = '') {
|
||||||
const longitude = typeof point.getLng === 'function' ? point.getLng() : point.lng;
|
const longitude =
|
||||||
const latitude = typeof point.getLat === 'function' ? point.getLat() : point.lat;
|
typeof point.getLng === 'function' ? point.getLng() : point.lng ?? point.longitude;
|
||||||
|
const latitude =
|
||||||
|
typeof point.getLat === 'function' ? point.getLat() : point.lat ?? point.latitude;
|
||||||
if (!Number.isFinite(Number(longitude)) || !Number.isFinite(Number(latitude))) {
|
if (!Number.isFinite(Number(longitude)) || !Number.isFinite(Number(latitude))) {
|
||||||
this.$message.warning('选点坐标无效');
|
this.$message.warning('选点坐标无效');
|
||||||
return;
|
return;
|
||||||
@@ -227,7 +275,12 @@ export default {
|
|||||||
this.runGeocode('address', lnglat)
|
this.runGeocode('address', lnglat)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
const detailAddress = result.regeocode?.formattedAddress || fallbackAddress;
|
const detailAddress = result.regeocode?.formattedAddress || fallbackAddress;
|
||||||
this.selected = { ...this.selected, address: detailAddress };
|
const region = this.resolveRegionFromGeocode(result);
|
||||||
|
this.selected = {
|
||||||
|
...this.selected,
|
||||||
|
address: detailAddress,
|
||||||
|
...region,
|
||||||
|
};
|
||||||
this.keyword = detailAddress || this.keyword;
|
this.keyword = detailAddress || this.keyword;
|
||||||
this.status = detailAddress || '已选点,可确认回填';
|
this.status = detailAddress || '已选点,可确认回填';
|
||||||
})
|
})
|
||||||
@@ -241,6 +294,26 @@ export default {
|
|||||||
this.resolving = false;
|
this.resolving = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
resolveRegionFromGeocode(result = {}) {
|
||||||
|
const component = result.regeocode?.addressComponent || {};
|
||||||
|
const city = Array.isArray(component.city) ? '' : String(component.city || '').trim();
|
||||||
|
const province = String(component.province || '').trim();
|
||||||
|
const district = String(component.district || '').trim();
|
||||||
|
const country = String(component.country || '').trim();
|
||||||
|
const adcode = String(component.adcode || '').trim();
|
||||||
|
return {
|
||||||
|
country,
|
||||||
|
province,
|
||||||
|
city: city || province,
|
||||||
|
district,
|
||||||
|
adcode,
|
||||||
|
regionCode: adcode,
|
||||||
|
provinceName: province,
|
||||||
|
cityName: city || province,
|
||||||
|
districtName: district,
|
||||||
|
countryName: country,
|
||||||
|
};
|
||||||
|
},
|
||||||
renderMarker(point) {
|
renderMarker(point) {
|
||||||
this.clearMarker();
|
this.clearMarker();
|
||||||
this.marker = new window.AMap.Marker({ position: point });
|
this.marker = new window.AMap.Marker({ position: point });
|
||||||
@@ -253,12 +326,53 @@ export default {
|
|||||||
this.marker = null;
|
this.marker = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
syncSelectedFromMarker() {
|
||||||
|
// 确认时以当前标记点坐标为准,避免换地址后仍回填上一次经纬度
|
||||||
|
if (!this.marker || typeof this.marker.getPosition !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const point = this.marker.getPosition();
|
||||||
|
if (!point) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const longitude =
|
||||||
|
typeof point.getLng === 'function' ? point.getLng() : point.lng ?? point.longitude;
|
||||||
|
const latitude =
|
||||||
|
typeof point.getLat === 'function' ? point.getLat() : point.lat ?? point.latitude;
|
||||||
|
if (!Number.isFinite(Number(longitude)) || !Number.isFinite(Number(latitude))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.selected = {
|
||||||
|
...this.selected,
|
||||||
|
longitude: Number(longitude),
|
||||||
|
latitude: Number(latitude),
|
||||||
|
};
|
||||||
|
},
|
||||||
confirm() {
|
confirm() {
|
||||||
|
this.syncSelectedFromMarker();
|
||||||
|
if (!this.hasCoordinateValue(this.selected.longitude) || !this.hasCoordinateValue(this.selected.latitude)) {
|
||||||
|
this.$message.warning('请先搜索或点击地图完成选点');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.selected.address) {
|
if (!this.selected.address) {
|
||||||
this.$message.warning('请等待地址解析完成后再确认');
|
this.$message.warning('请等待地址解析完成后再确认');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.$emit('confirm', this.selected.address);
|
this.$emit('confirm', {
|
||||||
|
address: this.selected.address,
|
||||||
|
longitude: Number(this.selected.longitude),
|
||||||
|
latitude: Number(this.selected.latitude),
|
||||||
|
country: this.selected.country || this.selected.countryName || '',
|
||||||
|
province: this.selected.province || this.selected.provinceName || '',
|
||||||
|
city: this.selected.city || this.selected.cityName || '',
|
||||||
|
district: this.selected.district || this.selected.districtName || '',
|
||||||
|
adcode: this.selected.adcode || this.selected.regionCode || '',
|
||||||
|
regionCode: this.selected.regionCode || this.selected.adcode || '',
|
||||||
|
countryName: this.selected.countryName || this.selected.country || '',
|
||||||
|
provinceName: this.selected.provinceName || this.selected.province || '',
|
||||||
|
cityName: this.selected.cityName || this.selected.city || '',
|
||||||
|
districtName: this.selected.districtName || this.selected.district || '',
|
||||||
|
});
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -278,7 +392,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__map {
|
&__map {
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 420px;
|
height: 420px;
|
||||||
@@ -286,9 +399,19 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
display: flex;
|
position: relative;
|
||||||
gap: 12px;
|
display: block;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.address-map-picker__map {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.map-search-results) {
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="modelValue"
|
||||||
|
title="变更记录详情"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
align-center
|
||||||
|
width="1100px"
|
||||||
|
class="change-record-detail-dialog"
|
||||||
|
@update:model-value="$emit('update:modelValue', $event)"
|
||||||
|
@open="resetPage"
|
||||||
|
>
|
||||||
|
<el-table
|
||||||
|
:data="pageRows"
|
||||||
|
border
|
||||||
|
max-height="60vh"
|
||||||
|
:show-overflow-tooltip="false"
|
||||||
|
>
|
||||||
|
<el-table-column prop="field" label="变更字段" min-width="180" />
|
||||||
|
<el-table-column
|
||||||
|
prop="before"
|
||||||
|
label="变更前"
|
||||||
|
min-width="360"
|
||||||
|
class-name="change-record-detail-value"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="after"
|
||||||
|
label="变更后"
|
||||||
|
min-width="500"
|
||||||
|
class-name="change-record-detail-value"
|
||||||
|
/>
|
||||||
|
</el-table>
|
||||||
|
<el-empty v-if="!total" description="暂无变更内容" :image-size="60" />
|
||||||
|
<div class="change-record-detail-dialog__pager">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
background
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="$emit('update:modelValue', false)">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ChangeRecordDetailDialog',
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
rows: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
total() {
|
||||||
|
return this.rows.length;
|
||||||
|
},
|
||||||
|
pageRows() {
|
||||||
|
const start = (this.currentPage - 1) * this.pageSize;
|
||||||
|
return this.rows.slice(start, start + this.pageSize);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
rows() {
|
||||||
|
const maxPage = Math.max(1, Math.ceil(this.total / this.pageSize) || 1);
|
||||||
|
if (this.currentPage > maxPage) this.currentPage = 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetPage() {
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.pageSize = 10;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.change-record-detail-dialog__pager {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.change-record-detail-value .cell) {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow: visible;
|
||||||
|
text-overflow: clip;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -576,7 +576,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mode-content :deep(.el-form-item) {
|
.mode-content :deep(.el-form-item) {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-content :deep(.el-form-item:last-child) {
|
.mode-content :deep(.el-form-item:last-child) {
|
||||||
|
|||||||
@@ -1,20 +1,55 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="results.length" class="map-search-results">
|
<div v-if="results.length" class="map-search-results">
|
||||||
<div class="map-search-results__title">搜索结果</div>
|
<div class="map-search-results__list">
|
||||||
<el-empty v-if="!results.length" description="暂无搜索结果" :image-size="60" />
|
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in results"
|
v-for="(item, index) in pageResults"
|
||||||
:key="item.id || index"
|
:key="item.id || `${currentPage}-${index}`"
|
||||||
class="map-search-results__item"
|
class="map-search-results__item"
|
||||||
@click="$emit('select', item)"
|
:class="{ 'is-active': isActive(item, index) }"
|
||||||
|
@click="handleSelect(item, index)"
|
||||||
>
|
>
|
||||||
<div class="map-search-results__name">{{ item.name || item.address || '未命名地址' }}</div>
|
<img
|
||||||
<div class="map-search-results__address">{{ item.address || item.name || '-' }}</div>
|
v-if="item.photo || item.image"
|
||||||
|
class="map-search-results__thumb"
|
||||||
|
:src="item.photo || item.image"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<div class="map-search-results__body">
|
||||||
|
<div class="map-search-results__name">
|
||||||
|
{{ item.name || item.address || '未命名地址' }}
|
||||||
|
</div>
|
||||||
|
<div class="map-search-results__address">
|
||||||
|
地址:{{ item.address || item.name || '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="totalPages > 1" class="map-search-results__pager">
|
||||||
|
<button
|
||||||
|
v-for="page in visiblePages"
|
||||||
|
:key="page"
|
||||||
|
type="button"
|
||||||
|
class="map-search-results__page"
|
||||||
|
:class="{ 'is-active': page === currentPage }"
|
||||||
|
@click="currentPage = page"
|
||||||
|
>
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="map-search-results__page map-search-results__page--next"
|
||||||
|
:disabled="currentPage >= totalPages"
|
||||||
|
@click="goNext"
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const PAGE_SIZE = 5;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
results: {
|
results: {
|
||||||
@@ -23,43 +58,167 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: ['select'],
|
emits: ['select'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentPage: 1,
|
||||||
|
activeKey: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
totalPages() {
|
||||||
|
return Math.max(1, Math.ceil((this.results || []).length / PAGE_SIZE));
|
||||||
|
},
|
||||||
|
pageResults() {
|
||||||
|
const start = (this.currentPage - 1) * PAGE_SIZE;
|
||||||
|
return (this.results || []).slice(start, start + PAGE_SIZE);
|
||||||
|
},
|
||||||
|
visiblePages() {
|
||||||
|
const maxButtons = 3;
|
||||||
|
let start = Math.max(1, this.currentPage - 1);
|
||||||
|
let end = Math.min(this.totalPages, start + maxButtons - 1);
|
||||||
|
start = Math.max(1, end - maxButtons + 1);
|
||||||
|
const pages = [];
|
||||||
|
for (let page = start; page <= end; page += 1) {
|
||||||
|
pages.push(page);
|
||||||
|
}
|
||||||
|
return pages;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
results() {
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.activeKey = '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resultKey(item, index) {
|
||||||
|
return item?.id ?? `${this.currentPage}-${index}`;
|
||||||
|
},
|
||||||
|
isActive(item, index) {
|
||||||
|
return this.activeKey === this.resultKey(item, index);
|
||||||
|
},
|
||||||
|
handleSelect(item, index) {
|
||||||
|
this.activeKey = this.resultKey(item, index);
|
||||||
|
this.$emit('select', item);
|
||||||
|
},
|
||||||
|
goNext() {
|
||||||
|
if (this.currentPage < this.totalPages) {
|
||||||
|
this.currentPage += 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.map-search-results {
|
.map-search-results {
|
||||||
width: 260px;
|
position: absolute;
|
||||||
height: 420px;
|
top: 0;
|
||||||
overflow-y: auto;
|
left: 0;
|
||||||
border: 1px solid #eff1f7;
|
z-index: 2000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 360px;
|
||||||
|
max-height: 360px;
|
||||||
|
overflow: hidden;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
flex: 0 0 260px;
|
border: 1px solid #dcdfe6;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
&__title {
|
&__list {
|
||||||
padding: 10px 12px;
|
flex: 1 1 auto;
|
||||||
border-bottom: 1px solid #eff1f7;
|
min-height: 0;
|
||||||
font-weight: 600;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 1px solid #f5f5f5;
|
border-bottom: 1px solid #ebeef5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item:hover {
|
&__item:hover,
|
||||||
background: #f5f9ff;
|
&__item.is-active {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__thumb {
|
||||||
|
flex: 0 0 48px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__name {
|
&__name {
|
||||||
|
overflow: hidden;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__address {
|
&__address {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pager {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__page {
|
||||||
|
min-width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 26px;
|
||||||
|
text-align: center;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
color: #409eff;
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
color: #409eff;
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: #c0c4cc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--next {
|
||||||
|
min-width: 56px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<div class="avue-crud__right table-header-tools">
|
||||||
|
<el-button v-if="showRefresh" icon="el-icon-refresh" circle @click="$emit('refresh')" />
|
||||||
|
<el-button
|
||||||
|
v-if="showColumn"
|
||||||
|
icon="el-icon-operation"
|
||||||
|
circle
|
||||||
|
@click="columnSettingVisible = true"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
v-if="showSearch"
|
||||||
|
icon="el-icon-search"
|
||||||
|
circle
|
||||||
|
@click="$emit('update:searchVisible', !searchVisible)"
|
||||||
|
/>
|
||||||
|
<el-button v-if="showGrid" icon="el-icon-grid" circle @click="toggleTableSize" />
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-if="showColumn"
|
||||||
|
v-model="columnSettingVisible"
|
||||||
|
title="列设置"
|
||||||
|
width="360px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<el-checkbox-group
|
||||||
|
class="table-header-tools__column-setting"
|
||||||
|
:model-value="visibleColumnKeys"
|
||||||
|
@update:model-value="$emit('update:visibleColumnKeys', $event)"
|
||||||
|
>
|
||||||
|
<el-checkbox v-for="item in columns" :key="item.prop" :label="item.prop">
|
||||||
|
{{ item.label }}
|
||||||
|
</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="columnSettingVisible = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const TABLE_SIZE_OPTIONS = ['default', 'large', 'small'];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TableHeaderTools',
|
||||||
|
props: {
|
||||||
|
columns: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
visibleColumnKeys: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
tableSize: {
|
||||||
|
type: String,
|
||||||
|
default: 'default',
|
||||||
|
},
|
||||||
|
searchVisible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
showRefresh: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
showColumn: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
showSearch: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
showGrid: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['refresh', 'update:visibleColumnKeys', 'update:tableSize', 'update:searchVisible'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
columnSettingVisible: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleTableSize() {
|
||||||
|
const index = TABLE_SIZE_OPTIONS.indexOf(this.tableSize);
|
||||||
|
const next = TABLE_SIZE_OPTIONS[(index + 1) % TABLE_SIZE_OPTIONS.length];
|
||||||
|
this.$emit('update:tableSize', next);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.table-header-tools.avue-crud__right {
|
||||||
|
width: auto;
|
||||||
|
margin: 0 0 0 auto;
|
||||||
|
flex: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header-tools__column-setting {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 360px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -9,18 +9,21 @@
|
|||||||
<el-table :data="rows" border @selection-change="selectedRows = $event">
|
<el-table :data="rows" border @selection-change="selectedRows = $event">
|
||||||
<el-table-column v-if="!readonly" type="selection" width="55" align="center" />
|
<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 type="index" label="序号" width="70" align="center" />
|
||||||
<el-table-column label="文件名" min-width="240" show-overflow-tooltip>
|
<el-table-column
|
||||||
|
label="文件名"
|
||||||
|
min-width="240"
|
||||||
|
show-overflow-tooltip
|
||||||
|
class-name="is-ellipsis-link"
|
||||||
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span
|
<span
|
||||||
class="vehicle-attachment-table__file-name"
|
class="attachment-file-name"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click="previewAttachment(row)"
|
@click="previewAttachment(row)"
|
||||||
@keydown.enter="previewAttachment(row)"
|
@keydown.enter="previewAttachment(row)"
|
||||||
@keydown.space.prevent="previewAttachment(row)"
|
@keydown.space.prevent="previewAttachment(row)"
|
||||||
>
|
>{{ attachmentName(row) }}</span>
|
||||||
{{ attachmentName(row) }}
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="附件描述" min-width="220">
|
<el-table-column label="附件描述" min-width="220">
|
||||||
@@ -308,16 +311,6 @@ export default {
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__file-name {
|
|
||||||
color: #409eff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__file-name:hover,
|
|
||||||
&__file-name:focus-visible {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__upload {
|
&__upload {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|||||||
@@ -212,7 +212,14 @@ export default {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
},
|
},
|
||||||
acceptText() {
|
acceptText() {
|
||||||
return this.acceptedList.join(',');
|
return this.acceptedList
|
||||||
|
.map(item => {
|
||||||
|
const value = String(item).trim();
|
||||||
|
if (!value || value.includes('/') || value.startsWith('.')) return value;
|
||||||
|
return `.${value}`;
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(',');
|
||||||
},
|
},
|
||||||
tipText() {
|
tipText() {
|
||||||
return this.tip || `${UNIFIED_ATTACHMENT_TIP_PREFIX}${this.maxSize}M`;
|
return this.tip || `${UNIFIED_ATTACHMENT_TIP_PREFIX}${this.maxSize}M`;
|
||||||
@@ -304,12 +311,22 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleSuccess(response, file, files) {
|
handleSuccess(response, file, files) {
|
||||||
if (!response || response.success === false || (response.code && response.code !== 200)) {
|
const code = response?.code;
|
||||||
|
const codeInvalid = code !== undefined && code !== null && code !== '' && Number(code) !== 200;
|
||||||
|
if (!response || response.success === false || codeInvalid) {
|
||||||
this.$message.error((response && response.msg) || '上传失败');
|
this.$message.error((response && response.msg) || '上传失败');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.emitUploadFiles(this.multiple ? files : [file]);
|
if (file && !file.response) {
|
||||||
this.$emit('success', this.normalizeUploadFile(file));
|
file.response = response;
|
||||||
|
}
|
||||||
|
const normalized = this.normalizeUploadFile(file, response);
|
||||||
|
if (!normalized.url) {
|
||||||
|
this.$message.error('上传成功但未返回文件地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$emit('success', normalized);
|
||||||
|
this.emitUploadFiles(this.multiple ? files : [file], normalized);
|
||||||
this.$message.success('上传成功');
|
this.$message.success('上传成功');
|
||||||
},
|
},
|
||||||
handleChange(file, files) {
|
handleChange(file, files) {
|
||||||
@@ -323,35 +340,89 @@ export default {
|
|||||||
this.emitUploadFiles(files);
|
this.emitUploadFiles(files);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
emitUploadFiles(files) {
|
emitUploadFiles(files, preferredFile) {
|
||||||
const list = files
|
const list = (files || [])
|
||||||
.filter(file => file.status === 'success' || this.getFileUrl(file))
|
.filter(file => {
|
||||||
.map(this.normalizeUploadFile)
|
if (file?.status === 'fail') return false;
|
||||||
|
return (
|
||||||
|
file?.status === 'success' ||
|
||||||
|
this.getFileUrl(file) ||
|
||||||
|
file?.response?.data ||
|
||||||
|
file?.response?.link ||
|
||||||
|
file?.response
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map(file => this.normalizeUploadFile(file))
|
||||||
.filter(item => item.url);
|
.filter(item => item.url);
|
||||||
this.$emit('update:modelValue', list);
|
|
||||||
this.$emit('change', list);
|
if (preferredFile?.url) {
|
||||||
|
const exists = list.some(
|
||||||
|
item =>
|
||||||
|
(preferredFile.uid && item.uid && String(item.uid) === String(preferredFile.uid)) ||
|
||||||
|
item.url === preferredFile.url
|
||||||
|
);
|
||||||
|
if (!exists) list.push(preferredFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = this.parseValue(this.modelValue).map(item => ({
|
||||||
|
...item,
|
||||||
|
url: this.getFileUrl(item),
|
||||||
|
}));
|
||||||
|
const merged = [...current];
|
||||||
|
list.forEach(file => {
|
||||||
|
const index = merged.findIndex(
|
||||||
|
item =>
|
||||||
|
(file.uid && item.uid && String(item.uid) === String(file.uid)) ||
|
||||||
|
(file.url && this.getFileUrl(item) === file.url)
|
||||||
|
);
|
||||||
|
if (index >= 0) merged.splice(index, 1, { ...merged[index], ...file });
|
||||||
|
else merged.push(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = merged.filter(item => this.getFileUrl(item));
|
||||||
|
this.$emit('update:modelValue', result);
|
||||||
|
this.$emit('change', result);
|
||||||
},
|
},
|
||||||
normalizeUploadFile(file) {
|
normalizeUploadFile(file, responseOverride) {
|
||||||
const data = (file.response && file.response.data) || file.data || file;
|
const response = responseOverride || file?.response;
|
||||||
const url = data.link || data.url || data.domain || file.url || '';
|
let data = {};
|
||||||
|
if (response && typeof response === 'object') {
|
||||||
|
if (response.data && typeof response.data === 'object') {
|
||||||
|
data = response.data;
|
||||||
|
} else if (response.link || response.url || response.domain) {
|
||||||
|
data = response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!data.link && !data.url && !data.domain) {
|
||||||
|
data = file?.data && typeof file.data === 'object' ? file.data : file || {};
|
||||||
|
}
|
||||||
|
const url =
|
||||||
|
data.link ||
|
||||||
|
data.url ||
|
||||||
|
data.domain ||
|
||||||
|
data.fileLink ||
|
||||||
|
data.fileUrl ||
|
||||||
|
file?.url ||
|
||||||
|
file?.link ||
|
||||||
|
'';
|
||||||
const originalName =
|
const originalName =
|
||||||
data.originalName ||
|
data.originalName ||
|
||||||
file.originalName ||
|
file?.originalName ||
|
||||||
file.name ||
|
file?.name ||
|
||||||
data.name ||
|
data.name ||
|
||||||
this.getFileName(url) ||
|
this.getFileName(url) ||
|
||||||
'附件';
|
'附件';
|
||||||
const extension = this.getExtension({ name: originalName, url });
|
const extension = this.getExtension({ name: originalName, url });
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
uid: file.uid || data.uid,
|
uid: file?.uid || data.uid,
|
||||||
originalName,
|
originalName,
|
||||||
name: originalName,
|
name: originalName,
|
||||||
url,
|
url,
|
||||||
link: data.link || url,
|
link: data.link || url,
|
||||||
size: data.size || data.attachSize || file.size || '',
|
size: data.size || data.attachSize || file?.size || '',
|
||||||
extension,
|
extension,
|
||||||
mimeType: data.mimeType || data.contentType || file.raw?.type || MIME_MAP[extension] || '',
|
mimeType: data.mimeType || data.contentType || file?.raw?.type || MIME_MAP[extension] || '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handlePreview(file) {
|
handlePreview(file) {
|
||||||
|
|||||||
@@ -146,6 +146,13 @@ export const createOption = () => ({
|
|||||||
showWordLimit: true,
|
showWordLimit: true,
|
||||||
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
minWidth: 120,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '更新时间',
|
label: '更新时间',
|
||||||
prop: 'updateTime',
|
prop: 'updateTime',
|
||||||
@@ -298,6 +305,10 @@ export const createOption = () => ({
|
|||||||
searchOrder: 3,
|
searchOrder: 3,
|
||||||
searchPlaceholder: '请选择',
|
searchPlaceholder: '请选择',
|
||||||
dicData: [],
|
dicData: [],
|
||||||
|
props: {
|
||||||
|
label: 'label',
|
||||||
|
value: 'value',
|
||||||
|
},
|
||||||
filterable: true,
|
filterable: true,
|
||||||
hide: true,
|
hide: true,
|
||||||
display: false,
|
display: false,
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ const dimensionOptions = [
|
|||||||
{ label: '重量', value: '重量' },
|
{ label: '重量', value: '重量' },
|
||||||
{ label: '体积', value: '体积' },
|
{ label: '体积', value: '体积' },
|
||||||
{ label: '数量', value: '数量' },
|
{ label: '数量', value: '数量' },
|
||||||
|
{ label: '整车', value: '整车' },
|
||||||
|
{ label: '里程', value: '里程' },
|
||||||
|
{ label: '吨·公里', value: '吨·公里' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
@@ -42,6 +45,7 @@ export const createOption = () => ({
|
|||||||
label: '计量单位编码',
|
label: '计量单位编码',
|
||||||
prop: 'unitCode',
|
prop: 'unitCode',
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
|
span: 24,
|
||||||
search: true,
|
search: true,
|
||||||
searchOrder: 4,
|
searchOrder: 4,
|
||||||
searchSpan: 6,
|
searchSpan: 6,
|
||||||
|
|||||||
+143
-139
@@ -35,26 +35,7 @@ export const formatCargoValue = value => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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',
|
||||||
@@ -79,12 +60,24 @@ export const option = createCrudOption([
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '一级货物类型编码',
|
label: '一级货物类型',
|
||||||
prop: 'firstCargoTypeCode',
|
prop: 'firstCargoTypeName',
|
||||||
hide: true,
|
formslot: true,
|
||||||
display: false,
|
order: 60,
|
||||||
addDisplay: false,
|
span: 12,
|
||||||
|
minWidth: 150,
|
||||||
editDisabled: true,
|
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: '二级货物类型编码',
|
||||||
@@ -92,9 +85,132 @@ export const option = createCrudOption([
|
|||||||
display: false,
|
display: false,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisabled: true,
|
editDisabled: true,
|
||||||
minWidth: 180,
|
minWidth: 160,
|
||||||
},
|
},
|
||||||
// 搜索字段与表格、表单字段独立,避免筛选配置影响数据展示和编辑布局。
|
{
|
||||||
|
label: '包装',
|
||||||
|
prop: 'packageType',
|
||||||
|
type: 'select',
|
||||||
|
dicData: packageOptions,
|
||||||
|
order: 39,
|
||||||
|
span: 12,
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '品牌',
|
||||||
|
prop: 'brand',
|
||||||
|
search: false,
|
||||||
|
order: 40,
|
||||||
|
span: 12,
|
||||||
|
minWidth: 120,
|
||||||
|
placeholder: '请输入',
|
||||||
|
rules: textRule('品牌', 50),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '规格',
|
||||||
|
prop: 'specification',
|
||||||
|
order: 29,
|
||||||
|
span: 12,
|
||||||
|
minWidth: 120,
|
||||||
|
placeholder: '请输入',
|
||||||
|
rules: textRule('规格', 50),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '型号',
|
||||||
|
prop: 'model',
|
||||||
|
order: 19,
|
||||||
|
span: 12,
|
||||||
|
minWidth: 120,
|
||||||
|
placeholder: '请输入',
|
||||||
|
rules: textRule('型号', 50, true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '单价',
|
||||||
|
prop: 'cargoValue',
|
||||||
|
formslot: true,
|
||||||
|
order: 30,
|
||||||
|
formatter: row => formatCargoValue(row.cargoValue),
|
||||||
|
minWidth: 120,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!/^\d+(\.\d{1,2})?$/.test(String(value))) {
|
||||||
|
callback(new Error('单价只能输入数字,最多保留2位小数'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '计价单位',
|
||||||
|
prop: 'priceUnit',
|
||||||
|
type: 'select',
|
||||||
|
dicData: [],
|
||||||
|
display: false,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '尺寸',
|
||||||
|
prop: 'sizeText',
|
||||||
|
minWidth: 120,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
rules: textRule('尺寸', 100),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '说明',
|
||||||
|
prop: 'descriptionOne',
|
||||||
|
order: 20,
|
||||||
|
span: 12,
|
||||||
|
minWidth: 160,
|
||||||
|
placeholder: '请输入',
|
||||||
|
rules: textRule('说明', 100),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '备注',
|
||||||
|
prop: 'remark',
|
||||||
|
type: 'textarea',
|
||||||
|
order: 10,
|
||||||
|
minRows: 2,
|
||||||
|
span: 24,
|
||||||
|
maxlength: 200,
|
||||||
|
showWordLimit: true,
|
||||||
|
placeholder: '请输入',
|
||||||
|
rules: textRule('备注', 200),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '组织',
|
||||||
|
prop: 'deptName',
|
||||||
|
minWidth: 150,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
},
|
||||||
|
...auditColumns
|
||||||
|
.filter(column => ['createUserName', 'createTime', 'updateTime'].includes(column.prop))
|
||||||
|
.sort(
|
||||||
|
(first, second) =>
|
||||||
|
['createUserName', 'createTime', 'updateTime'].indexOf(first.prop) -
|
||||||
|
['createUserName', 'createTime', 'updateTime'].indexOf(second.prop)
|
||||||
|
),
|
||||||
|
|
||||||
|
// ---------- 表单隐藏字段 ----------
|
||||||
|
{
|
||||||
|
label: '一级货物类型编码',
|
||||||
|
prop: 'firstCargoTypeCode',
|
||||||
|
hide: true,
|
||||||
|
display: false,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisabled: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- 搜索字段(与表格、表单独立) ----------
|
||||||
{
|
{
|
||||||
label: '货物名称',
|
label: '货物名称',
|
||||||
prop: 'searchCargoName',
|
prop: 'searchCargoName',
|
||||||
@@ -186,118 +302,6 @@ export const option = createCrudOption([
|
|||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
viewDisplay: false,
|
viewDisplay: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: '包装',
|
|
||||||
prop: 'packageType',
|
|
||||||
type: 'select',
|
|
||||||
dicData: packageOptions,
|
|
||||||
order: 39,
|
|
||||||
span: 12,
|
|
||||||
minWidth: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '品牌',
|
|
||||||
prop: 'brand',
|
|
||||||
search: false,
|
|
||||||
order: 40,
|
|
||||||
span: 12,
|
|
||||||
minWidth: 120,
|
|
||||||
placeholder: '请输入',
|
|
||||||
rules: textRule('品牌', 50),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '规格',
|
|
||||||
prop: 'specification',
|
|
||||||
order: 29,
|
|
||||||
span: 12,
|
|
||||||
minWidth: 120,
|
|
||||||
placeholder: '请输入',
|
|
||||||
rules: textRule('规格', 50),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '货值',
|
|
||||||
prop: 'cargoValue',
|
|
||||||
formslot: true,
|
|
||||||
order: 30,
|
|
||||||
formatter: row => formatCargoValue(row.cargoValue),
|
|
||||||
minWidth: 120,
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
validator: (rule, value, callback) => {
|
|
||||||
if (value === undefined || value === null || value === '') {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!/^\d+(\.\d{1,2})?$/.test(String(value))) {
|
|
||||||
callback(new Error('货值只能输入数字,最多保留2位小数'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
callback();
|
|
||||||
},
|
|
||||||
trigger: 'blur',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '计价单位',
|
|
||||||
prop: 'priceUnit',
|
|
||||||
type: 'select',
|
|
||||||
dicData: [],
|
|
||||||
display: false,
|
|
||||||
minWidth: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '型号',
|
|
||||||
prop: 'model',
|
|
||||||
order: 19,
|
|
||||||
span: 12,
|
|
||||||
minWidth: 120,
|
|
||||||
placeholder: '请输入',
|
|
||||||
rules: textRule('型号', 50, true),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '尺寸',
|
|
||||||
prop: 'sizeText',
|
|
||||||
minWidth: 120,
|
|
||||||
addDisplay: false,
|
|
||||||
editDisplay: false,
|
|
||||||
rules: textRule('尺寸', 100),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '说明',
|
|
||||||
prop: 'descriptionOne',
|
|
||||||
order: 20,
|
|
||||||
span: 12,
|
|
||||||
minWidth: 160,
|
|
||||||
placeholder: '请输入',
|
|
||||||
rules: textRule('说明', 100),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '备注',
|
|
||||||
prop: 'remark',
|
|
||||||
type: 'textarea',
|
|
||||||
order: 10,
|
|
||||||
minRows: 2,
|
|
||||||
span: 24,
|
|
||||||
maxlength: 200,
|
|
||||||
showWordLimit: true,
|
|
||||||
placeholder: '请输入',
|
|
||||||
rules: textRule('备注', 200),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '组织',
|
|
||||||
prop: 'deptName',
|
|
||||||
minWidth: 150,
|
|
||||||
addDisplay: false,
|
|
||||||
editDisplay: false,
|
|
||||||
},
|
|
||||||
...auditColumns
|
|
||||||
.filter(column => ['updateTime', 'createTime'].includes(column.prop))
|
|
||||||
.sort(
|
|
||||||
(first, second) =>
|
|
||||||
['updateTime', 'createTime'].indexOf(first.prop) -
|
|
||||||
['updateTime', 'createTime'].indexOf(second.prop)
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 搜索栏标签统一加宽,给较长的货物类型字段预留足够展示空间。
|
// 搜索栏标签统一加宽,给较长的货物类型字段预留足够展示空间。
|
||||||
|
|||||||
@@ -281,14 +281,13 @@ export const option = {
|
|||||||
filterable: true,
|
filterable: true,
|
||||||
},
|
},
|
||||||
...auditColumns
|
...auditColumns
|
||||||
.filter(column => column.prop !== 'createUserName')
|
|
||||||
.map(column =>
|
.map(column =>
|
||||||
['createTime', 'updateTime'].includes(column.prop) ? { ...column, minWidth: 190 } : column
|
['createTime', 'updateTime'].includes(column.prop) ? { ...column, minWidth: 190 } : column
|
||||||
)
|
)
|
||||||
.sort(
|
.sort(
|
||||||
(first, second) =>
|
(first, second) =>
|
||||||
['updateUserName', 'updateTime', 'createTime'].indexOf(first.prop) -
|
['createUserName', 'updateUserName', 'createTime', 'updateTime'].indexOf(first.prop) -
|
||||||
['updateUserName', 'updateTime', 'createTime'].indexOf(second.prop)
|
['createUserName', 'updateUserName', 'createTime', 'updateTime'].indexOf(second.prop)
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
dialogCustomClass: 'avue-dialog--common-route',
|
dialogCustomClass: 'avue-dialog--common-route',
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ export const contractCategoryOptions = [
|
|||||||
export const signTypeOptions = [
|
export const signTypeOptions = [
|
||||||
{ label: '新签', value: '新签' },
|
{ label: '新签', value: '新签' },
|
||||||
{ label: '续签', value: '续签' },
|
{ label: '续签', value: '续签' },
|
||||||
|
{ label: '补充协议', value: '补充协议' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const contractStageOptions = [
|
export const contractStageOptions = [
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
contractStageOptions,
|
contractStageOptions,
|
||||||
createCrudOption,
|
createCrudOption,
|
||||||
effectiveTypeOptions,
|
effectiveTypeOptions,
|
||||||
|
nonNegativeRule,
|
||||||
selectRule,
|
selectRule,
|
||||||
signTypeOptions,
|
signTypeOptions,
|
||||||
textRule,
|
textRule,
|
||||||
@@ -61,13 +62,14 @@ export const config = {
|
|||||||
['contractNo', '合同编号'],
|
['contractNo', '合同编号'],
|
||||||
['contractName', '合同名称'],
|
['contractName', '合同名称'],
|
||||||
['projectName', '所属项目'],
|
['projectName', '所属项目'],
|
||||||
['organizationName', '所属组织'],
|
['organizationName', '合同负责部门'],
|
||||||
['contractCategory', '合同类别'],
|
['contractCategory', '合同类别'],
|
||||||
['signType', '签约类型'],
|
['signType', '签约类型'],
|
||||||
['partyA', '甲方'],
|
['partyA', '甲方'],
|
||||||
['partyB', '乙方'],
|
['partyB', '乙方'],
|
||||||
['handlerUserName', '经办人'],
|
['handlerUserName', '经办人'],
|
||||||
['signDate', '签订日期'],
|
['signDate', '签订日期'],
|
||||||
|
['applyDate', '申请日期'],
|
||||||
['contractFormat', '合同格式'],
|
['contractFormat', '合同格式'],
|
||||||
['legalSealFlag', '是否需要加盖法人章'],
|
['legalSealFlag', '是否需要加盖法人章'],
|
||||||
['copyCount', '一式(份)'],
|
['copyCount', '一式(份)'],
|
||||||
@@ -76,7 +78,8 @@ export const config = {
|
|||||||
['templateFlag', '是否范本'],
|
['templateFlag', '是否范本'],
|
||||||
['originalContractNo', '原件合同编号'],
|
['originalContractNo', '原件合同编号'],
|
||||||
['electronicSealFlag', '是否电子章'],
|
['electronicSealFlag', '是否电子章'],
|
||||||
['remark', '备注', 2],
|
['mainContent', '合同主要内容', 2],
|
||||||
|
['remark', '其他事项说明', 2],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -114,7 +117,7 @@ export const config = {
|
|||||||
operations: [
|
operations: [
|
||||||
{
|
{
|
||||||
action: 'flow',
|
action: 'flow',
|
||||||
label: '查看流程',
|
label: '流程',
|
||||||
statusProp: 'approvalStatus',
|
statusProp: 'approvalStatus',
|
||||||
status: ['reviewing', 'change_reviewing'],
|
status: ['reviewing', 'change_reviewing'],
|
||||||
stage: ['temporary', 'formal'],
|
stage: ['temporary', 'formal'],
|
||||||
@@ -206,10 +209,10 @@ export const config = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'flow',
|
action: 'flow',
|
||||||
label: '查看流程',
|
label: '流程',
|
||||||
statusProp: 'approvalStatus',
|
statusProp: 'approvalStatus',
|
||||||
status: ['approved', 'change_approved'],
|
status: ['approved', 'change_approved'],
|
||||||
stage: ['formal', 'terminated'],
|
stage: ['temporary', 'formal', 'terminated'],
|
||||||
permission: 'contract_manage_view',
|
permission: 'contract_manage_view',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -294,17 +297,17 @@ export const option = createCrudOption([
|
|||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '所属组织',
|
label: '合同负责部门',
|
||||||
prop: 'organizationName',
|
prop: 'organizationName',
|
||||||
search: true,
|
search: true,
|
||||||
searchOrder: 3,
|
searchOrder: 3,
|
||||||
searchslot: true,
|
searchslot: true,
|
||||||
searchPlaceholder: '请选择 所属组织',
|
searchPlaceholder: '请选择 合同负责部门',
|
||||||
formslot: true,
|
formslot: true,
|
||||||
order: 120,
|
order: 120,
|
||||||
dicData: [],
|
dicData: [],
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
rules: selectRule('所属组织'),
|
rules: selectRule('合同负责部门'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '合同类别',
|
label: '合同类别',
|
||||||
@@ -435,6 +438,17 @@ export const option = createCrudOption([
|
|||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
rules: [{ required: true, message: '请选择签订日期', trigger: 'change' }],
|
rules: [{ required: true, message: '请选择签订日期', trigger: 'change' }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '申请日期',
|
||||||
|
prop: 'applyDate',
|
||||||
|
type: 'date',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
sortable: true,
|
||||||
|
minWidth: 130,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '结算类型',
|
label: '结算类型',
|
||||||
prop: 'settlementMode',
|
prop: 'settlementMode',
|
||||||
@@ -468,11 +482,10 @@ export const option = createCrudOption([
|
|||||||
{ label: '否', value: 0 },
|
{ label: '否', value: 0 },
|
||||||
{ label: '是', value: 1 },
|
{ label: '是', value: 1 },
|
||||||
],
|
],
|
||||||
value: 0,
|
clearable: true,
|
||||||
hide: true,
|
hide: true,
|
||||||
order: 70,
|
order: 70,
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
rules: selectRule('是否需要加盖法人章'),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '一式(份)',
|
label: '一式(份)',
|
||||||
@@ -500,6 +513,10 @@ export const option = createCrudOption([
|
|||||||
hide: true,
|
hide: true,
|
||||||
order: 45,
|
order: 45,
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
|
rules: [
|
||||||
|
{ required: true, message: '请输入合同金额', trigger: 'blur' },
|
||||||
|
nonNegativeRule('合同金额'),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '是否范本',
|
label: '是否范本',
|
||||||
@@ -532,6 +549,13 @@ export const option = createCrudOption([
|
|||||||
order: 30,
|
order: 30,
|
||||||
minWidth: 110,
|
minWidth: 110,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
minWidth: 120,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
prop: 'createTime',
|
prop: 'createTime',
|
||||||
@@ -610,7 +634,19 @@ export const option = createCrudOption([
|
|||||||
minWidth: 110,
|
minWidth: 110,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '备注',
|
label: '合同主要内容',
|
||||||
|
prop: 'mainContent',
|
||||||
|
type: 'textarea',
|
||||||
|
span: 24,
|
||||||
|
minRows: 4,
|
||||||
|
hide: true,
|
||||||
|
order: 41,
|
||||||
|
maxlength: 1000,
|
||||||
|
showWordLimit: true,
|
||||||
|
rules: textRule('合同主要内容', 1000),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '其他事项说明',
|
||||||
prop: 'remark',
|
prop: 'remark',
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
span: 24,
|
span: 24,
|
||||||
@@ -619,7 +655,7 @@ export const option = createCrudOption([
|
|||||||
order: 40,
|
order: 40,
|
||||||
maxlength: 2000,
|
maxlength: 2000,
|
||||||
showWordLimit: true,
|
showWordLimit: true,
|
||||||
rules: textRule('备注', 2000),
|
rules: textRule('其他事项说明', 2000),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '',
|
label: '',
|
||||||
@@ -730,7 +766,7 @@ export const option = createCrudOption([
|
|||||||
labelWidth: 0,
|
labelWidth: 0,
|
||||||
},
|
},
|
||||||
...auditColumns
|
...auditColumns
|
||||||
.filter(column => column.prop !== 'createTime')
|
.filter(column => !['createTime', 'createUserName'].includes(column.prop))
|
||||||
.map(column => ({ ...column, hide: true })),
|
.map(column => ({ ...column, hide: true })),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const loadingManageConfig = {
|
|||||||
exportColumns: [
|
exportColumns: [
|
||||||
{ prop: 'loadingNo', label: '配载单号' },
|
{ prop: 'loadingNo', label: '配载单号' },
|
||||||
{ prop: 'loadingSubNos', label: '配载子单号' },
|
{ prop: 'loadingSubNos', label: '配载子单号' },
|
||||||
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' },
|
{ prop: 'vehicleNo', label: '车牌号/船班号/船次号/班次号' },
|
||||||
{ prop: 'driverName', label: '司机' },
|
{ prop: 'driverName', label: '司机' },
|
||||||
{ prop: 'driverPhone', label: '联系电话' },
|
{ prop: 'driverPhone', label: '联系电话' },
|
||||||
{ prop: 'carrierType', label: '承运类型' },
|
{ prop: 'carrierType', label: '承运类型' },
|
||||||
@@ -41,6 +41,7 @@ export const loadingManageConfig = {
|
|||||||
{ prop: 'carrierName', label: '承运商' },
|
{ prop: 'carrierName', label: '承运商' },
|
||||||
{ prop: 'transportType', label: '运输方式' },
|
{ prop: 'transportType', label: '运输方式' },
|
||||||
{ prop: 'dataSource', label: '数据来源' },
|
{ prop: 'dataSource', label: '数据来源' },
|
||||||
|
{ prop: 'createUserName', label: '创建人' },
|
||||||
{ prop: 'createTime', label: '创建时间' },
|
{ prop: 'createTime', label: '创建时间' },
|
||||||
{ prop: 'updateTime', label: '更新时间' },
|
{ prop: 'updateTime', label: '更新时间' },
|
||||||
{ prop: 'currentProcessNode', label: '过程节点' },
|
{ prop: 'currentProcessNode', label: '过程节点' },
|
||||||
|
|||||||
@@ -112,12 +112,12 @@ export const option = {
|
|||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
},
|
},
|
||||||
...auditColumns
|
...auditColumns
|
||||||
.filter(column => ['updateTime', 'createTime'].includes(column.prop))
|
.filter(column => ['createUserName', 'createTime', 'updateTime'].includes(column.prop))
|
||||||
.map(column => ({ ...column, minWidth: 190 }))
|
.map(column => ({ ...column, minWidth: 190 }))
|
||||||
.sort(
|
.sort(
|
||||||
(first, second) =>
|
(first, second) =>
|
||||||
['updateTime', 'createTime'].indexOf(first.prop) -
|
['createUserName', 'createTime', 'updateTime'].indexOf(first.prop) -
|
||||||
['updateTime', 'createTime'].indexOf(second.prop)
|
['createUserName', 'createTime', 'updateTime'].indexOf(second.prop)
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
label: '项目ID集合',
|
label: '项目ID集合',
|
||||||
|
|||||||
@@ -57,16 +57,24 @@ export const config = {
|
|||||||
action: 'submitApproval',
|
action: 'submitApproval',
|
||||||
label: '提交',
|
label: '提交',
|
||||||
statusProp: 'approvalStatus',
|
statusProp: 'approvalStatus',
|
||||||
status: ['draft'],
|
status: ['draft', 'rejected', 'withdrawn'],
|
||||||
permission: 'project_apply_submit',
|
permission: 'project_apply_submit',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'withdraw',
|
action: 'withdraw',
|
||||||
label: '撤回',
|
label: '撤回',
|
||||||
statusProp: 'approvalStatus',
|
statusProp: 'approvalStatus',
|
||||||
status: ['reviewing'],
|
status: ['draft', 'reviewing'],
|
||||||
permission: 'project_apply_withdraw',
|
permission: 'project_apply_withdraw',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
action: 'mkAudit',
|
||||||
|
label: '流程',
|
||||||
|
statusProp: 'approvalStatus',
|
||||||
|
status: ['reviewing', 'change_reviewing', 'approved', 'change_approved'],
|
||||||
|
permission: 'project_apply_approve',
|
||||||
|
mkOnly: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
action: 'approve',
|
action: 'approve',
|
||||||
label: '通过',
|
label: '通过',
|
||||||
@@ -74,6 +82,7 @@ export const config = {
|
|||||||
statusProp: 'approvalStatus',
|
statusProp: 'approvalStatus',
|
||||||
status: ['reviewing', 'change_reviewing'],
|
status: ['reviewing', 'change_reviewing'],
|
||||||
permission: 'project_apply_approve',
|
permission: 'project_apply_approve',
|
||||||
|
localOnly: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'reject',
|
action: 'reject',
|
||||||
@@ -82,6 +91,7 @@ export const config = {
|
|||||||
statusProp: 'approvalStatus',
|
statusProp: 'approvalStatus',
|
||||||
status: ['reviewing', 'change_reviewing'],
|
status: ['reviewing', 'change_reviewing'],
|
||||||
permission: 'project_apply_reject',
|
permission: 'project_apply_reject',
|
||||||
|
localOnly: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'startChange',
|
action: 'startChange',
|
||||||
@@ -442,5 +452,9 @@ export const option = createCrudOption([
|
|||||||
span: 24,
|
span: 24,
|
||||||
hide: true,
|
hide: true,
|
||||||
},
|
},
|
||||||
...auditColumns.map(column => ({ ...column, hide: true })),
|
...auditColumns.map(column =>
|
||||||
|
['createUserName', 'createTime'].includes(column.prop)
|
||||||
|
? column
|
||||||
|
: { ...column, hide: true }
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -332,9 +332,13 @@ export const option = {
|
|||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
},
|
},
|
||||||
...auditColumns.map(column => ({ ...column, hide: true })),
|
...auditColumns.map(column =>
|
||||||
|
['createUserName', 'createTime'].includes(column.prop)
|
||||||
|
? column
|
||||||
|
: { ...column, hide: true }
|
||||||
|
),
|
||||||
])),
|
])),
|
||||||
dialogWidth: '96%',
|
dialogWidth: 1100,
|
||||||
menuFixed: 'right',
|
menuFixed: 'right',
|
||||||
menuWidth: 320,
|
menuWidth: 320,
|
||||||
index: false,
|
index: false,
|
||||||
|
|||||||
@@ -249,9 +249,10 @@ export const config = {
|
|||||||
title: '其它信息',
|
title: '其它信息',
|
||||||
fields: [
|
fields: [
|
||||||
['remark', '备注'],
|
['remark', '备注'],
|
||||||
|
['createUserName', '创建人'],
|
||||||
|
['createTime', '创建时间'],
|
||||||
['updateUserName', '更新人'],
|
['updateUserName', '更新人'],
|
||||||
['updateTime', '更新时间'],
|
['updateTime', '更新时间'],
|
||||||
['createTime', '创建时间'],
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -399,7 +400,6 @@ export const option = {
|
|||||||
span: 6,
|
span: 6,
|
||||||
order: 350,
|
order: 350,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
rules: [{ required: true, message: '请选择计划开始日期', trigger: 'change' }],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '计划结束日期',
|
label: '计划结束日期',
|
||||||
@@ -410,7 +410,6 @@ export const option = {
|
|||||||
span: 6,
|
span: 6,
|
||||||
order: 340,
|
order: 340,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
rules: [{ required: true, message: '请选择计划结束日期', trigger: 'change' }],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '数据来源',
|
label: '数据来源',
|
||||||
@@ -457,6 +456,9 @@ export const option = {
|
|||||||
showWordLimit: true,
|
showWordLimit: true,
|
||||||
rules: textRule('备注', 200),
|
rules: textRule('备注', 200),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
...auditColumn('createUserName'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
...auditColumn('updateUserName'),
|
...auditColumn('updateUserName'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -98,6 +98,24 @@ const getProcessConfigNodes = row => {
|
|||||||
|
|
||||||
const isAcceptProcessNode = node => node.key === 'accept' || node.name === '接单';
|
const isAcceptProcessNode = node => node.key === 'accept' || node.name === '接单';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过程节点列显示文案。
|
||||||
|
*
|
||||||
|
* 该列在库里存的是「原始值」,且历史上写入了两种格式:司机打卡推进写的是配置的
|
||||||
|
* key(unload / dispatch / load …),配载单新建默认值写的是中文名(接单)。
|
||||||
|
* 故这里按两种格式都认一次,认不出就原样显示,不把值吞掉。
|
||||||
|
*
|
||||||
|
* 运单创建后、司机首次打卡前该字段为空,属正常——显示「-」,避免留白看不出是没值还是没渲染。
|
||||||
|
*/
|
||||||
|
const formatCurrentProcessNode = row => {
|
||||||
|
const value = String(getFirstValue(row, ['currentProcessNode', 'currentNode']) || '').trim();
|
||||||
|
if (!value) return '-';
|
||||||
|
const node = getProcessConfigNodes(row).find(
|
||||||
|
item => item.key === value || item.name === value
|
||||||
|
);
|
||||||
|
return node?.name || node?.key || value;
|
||||||
|
};
|
||||||
|
|
||||||
const isTruthyFlag = value => {
|
const isTruthyFlag = value => {
|
||||||
if (value === true || value === 1) return true;
|
if (value === true || value === 1) return true;
|
||||||
if (value === false || value === 0 || value === null || value === undefined || value === '') {
|
if (value === false || value === 0 || value === null || value === undefined || value === '') {
|
||||||
@@ -543,7 +561,8 @@ export const option = {
|
|||||||
prop: 'projectName',
|
prop: 'projectName',
|
||||||
formslot: true,
|
formslot: true,
|
||||||
search: true,
|
search: true,
|
||||||
searchLabel: '项目',
|
searchLabel: '项目名称',
|
||||||
|
searchPlaceholder: '请选择或输入',
|
||||||
searchOrder: 22,
|
searchOrder: 22,
|
||||||
span: 6,
|
span: 6,
|
||||||
order: 890,
|
order: 890,
|
||||||
@@ -555,6 +574,7 @@ export const option = {
|
|||||||
prop: 'customerName',
|
prop: 'customerName',
|
||||||
search: true,
|
search: true,
|
||||||
searchLabel: '客户名称',
|
searchLabel: '客户名称',
|
||||||
|
searchPlaceholder: '请选择或输入',
|
||||||
searchOrder: 21,
|
searchOrder: 21,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
@@ -563,10 +583,10 @@ export const option = {
|
|||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '车牌号/航班号/船号/班列号',
|
label: '车牌号/船班号/船次号/班次号',
|
||||||
prop: 'vehicleNo',
|
prop: 'vehicleNo',
|
||||||
search: true,
|
search: true,
|
||||||
searchLabel: '车/船/航班/班列',
|
searchLabel: '车牌号/船班号/船次号/班次号',
|
||||||
searchOrder: 13,
|
searchOrder: 13,
|
||||||
minWidth: 210,
|
minWidth: 210,
|
||||||
display: false,
|
display: false,
|
||||||
@@ -576,6 +596,7 @@ export const option = {
|
|||||||
prop: 'driverName',
|
prop: 'driverName',
|
||||||
search: true,
|
search: true,
|
||||||
searchLabel: '司机名称',
|
searchLabel: '司机名称',
|
||||||
|
searchPlaceholder: '请选择或输入',
|
||||||
searchOrder: 14,
|
searchOrder: 14,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
display: false,
|
display: false,
|
||||||
@@ -605,6 +626,7 @@ export const option = {
|
|||||||
{
|
{
|
||||||
label: '过程节点',
|
label: '过程节点',
|
||||||
prop: 'currentProcessNode',
|
prop: 'currentProcessNode',
|
||||||
|
formatter: row => formatCurrentProcessNode(row),
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
@@ -626,6 +648,7 @@ export const option = {
|
|||||||
prop: 'carrierName',
|
prop: 'carrierName',
|
||||||
search: true,
|
search: true,
|
||||||
searchLabel: '承运商名称',
|
searchLabel: '承运商名称',
|
||||||
|
searchPlaceholder: '请选择或输入',
|
||||||
searchOrder: 15,
|
searchOrder: 15,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
display: false,
|
display: false,
|
||||||
@@ -729,15 +752,30 @@ export const option = {
|
|||||||
prop: 'planName',
|
prop: 'planName',
|
||||||
formslot: true,
|
formslot: true,
|
||||||
search: true,
|
search: true,
|
||||||
|
searchPlaceholder: '请选择或输入',
|
||||||
searchOrder: 7,
|
searchOrder: 7,
|
||||||
span: 6,
|
span: 6,
|
||||||
order: 870,
|
order: 870,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
|
formatter: row => {
|
||||||
|
const value = row?.planName;
|
||||||
|
if (
|
||||||
|
value === undefined ||
|
||||||
|
value === null ||
|
||||||
|
value === '' ||
|
||||||
|
String(value) === '-1' ||
|
||||||
|
Number(value) === -1
|
||||||
|
) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '货物类型',
|
label: '货物类型',
|
||||||
prop: 'cargoType',
|
prop: 'cargoType',
|
||||||
search: true,
|
search: true,
|
||||||
|
searchPlaceholder: '请选择或输入',
|
||||||
searchOrder: 18,
|
searchOrder: 18,
|
||||||
formatter: row => formatGoodsField(row, ['cargoType', 'goodsType', 'typeName']),
|
formatter: row => formatGoodsField(row, ['cargoType', 'goodsType', 'typeName']),
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
@@ -864,6 +902,9 @@ export const option = {
|
|||||||
viewDisplay: false,
|
viewDisplay: false,
|
||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
...auditColumn('createUserName'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
...auditColumn('createTime'),
|
...auditColumn('createTime'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export const generateWaybillColumns = [
|
|||||||
{ label: '运单号', prop: 'waybillNo', minWidth: 160, link: true },
|
{ label: '运单号', prop: 'waybillNo', minWidth: 160, link: true },
|
||||||
{ label: '项目名称', prop: 'projectName', minWidth: 160 },
|
{ label: '项目名称', prop: 'projectName', minWidth: 160 },
|
||||||
{ label: '客户/委托方', prop: 'customerName', minWidth: 160 },
|
{ label: '客户/委托方', prop: 'customerName', minWidth: 160 },
|
||||||
{ label: '车牌号/航班号/船号/班列号', prop: 'vehicleNo', minWidth: 190 },
|
{ label: '车牌号/船班号/船次号/班次号', prop: 'vehicleNo', minWidth: 190 },
|
||||||
{ label: '司机', prop: 'driverName', minWidth: 120 },
|
{ label: '司机', prop: 'driverName', minWidth: 120 },
|
||||||
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
|
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
|
||||||
{ label: '运输类型', prop: 'transportType', minWidth: 130 },
|
{ label: '运输类型', prop: 'transportType', minWidth: 130 },
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const GRANT_TYPE_DIC = [
|
|||||||
{ label: '社交登录', value: 'social' },
|
{ label: '社交登录', value: 'social' },
|
||||||
{ label: '客户端凭证', value: 'client_credentials' },
|
{ label: '客户端凭证', value: 'client_credentials' },
|
||||||
{ label: '刷新令牌', value: 'refresh_token' },
|
{ label: '刷新令牌', value: 'refresh_token' },
|
||||||
|
{ label: '退出登录', value: 'logout' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const authLogOption = {
|
export const authLogOption = {
|
||||||
@@ -38,7 +39,7 @@ export const authLogOption = {
|
|||||||
prop: 'realName',
|
prop: 'realName',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '授权类型',
|
label: '操作类型',
|
||||||
prop: 'grantType',
|
prop: 'grantType',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
search: true,
|
search: true,
|
||||||
@@ -80,7 +81,7 @@ export const authLogOption = {
|
|||||||
width: 120,
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '登录时间',
|
label: '操作时间',
|
||||||
prop: 'loginTime',
|
prop: 'loginTime',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
span: 24,
|
span: 24,
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
const identityDic = [
|
||||||
|
{ label: '司机', value: 'driver' },
|
||||||
|
{ label: '管理员', value: 'admin' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const statusDic = [
|
||||||
|
{ label: '启用', value: 1 },
|
||||||
|
{ label: '停用', value: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const validatePhone = (rule, value, callback) => {
|
||||||
|
const phone = String(value || '').trim();
|
||||||
|
if (!phone) {
|
||||||
|
callback(new Error('请输入手机号'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!/^1\d{10}$/.test(phone)) {
|
||||||
|
callback(new Error('请输入正确的手机号'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateRemark = (rule, value, callback) => {
|
||||||
|
if (String(value || '').length > 200) {
|
||||||
|
callback(new Error('备注不能超过200字'));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
height: 'auto',
|
||||||
|
calcHeight: 22,
|
||||||
|
dialogWidth: 760,
|
||||||
|
labelPosition: 'right',
|
||||||
|
labelWidth: 'auto',
|
||||||
|
tip: false,
|
||||||
|
searchBtnText: '查询',
|
||||||
|
emptyBtnText: '重置',
|
||||||
|
saveBtnText: '提交',
|
||||||
|
updateBtnText: '提交',
|
||||||
|
searchShow: true,
|
||||||
|
searchIcon: true,
|
||||||
|
searchLabelWidth: 160,
|
||||||
|
searchMenuSpan: 24,
|
||||||
|
searchIndex: 4,
|
||||||
|
searchMenuPosition: 'right',
|
||||||
|
border: true,
|
||||||
|
index: true,
|
||||||
|
indexLabel: '序号',
|
||||||
|
indexWidth: 70,
|
||||||
|
viewBtn: false,
|
||||||
|
delBtn: false,
|
||||||
|
editBtn: false,
|
||||||
|
addBtn: false,
|
||||||
|
selection: true,
|
||||||
|
dialogClickModal: false,
|
||||||
|
menuWidth: 320,
|
||||||
|
column: [
|
||||||
|
{
|
||||||
|
label: '手机号',
|
||||||
|
prop: 'phone',
|
||||||
|
minWidth: 130,
|
||||||
|
search: true,
|
||||||
|
searchOrder: 4,
|
||||||
|
searchSpan: 6,
|
||||||
|
searchPlaceholder: '请输入',
|
||||||
|
maxlength: 11,
|
||||||
|
showWordLimit: true,
|
||||||
|
order: 100,
|
||||||
|
rules: [
|
||||||
|
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||||
|
{ validator: validatePhone, trigger: 'blur' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '账号',
|
||||||
|
prop: 'account',
|
||||||
|
minWidth: 130,
|
||||||
|
search: true,
|
||||||
|
searchOrder: 3,
|
||||||
|
searchSpan: 6,
|
||||||
|
searchPlaceholder: '请输入',
|
||||||
|
maxlength: 45,
|
||||||
|
order: 90,
|
||||||
|
placeholder: '默认与手机号相同',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '姓名',
|
||||||
|
prop: 'realName',
|
||||||
|
minWidth: 100,
|
||||||
|
search: true,
|
||||||
|
searchOrder: 2,
|
||||||
|
searchSpan: 6,
|
||||||
|
searchPlaceholder: '请输入',
|
||||||
|
maxlength: 10,
|
||||||
|
showWordLimit: true,
|
||||||
|
order: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '昵称',
|
||||||
|
prop: 'name',
|
||||||
|
minWidth: 100,
|
||||||
|
maxlength: 20,
|
||||||
|
showWordLimit: true,
|
||||||
|
order: 70,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '小程序身份',
|
||||||
|
prop: 'identities',
|
||||||
|
type: 'select',
|
||||||
|
multiple: true,
|
||||||
|
dataType: 'string',
|
||||||
|
dicData: identityDic,
|
||||||
|
minWidth: 140,
|
||||||
|
search: true,
|
||||||
|
searchOrder: 1,
|
||||||
|
searchSpan: 6,
|
||||||
|
searchPlaceholder: '请选择',
|
||||||
|
order: 60,
|
||||||
|
rules: [{ required: true, message: '请选择小程序身份', trigger: 'change' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '性别',
|
||||||
|
prop: 'sex',
|
||||||
|
type: 'select',
|
||||||
|
dicUrl: '/blade-system/dict/dictionary?code=sex',
|
||||||
|
props: {
|
||||||
|
label: 'dictValue',
|
||||||
|
value: 'dictKey',
|
||||||
|
},
|
||||||
|
dataType: 'number',
|
||||||
|
minWidth: 80,
|
||||||
|
hide: true,
|
||||||
|
order: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'openid',
|
||||||
|
prop: 'openid',
|
||||||
|
minWidth: 180,
|
||||||
|
overHidden: true,
|
||||||
|
hide: true,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: true,
|
||||||
|
viewDisplay: true,
|
||||||
|
order: 40,
|
||||||
|
placeholder: '可清空以解绑微信',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '状态',
|
||||||
|
prop: 'status',
|
||||||
|
type: 'select',
|
||||||
|
dicData: statusDic,
|
||||||
|
dataType: 'number',
|
||||||
|
minWidth: 90,
|
||||||
|
search: true,
|
||||||
|
searchOrder: 0,
|
||||||
|
searchSpan: 6,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
value: 1,
|
||||||
|
order: 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '备注',
|
||||||
|
prop: 'remark',
|
||||||
|
type: 'textarea',
|
||||||
|
span: 24,
|
||||||
|
minRows: 2,
|
||||||
|
maxlength: 200,
|
||||||
|
showWordLimit: true,
|
||||||
|
hide: true,
|
||||||
|
order: 20,
|
||||||
|
rules: [{ validator: validateRemark, trigger: 'blur' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创建时间',
|
||||||
|
prop: 'createTime',
|
||||||
|
minWidth: 170,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
viewDisplay: true,
|
||||||
|
order: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '更新人',
|
||||||
|
prop: 'updateUserName',
|
||||||
|
minWidth: 100,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
viewDisplay: true,
|
||||||
|
order: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '更新时间',
|
||||||
|
prop: 'updateTime',
|
||||||
|
minWidth: 170,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
viewDisplay: true,
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
import { getDeptLazyTree } from '@/api/system/dept';
|
import { getDeptLazyTree } from '@/api/system/dept';
|
||||||
|
import { validateLoginPassword } from '@/utils/validate';
|
||||||
|
|
||||||
export const userOption = safe => {
|
export const userOption = safe => {
|
||||||
const validatePass = (rule, value, callback) => {
|
const validatePass = (rule, value, callback) => {
|
||||||
if (value === '') {
|
validateLoginPassword(rule, value, callback);
|
||||||
callback(new Error('请输入密码'));
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const validatePass2 = (rule, value, callback) => {
|
const validatePass2 = (rule, value, callback) => {
|
||||||
if (value === '') {
|
if (value === '') {
|
||||||
@@ -30,7 +27,8 @@ export const userOption = safe => {
|
|||||||
index: true,
|
index: true,
|
||||||
selection: true,
|
selection: true,
|
||||||
addBtn: false,
|
addBtn: false,
|
||||||
viewBtn: true,
|
viewBtn: false,
|
||||||
|
menuWidth: 160,
|
||||||
dialogType: 'dialog',
|
dialogType: 'dialog',
|
||||||
dialogWidth: '90%',
|
dialogWidth: '90%',
|
||||||
labelPosition: 'right',
|
labelPosition: 'right',
|
||||||
@@ -40,8 +38,11 @@ export const userOption = safe => {
|
|||||||
{
|
{
|
||||||
label: '账号名',
|
label: '账号名',
|
||||||
prop: 'account',
|
prop: 'account',
|
||||||
|
slot: true,
|
||||||
|
minWidth: 180,
|
||||||
search: true,
|
search: true,
|
||||||
searchOrder: 2,
|
searchOrder: 2,
|
||||||
|
searchPlaceholder: '支持模糊搜索',
|
||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -49,6 +50,7 @@ export const userOption = safe => {
|
|||||||
prop: 'realName',
|
prop: 'realName',
|
||||||
search: true,
|
search: true,
|
||||||
searchOrder: 4,
|
searchOrder: 4,
|
||||||
|
searchPlaceholder: '支持模糊搜索',
|
||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -56,6 +58,7 @@ export const userOption = safe => {
|
|||||||
prop: 'phone',
|
prop: 'phone',
|
||||||
search: true,
|
search: true,
|
||||||
searchOrder: 3,
|
searchOrder: 3,
|
||||||
|
searchPlaceholder: '支持模糊搜索',
|
||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
{ label: '工号', prop: 'code', display: false },
|
{ label: '工号', prop: 'code', display: false },
|
||||||
@@ -63,7 +66,8 @@ export const userOption = safe => {
|
|||||||
label: '所属部门',
|
label: '所属部门',
|
||||||
prop: 'deptName',
|
prop: 'deptName',
|
||||||
slot: true,
|
slot: true,
|
||||||
overHidden: true,
|
minWidth: 200,
|
||||||
|
overHidden: false,
|
||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
{ label: '岗位', prop: 'postName', overHidden: true, display: false },
|
{ label: '岗位', prop: 'postName', overHidden: true, display: false },
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
// 一级 value 与二级 value 都用中文原文:库里 exception_type 存的是大类中文名、
|
||||||
|
// exception_reason 存子类中文名(小程序上报即如此),此处必须与之一致,
|
||||||
|
// 否则按异常类型做精确匹配的查询会一行都查不到。
|
||||||
export const exceptionCategoryOptions = [
|
export const exceptionCategoryOptions = [
|
||||||
{
|
{
|
||||||
label: '交通异常',
|
label: '交通异常',
|
||||||
value: 'traffic',
|
value: '交通异常',
|
||||||
children: [
|
children: [
|
||||||
{ label: '道路拥堵', value: '道路拥堵' },
|
{ label: '道路拥堵', value: '道路拥堵' },
|
||||||
{ label: '交通事故', value: '交通事故' },
|
{ label: '交通事故', value: '交通事故' },
|
||||||
@@ -12,7 +15,7 @@ export const exceptionCategoryOptions = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '车辆异常',
|
label: '车辆异常',
|
||||||
value: 'vehicle',
|
value: '车辆异常',
|
||||||
children: [
|
children: [
|
||||||
{ label: '车辆故障维修', value: '车辆故障维修' },
|
{ label: '车辆故障维修', value: '车辆故障维修' },
|
||||||
{ label: '车辆事故', value: '车辆事故' },
|
{ label: '车辆事故', value: '车辆事故' },
|
||||||
@@ -24,7 +27,7 @@ export const exceptionCategoryOptions = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '货物异常',
|
label: '货物异常',
|
||||||
value: 'cargo',
|
value: '货物异常',
|
||||||
children: [
|
children: [
|
||||||
{ label: '货损', value: '货损' },
|
{ label: '货损', value: '货损' },
|
||||||
{ label: '货差', value: '货差' },
|
{ label: '货差', value: '货差' },
|
||||||
@@ -35,7 +38,7 @@ export const exceptionCategoryOptions = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '其他异常',
|
label: '其他异常',
|
||||||
value: 'other',
|
value: '其他异常',
|
||||||
children: [
|
children: [
|
||||||
{ label: '变更路线地址', value: '变更路线地址' },
|
{ label: '变更路线地址', value: '变更路线地址' },
|
||||||
{ label: '其他', value: '其他异常-其他' },
|
{ label: '其他', value: '其他异常-其他' },
|
||||||
@@ -43,6 +46,16 @@ export const exceptionCategoryOptions = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** 一级类型的中文名列表,供编辑表单的级联选择使用 */
|
||||||
|
export const exceptionTypeOptions = exceptionCategoryOptions.map(item => ({
|
||||||
|
label: item.label,
|
||||||
|
value: item.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
/** 按一级类型取二级选项 */
|
||||||
|
export const exceptionReasonOptions = typeValue =>
|
||||||
|
exceptionCategoryOptions.find(item => item.value === typeValue)?.children || [];
|
||||||
|
|
||||||
export const disposalStatusOptions = [
|
export const disposalStatusOptions = [
|
||||||
{ label: '待处理', value: 'pending' },
|
{ label: '待处理', value: 'pending' },
|
||||||
{ label: '处理中', value: 'processing' },
|
{ label: '处理中', value: 'processing' },
|
||||||
@@ -65,20 +78,30 @@ export const tableColumns = [
|
|||||||
{ label: '项目', prop: 'projectName', minWidth: 160 },
|
{ label: '项目', prop: 'projectName', minWidth: 160 },
|
||||||
{ label: '车辆', prop: 'vehicleNo', minWidth: 150, link: true },
|
{ label: '车辆', prop: 'vehicleNo', minWidth: 150, link: true },
|
||||||
{ label: '上报人', prop: 'reporterName', minWidth: 120 },
|
{ label: '上报人', prop: 'reporterName', minWidth: 120 },
|
||||||
|
{ label: '上报时间', prop: 'reportTime', minWidth: 170 },
|
||||||
{ label: '异常类型', prop: 'exceptionTypeName', minWidth: 160 },
|
{ label: '异常类型', prop: 'exceptionTypeName', minWidth: 160 },
|
||||||
{ label: '上报说明', prop: 'reportDescription', minWidth: 180 },
|
{ label: '上报说明', prop: 'reportDescription', minWidth: 180 },
|
||||||
|
{ label: '异常节点', prop: 'nodeName', minWidth: 120 },
|
||||||
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
|
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
|
||||||
{ label: '最新跟进说明', prop: 'latestFollowContent', minWidth: 200 },
|
{ label: '最新跟进说明', prop: 'latestFollowContent', minWidth: 200 },
|
||||||
{ label: '状态', prop: 'disposalStatusName', minWidth: 120 },
|
{ label: '状态', prop: 'disposalStatusName', minWidth: 120 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const detailFields = [
|
// 详情/跟进弹窗的只读信息,按来源分两块:
|
||||||
{ label: '配载单号/总单号', prop: 'loadingOrMasterNo', link: true },
|
// 「运单信息」是上报时从运单复制的字段,「异常信息」是司机上报的内容。
|
||||||
{ label: '运单号', prop: 'waybillNo', link: true },
|
// 与编辑弹窗的分区口径一致,改动时两处要一起改。
|
||||||
{ label: '项目', prop: 'projectName', link: true },
|
export const detailWaybillFields = [
|
||||||
{ label: '车牌号', prop: 'vehicleNo', link: true },
|
{ label: '运单号', prop: 'waybillNo' },
|
||||||
{ label: '上报人', prop: 'reporterName' },
|
{ label: '配载单号/总单号', prop: 'loadingOrMasterNo' },
|
||||||
|
{ label: '车牌号', prop: 'vehicleNo' },
|
||||||
|
{ label: '项目', prop: 'projectName' },
|
||||||
{ label: '承运商', prop: 'carrierName' },
|
{ label: '承运商', prop: 'carrierName' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const detailExceptionFields = [
|
||||||
|
{ label: '上报人', prop: 'reporterName' },
|
||||||
|
{ label: '上报时间', prop: 'reportTime' },
|
||||||
|
{ label: '异常节点', prop: 'nodeName' },
|
||||||
{ label: '异常类型', prop: 'exceptionTypeName' },
|
{ label: '异常类型', prop: 'exceptionTypeName' },
|
||||||
{ label: '异常原因', prop: 'exceptionReason' },
|
{ label: '异常原因', prop: 'exceptionReason' },
|
||||||
{ label: '上报说明', prop: 'reportDescription', span: 2 },
|
{ label: '上报说明', prop: 'reportDescription', span: 2 },
|
||||||
@@ -89,3 +112,21 @@ export const followColumns = [
|
|||||||
{ label: '跟进人', prop: 'followUserName', minWidth: 130 },
|
{ label: '跟进人', prop: 'followUserName', minWidth: 130 },
|
||||||
{ label: '跟进时间', prop: 'followTime', minWidth: 170 },
|
{ label: '跟进时间', prop: 'followTime', minWidth: 170 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 编辑表单只开放上报四件套。车牌、项目、承运商、配载单号由上报时从运单回填,
|
||||||
|
// 允许手改会让记录与运单脱钩;处置状态由跟进与完成流转,不在此处。
|
||||||
|
export const editFormFields = [
|
||||||
|
{ label: '异常类型', prop: 'exceptionType', type: 'select', required: true },
|
||||||
|
{ label: '异常原因', prop: 'exceptionReason', type: 'select' },
|
||||||
|
{ label: '上报说明', prop: 'reportDescription', type: 'textarea', required: true, span: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 编辑弹窗顶部的运单信息,只读展示,帮助运管员确认改的是哪一单。
|
||||||
|
// 上报人与上报时间属上报行为,放在「异常信息」里,不在此处重复。
|
||||||
|
export const editWaybillFields = [
|
||||||
|
{ label: '运单号', prop: 'waybillNo' },
|
||||||
|
{ label: '配载单号/总单号', prop: 'loadingOrMasterNo' },
|
||||||
|
{ label: '车牌号', prop: 'vehicleNo' },
|
||||||
|
{ label: '项目', prop: 'projectName' },
|
||||||
|
{ label: '承运商', prop: 'carrierName' },
|
||||||
|
];
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
export const option = {
|
||||||
|
height: 'auto',
|
||||||
|
calcHeight: 32,
|
||||||
|
tip: false,
|
||||||
|
searchShow: true,
|
||||||
|
searchMenuSpan: 24,
|
||||||
|
searchIcon: true,
|
||||||
|
searchIndex: 4,
|
||||||
|
searchMenuPosition: 'right',
|
||||||
|
border: true,
|
||||||
|
index: true,
|
||||||
|
indexLabel: '序号',
|
||||||
|
indexWidth: 70,
|
||||||
|
selection: false,
|
||||||
|
addBtn: false,
|
||||||
|
editBtn: false,
|
||||||
|
delBtn: false,
|
||||||
|
viewBtn: false,
|
||||||
|
columnBtn: false,
|
||||||
|
menuWidth: 110,
|
||||||
|
column: [
|
||||||
|
{
|
||||||
|
label: '司机姓名',
|
||||||
|
prop: 'driverName',
|
||||||
|
search: true,
|
||||||
|
searchOrder: 3,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '司机手机号',
|
||||||
|
prop: 'driverMobile',
|
||||||
|
search: true,
|
||||||
|
searchOrder: 2,
|
||||||
|
minWidth: 130,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '审核状态',
|
||||||
|
prop: 'statusText',
|
||||||
|
slot: true,
|
||||||
|
width: 110,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '提交时间',
|
||||||
|
prop: 'submitTime',
|
||||||
|
slot: true,
|
||||||
|
minWidth: 170,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
export const option = {
|
||||||
|
height: 'auto',
|
||||||
|
calcHeight: 32,
|
||||||
|
tip: false,
|
||||||
|
searchShow: true,
|
||||||
|
searchMenuSpan: 24,
|
||||||
|
searchIcon: true,
|
||||||
|
searchIndex: 4,
|
||||||
|
searchMenuPosition: 'right',
|
||||||
|
border: true,
|
||||||
|
index: true,
|
||||||
|
indexLabel: '序号',
|
||||||
|
indexWidth: 70,
|
||||||
|
selection: false,
|
||||||
|
addBtn: false,
|
||||||
|
editBtn: false,
|
||||||
|
delBtn: false,
|
||||||
|
viewBtn: false,
|
||||||
|
columnBtn: false,
|
||||||
|
menuWidth: 110,
|
||||||
|
column: [
|
||||||
|
{
|
||||||
|
label: '车牌号',
|
||||||
|
prop: 'bizKey',
|
||||||
|
search: true,
|
||||||
|
searchOrder: 3,
|
||||||
|
minWidth: 140,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '司机姓名',
|
||||||
|
prop: 'driverName',
|
||||||
|
search: true,
|
||||||
|
searchOrder: 2,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '司机手机号',
|
||||||
|
prop: 'driverMobile',
|
||||||
|
search: true,
|
||||||
|
searchOrder: 1,
|
||||||
|
minWidth: 130,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '审核状态',
|
||||||
|
prop: 'statusText',
|
||||||
|
slot: true,
|
||||||
|
width: 110,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '提交时间',
|
||||||
|
prop: 'submitTime',
|
||||||
|
slot: true,
|
||||||
|
minWidth: 170,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -125,6 +125,23 @@ export const option = {
|
|||||||
slot: true,
|
slot: true,
|
||||||
minWidth: 160,
|
minWidth: 160,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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: '准驾车型',
|
label: '准驾车型',
|
||||||
prop: 'drivingType',
|
prop: 'drivingType',
|
||||||
|
|||||||
@@ -91,6 +91,23 @@ export const option = {
|
|||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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: '',
|
label: '',
|
||||||
prop: 'expireStatus',
|
prop: 'expireStatus',
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ export const option = {
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// 这里是「最新一次提交的状态」缓存,不等于该车当前是否已认证:
|
||||||
|
// 已认证由生效快照指针决定,曾审核通过即永久保持,重新提交不会让它退回审核中
|
||||||
label: '认证状态',
|
label: '认证状态',
|
||||||
prop: 'certificationStatus',
|
prop: 'certificationStatus',
|
||||||
slot: true,
|
slot: true,
|
||||||
@@ -104,9 +106,10 @@ export const option = {
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
dicData: [
|
dicData: [
|
||||||
{ label: '全部', value: '' },
|
{ label: '全部', value: '' },
|
||||||
{ label: '认证中', value: 0 },
|
{ label: '审核中', value: 0 },
|
||||||
{ label: '已认证', value: 1 },
|
{ label: '已通过', value: 1 },
|
||||||
{ label: '已驳回', value: 2 },
|
{ label: '已驳回', value: 2 },
|
||||||
|
{ label: '已作废', value: 3 },
|
||||||
],
|
],
|
||||||
minWidth: 110,
|
minWidth: 110,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
@@ -151,6 +154,23 @@ export const option = {
|
|||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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: '',
|
label: '',
|
||||||
prop: 'expireStatus',
|
prop: 'expireStatus',
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ export const option = {
|
|||||||
height: 'auto',
|
height: 'auto',
|
||||||
calcHeight: 32,
|
calcHeight: 32,
|
||||||
dialogWidth: 1200,
|
dialogWidth: 1200,
|
||||||
|
// 弹窗根节点 class:avue-crud 弹窗默认 append-to-body 到 body,
|
||||||
|
// 页面内 scoped 样式命中不到弹窗内容,故用该 class 在全局样式里做兜底(见 styles/element-ui.scss)
|
||||||
|
dialogCustomClass: 'vehicle-dispatch-dialog',
|
||||||
labelPosition: 'right',
|
labelPosition: 'right',
|
||||||
labelWidth: 'auto',
|
labelWidth: 'auto',
|
||||||
tip: false,
|
tip: false,
|
||||||
@@ -101,6 +104,7 @@ export const option = {
|
|||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
|
viewDisplay: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '当前节点',
|
label: '当前节点',
|
||||||
@@ -108,6 +112,7 @@ export const option = {
|
|||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
|
viewDisplay: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '当前处理人',
|
label: '当前处理人',
|
||||||
@@ -115,6 +120,7 @@ export const option = {
|
|||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
|
viewDisplay: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '创建人',
|
label: '创建人',
|
||||||
@@ -122,6 +128,7 @@ export const option = {
|
|||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
|
viewDisplay: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
@@ -132,6 +139,7 @@ export const option = {
|
|||||||
minWidth: 170,
|
minWidth: 170,
|
||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
|
viewDisplay: false,
|
||||||
},
|
},
|
||||||
// ========== 新增/编辑表单专用(不进表格)==========
|
// ========== 新增/编辑表单专用(不进表格)==========
|
||||||
{
|
{
|
||||||
@@ -165,6 +173,7 @@ export const option = {
|
|||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
hide: true,
|
hide: true,
|
||||||
span: 24,
|
span: 24,
|
||||||
|
minRows: 2,
|
||||||
maxlength: 200,
|
maxlength: 200,
|
||||||
showWordLimit: true,
|
showWordLimit: true,
|
||||||
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
|
|
||||||
const validateNonNegative = (rule, value, callback) => {
|
const validateNonNegative = (rule, value, callback) => {
|
||||||
if (value === undefined || value === null || value === '') {
|
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||||
callback();
|
callback();
|
||||||
} else if (Number(value) < 0) {
|
} else if (Number(value) < 0) {
|
||||||
callback(new Error('不能小于 0'));
|
callback(new Error('不能小于 0'));
|
||||||
@@ -10,6 +10,9 @@ const validateNonNegative = (rule, value, callback) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatEmptySentinel = (row, value) =>
|
||||||
|
value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value;
|
||||||
|
|
||||||
export const vehicleTypeDic = [
|
export const vehicleTypeDic = [
|
||||||
{ label: '车辆', value: '车辆' },
|
{ label: '车辆', value: '车辆' },
|
||||||
{ label: '船舶', value: '船舶' },
|
{ label: '船舶', value: '船舶' },
|
||||||
@@ -135,6 +138,7 @@ export const option = {
|
|||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
span: 12,
|
span: 12,
|
||||||
placeholder: '请输入',
|
placeholder: '请输入',
|
||||||
|
formatter: formatEmptySentinel,
|
||||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -146,6 +150,7 @@ export const option = {
|
|||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
span: 12,
|
span: 12,
|
||||||
placeholder: '请输入',
|
placeholder: '请输入',
|
||||||
|
formatter: formatEmptySentinel,
|
||||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -204,6 +209,14 @@ export const option = {
|
|||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
viewDisplay: false,
|
viewDisplay: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
display: false,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
prop: 'createTime',
|
prop: 'createTime',
|
||||||
@@ -281,6 +294,8 @@ export const exportColumns = [
|
|||||||
{ prop: 'insuranceClaimAmount', label: '保险理赔金额' },
|
{ prop: 'insuranceClaimAmount', label: '保险理赔金额' },
|
||||||
{ prop: 'accidentReasonDamage', label: '事故原因及损坏情况' },
|
{ prop: 'accidentReasonDamage', label: '事故原因及损坏情况' },
|
||||||
{ prop: 'remark', label: '备注' },
|
{ prop: 'remark', label: '备注' },
|
||||||
|
{ prop: 'createUserName', label: '创建人' },
|
||||||
|
{ prop: 'createTime', label: '创建时间' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
|
|
||||||
const validateNonNegative = (rule, value, callback) => {
|
const validateNonNegative = (rule, value, callback) => {
|
||||||
if (value === undefined || value === null || value === '') {
|
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||||
callback();
|
callback();
|
||||||
} else if (Number(value) < 0) {
|
} else if (Number(value) < 0) {
|
||||||
callback(new Error('不能小于 0'));
|
callback(new Error('不能小于 0'));
|
||||||
@@ -10,6 +10,9 @@ const validateNonNegative = (rule, value, callback) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatEmptySentinel = (row, value) =>
|
||||||
|
value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value;
|
||||||
|
|
||||||
export const vehicleTypeDic = [
|
export const vehicleTypeDic = [
|
||||||
{ label: '车辆', value: '车辆' },
|
{ label: '车辆', value: '车辆' },
|
||||||
{ label: '船舶', value: '船舶' },
|
{ label: '船舶', value: '船舶' },
|
||||||
@@ -159,6 +162,7 @@ export const option = {
|
|||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
span: 12,
|
span: 12,
|
||||||
placeholder: '请输入',
|
placeholder: '请输入',
|
||||||
|
formatter: formatEmptySentinel,
|
||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: '请输入费用', trigger: 'blur' },
|
{ required: true, message: '请输入费用', trigger: 'blur' },
|
||||||
{ validator: validateNonNegative, trigger: 'blur' },
|
{ validator: validateNonNegative, trigger: 'blur' },
|
||||||
@@ -206,6 +210,14 @@ export const option = {
|
|||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
viewDisplay: false,
|
viewDisplay: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
display: false,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
prop: 'createTime',
|
prop: 'createTime',
|
||||||
@@ -282,6 +294,8 @@ export const exportColumns = [
|
|||||||
{ prop: 'fee', label: '费用(元)' },
|
{ prop: 'fee', label: '费用(元)' },
|
||||||
{ prop: 'assessmentUnit', label: '评定(复核)单位' },
|
{ prop: 'assessmentUnit', label: '评定(复核)单位' },
|
||||||
{ prop: 'remark', label: '备注' },
|
{ prop: 'remark', label: '备注' },
|
||||||
|
{ prop: 'createUserName', label: '创建人' },
|
||||||
|
{ prop: 'createTime', label: '创建时间' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
|
|||||||
@@ -147,6 +147,15 @@ export const option = {
|
|||||||
viewDisplay: false,
|
viewDisplay: false,
|
||||||
minWidth: 170,
|
minWidth: 170,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
display: false,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
viewDisplay: false,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
prop: 'createTime',
|
prop: 'createTime',
|
||||||
@@ -224,6 +233,8 @@ export const exportColumns = [
|
|||||||
{ prop: 'specificationModel', label: '规格型号' },
|
{ prop: 'specificationModel', label: '规格型号' },
|
||||||
{ prop: 'remark', label: '备注' },
|
{ prop: 'remark', label: '备注' },
|
||||||
{ prop: 'originalEquipmentNo', label: '原厂设备号' },
|
{ prop: 'originalEquipmentNo', label: '原厂设备号' },
|
||||||
|
{ prop: 'createUserName', label: '创建人' },
|
||||||
|
{ prop: 'createTime', label: '创建时间' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
|
|||||||
@@ -172,6 +172,14 @@ export const option = {
|
|||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
viewDisplay: false,
|
viewDisplay: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
display: false,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
prop: 'createTime',
|
prop: 'createTime',
|
||||||
@@ -218,6 +226,8 @@ export const exportColumns = [
|
|||||||
{ prop: 'exitStation', label: '出口站' },
|
{ prop: 'exitStation', label: '出口站' },
|
||||||
{ prop: 'dataSource', label: '数据来源' },
|
{ prop: 'dataSource', label: '数据来源' },
|
||||||
{ prop: 'remark', label: '备注' },
|
{ prop: 'remark', label: '备注' },
|
||||||
|
{ prop: 'createUserName', label: '创建人' },
|
||||||
|
{ prop: 'createTime', label: '创建时间' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
|
|
||||||
const validateNonNegative = (rule, value, callback) => {
|
const validateNonNegative = (rule, value, callback) => {
|
||||||
if (value === undefined || value === null || value === '') {
|
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||||
callback();
|
callback();
|
||||||
} else if (Number(value) < 0) {
|
} else if (Number(value) < 0) {
|
||||||
callback(new Error('不能小于 0'));
|
callback(new Error('不能小于 0'));
|
||||||
@@ -141,6 +141,7 @@ export const option = {
|
|||||||
formslot: true,
|
formslot: true,
|
||||||
append: '元',
|
append: '元',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
|
formatter: (row, value) => (value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value),
|
||||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -150,6 +151,7 @@ export const option = {
|
|||||||
formslot: true,
|
formslot: true,
|
||||||
append: '元',
|
append: '元',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
|
formatter: (row, value) => (value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value),
|
||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: '请输入保费', trigger: 'blur' },
|
{ required: true, message: '请输入保费', trigger: 'blur' },
|
||||||
{ validator: validateNonNegative, trigger: 'blur' },
|
{ validator: validateNonNegative, trigger: 'blur' },
|
||||||
@@ -159,6 +161,7 @@ export const option = {
|
|||||||
label: '发票号',
|
label: '发票号',
|
||||||
prop: 'invoiceNo',
|
prop: 'invoiceNo',
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
|
formatter: (row, value) => (value === undefined || value === null || value === '' || String(value) === '-1' ? '' : value),
|
||||||
rules: [{ max: 80, message: '最多 80 个字符', trigger: 'blur' }],
|
rules: [{ max: 80, message: '最多 80 个字符', trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -168,6 +171,7 @@ export const option = {
|
|||||||
format: 'YYYY-MM-DD',
|
format: 'YYYY-MM-DD',
|
||||||
valueFormat: 'YYYY-MM-DD',
|
valueFormat: 'YYYY-MM-DD',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
|
formatter: (row, value) => (value === undefined || value === null || value === '' || String(value) === '-1' ? '' : value),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '备注',
|
label: '备注',
|
||||||
@@ -178,6 +182,14 @@ export const option = {
|
|||||||
hide: true,
|
hide: true,
|
||||||
rules: [{ max: 200, message: '备注最多 200 个字', trigger: 'blur' }],
|
rules: [{ max: 200, message: '备注最多 200 个字', trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
display: false,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
prop: 'createTime',
|
prop: 'createTime',
|
||||||
@@ -256,6 +268,8 @@ export const exportColumns = [
|
|||||||
{ prop: 'invoiceNo', label: '发票号' },
|
{ prop: 'invoiceNo', label: '发票号' },
|
||||||
{ prop: 'invoiceDate', label: '开票日期' },
|
{ prop: 'invoiceDate', label: '开票日期' },
|
||||||
{ prop: 'remark', label: '备注' },
|
{ prop: 'remark', label: '备注' },
|
||||||
|
{ prop: 'createUserName', label: '创建人' },
|
||||||
|
{ prop: 'createTime', label: '创建时间' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
|
|
||||||
const isEmpty = value => value === undefined || value === null || value === '';
|
const isEmpty = value =>
|
||||||
|
value === undefined || value === null || value === '' || Number(value) === -1;
|
||||||
|
|
||||||
const validateNonNegative = (rule, value, callback) => {
|
const validateNonNegative = (rule, value, callback) => {
|
||||||
if (isEmpty(value)) {
|
if (isEmpty(value)) {
|
||||||
@@ -12,6 +13,8 @@ const validateNonNegative = (rule, value, callback) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatEmptySentinel = (row, value) => (isEmpty(value) ? '' : value);
|
||||||
|
|
||||||
export const option = {
|
export const option = {
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
calcHeight: 32,
|
calcHeight: 32,
|
||||||
@@ -60,6 +63,7 @@ export const option = {
|
|||||||
append: 'km',
|
append: 'km',
|
||||||
minWidth: 210,
|
minWidth: 210,
|
||||||
slot: true,
|
slot: true,
|
||||||
|
formatter: formatEmptySentinel,
|
||||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -70,6 +74,7 @@ export const option = {
|
|||||||
append: 'km',
|
append: 'km',
|
||||||
minWidth: 210,
|
minWidth: 210,
|
||||||
slot: true,
|
slot: true,
|
||||||
|
formatter: formatEmptySentinel,
|
||||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -80,6 +85,7 @@ export const option = {
|
|||||||
append: 'km',
|
append: 'km',
|
||||||
minWidth: 170,
|
minWidth: 170,
|
||||||
slot: true,
|
slot: true,
|
||||||
|
formatter: formatEmptySentinel,
|
||||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -90,6 +96,7 @@ export const option = {
|
|||||||
append: 'km',
|
append: 'km',
|
||||||
minWidth: 170,
|
minWidth: 170,
|
||||||
slot: true,
|
slot: true,
|
||||||
|
formatter: formatEmptySentinel,
|
||||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -133,6 +140,14 @@ export const option = {
|
|||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
viewDisplay: false,
|
viewDisplay: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
display: false,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
prop: 'createTime',
|
prop: 'createTime',
|
||||||
@@ -208,6 +223,8 @@ export const exportColumns = [
|
|||||||
{ prop: 'monthlyMileage', label: '本月行驶里程数' },
|
{ prop: 'monthlyMileage', label: '本月行驶里程数' },
|
||||||
{ prop: 'totalMileage', label: '累计行驶里程数' },
|
{ prop: 'totalMileage', label: '累计行驶里程数' },
|
||||||
{ prop: 'remark', label: '备注' },
|
{ prop: 'remark', label: '备注' },
|
||||||
|
{ prop: 'createUserName', label: '创建人' },
|
||||||
|
{ prop: 'createTime', label: '创建时间' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
|
|
||||||
const isEmpty = value => value === undefined || value === null || value === '';
|
const isEmpty = value =>
|
||||||
|
value === undefined || value === null || value === '' || Number(value) === -1;
|
||||||
|
|
||||||
const validateNonNegative = (rule, value, callback) => {
|
const validateNonNegative = (rule, value, callback) => {
|
||||||
if (isEmpty(value)) {
|
if (isEmpty(value)) {
|
||||||
@@ -12,6 +13,8 @@ const validateNonNegative = (rule, value, callback) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatEmptySentinel = (row, value) => (isEmpty(value) ? '' : value);
|
||||||
|
|
||||||
export const vehicleTypeDic = [
|
export const vehicleTypeDic = [
|
||||||
{ label: '车辆', value: '车辆' },
|
{ label: '车辆', value: '车辆' },
|
||||||
{ label: '船舶', value: '船舶' },
|
{ label: '船舶', value: '船舶' },
|
||||||
@@ -124,6 +127,7 @@ export const option = {
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
formslot: true,
|
formslot: true,
|
||||||
minWidth: 110,
|
minWidth: 110,
|
||||||
|
formatter: formatEmptySentinel,
|
||||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -133,6 +137,7 @@ export const option = {
|
|||||||
formslot: true,
|
formslot: true,
|
||||||
append: '元/升',
|
append: '元/升',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
|
formatter: formatEmptySentinel,
|
||||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -141,6 +146,7 @@ export const option = {
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
formslot: true,
|
formslot: true,
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
|
formatter: formatEmptySentinel,
|
||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: '请输入交易金额', trigger: 'blur' },
|
{ required: true, message: '请输入交易金额', trigger: 'blur' },
|
||||||
{ validator: validateNonNegative, trigger: 'blur' },
|
{ validator: validateNonNegative, trigger: 'blur' },
|
||||||
@@ -152,6 +158,7 @@ export const option = {
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
formslot: true,
|
formslot: true,
|
||||||
minWidth: 110,
|
minWidth: 110,
|
||||||
|
formatter: formatEmptySentinel,
|
||||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -225,6 +232,14 @@ export const option = {
|
|||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
viewDisplay: false,
|
viewDisplay: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
display: false,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
prop: 'createTime',
|
prop: 'createTime',
|
||||||
@@ -304,6 +319,8 @@ export const exportColumns = [
|
|||||||
{ prop: 'balance', label: '余额' },
|
{ prop: 'balance', label: '余额' },
|
||||||
{ prop: 'station', label: '站点' },
|
{ prop: 'station', label: '站点' },
|
||||||
{ prop: 'remark', label: '备注' },
|
{ prop: 'remark', label: '备注' },
|
||||||
|
{ prop: 'createUserName', label: '创建人' },
|
||||||
|
{ prop: 'createTime', label: '创建时间' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
|
|||||||
@@ -165,6 +165,14 @@ export const option = {
|
|||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
viewDisplay: false,
|
viewDisplay: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
display: false,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
prop: 'createTime',
|
prop: 'createTime',
|
||||||
@@ -209,6 +217,8 @@ export const exportColumns = [
|
|||||||
{ prop: 'amount', label: '金额' },
|
{ prop: 'amount', label: '金额' },
|
||||||
{ prop: 'dataSource', label: '数据来源' },
|
{ prop: 'dataSource', label: '数据来源' },
|
||||||
{ prop: 'remark', label: '备注' },
|
{ prop: 'remark', label: '备注' },
|
||||||
|
{ prop: 'createUserName', label: '创建人' },
|
||||||
|
{ prop: 'createTime', label: '创建时间' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
|
|
||||||
const validateNonNegative = (rule, value, callback) => {
|
const validateNonNegative = (rule, value, callback) => {
|
||||||
if (value === undefined || value === null || value === '') {
|
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||||
callback();
|
callback();
|
||||||
} else if (Number(value) < 0) {
|
} else if (Number(value) < 0) {
|
||||||
callback(new Error('不能小于 0'));
|
callback(new Error('不能小于 0'));
|
||||||
@@ -11,7 +11,7 @@ const validateNonNegative = (rule, value, callback) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const validatePositiveInteger = (rule, value, callback) => {
|
const validatePositiveInteger = (rule, value, callback) => {
|
||||||
if (value === undefined || value === null || value === '') {
|
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||||
callback();
|
callback();
|
||||||
} else if (!Number.isInteger(Number(value)) || Number(value) <= 0) {
|
} else if (!Number.isInteger(Number(value)) || Number(value) <= 0) {
|
||||||
callback(new Error('换胎数量必须为正整数'));
|
callback(new Error('换胎数量必须为正整数'));
|
||||||
@@ -20,6 +20,9 @@ const validatePositiveInteger = (rule, value, callback) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatEmptySentinel = (row, value) =>
|
||||||
|
value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value;
|
||||||
|
|
||||||
export const option = {
|
export const option = {
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
calcHeight: 32,
|
calcHeight: 32,
|
||||||
@@ -98,6 +101,7 @@ export const option = {
|
|||||||
minWidth: 110,
|
minWidth: 110,
|
||||||
span: 12,
|
span: 12,
|
||||||
placeholder: '请输入',
|
placeholder: '请输入',
|
||||||
|
formatter: formatEmptySentinel,
|
||||||
rules: [{ validator: validatePositiveInteger, trigger: 'blur' }],
|
rules: [{ validator: validatePositiveInteger, trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -108,6 +112,7 @@ export const option = {
|
|||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
span: 12,
|
span: 12,
|
||||||
placeholder: '请输入',
|
placeholder: '请输入',
|
||||||
|
formatter: formatEmptySentinel,
|
||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: '请输入换胎费用', trigger: 'blur' },
|
{ required: true, message: '请输入换胎费用', trigger: 'blur' },
|
||||||
{ validator: validateNonNegative, trigger: 'blur' },
|
{ validator: validateNonNegative, trigger: 'blur' },
|
||||||
@@ -142,6 +147,14 @@ export const option = {
|
|||||||
slot: true,
|
slot: true,
|
||||||
formslot: true,
|
formslot: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
display: false,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
prop: 'createTime',
|
prop: 'createTime',
|
||||||
@@ -217,6 +230,8 @@ export const exportColumns = [
|
|||||||
{ prop: 'replacementCost', label: '换胎费用' },
|
{ prop: 'replacementCost', label: '换胎费用' },
|
||||||
{ prop: 'replacementDescription', label: '换胎说明' },
|
{ prop: 'replacementDescription', label: '换胎说明' },
|
||||||
{ prop: 'remark', label: '备注' },
|
{ prop: 'remark', label: '备注' },
|
||||||
|
{ prop: 'createUserName', label: '创建人' },
|
||||||
|
{ prop: 'createTime', label: '创建时间' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
|
|||||||
@@ -97,6 +97,14 @@ export const option = {
|
|||||||
slot: true,
|
slot: true,
|
||||||
formslot: true,
|
formslot: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
display: false,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
prop: 'createTime',
|
prop: 'createTime',
|
||||||
@@ -169,6 +177,8 @@ export const exportColumns = [
|
|||||||
{ prop: 'changeItem', label: '变更事项' },
|
{ prop: 'changeItem', label: '变更事项' },
|
||||||
{ prop: 'changeContent', label: '变更内容' },
|
{ prop: 'changeContent', label: '变更内容' },
|
||||||
{ prop: 'remark', label: '备注' },
|
{ prop: 'remark', label: '备注' },
|
||||||
|
{ prop: 'createUserName', label: '创建人' },
|
||||||
|
{ prop: 'createTime', label: '创建时间' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
|
|
||||||
const validateNonNegative = (rule, value, callback) => {
|
const validateNonNegative = (rule, value, callback) => {
|
||||||
if (value === undefined || value === null || value === '') {
|
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||||
callback();
|
callback();
|
||||||
} else if (Number(value) < 0) {
|
} else if (Number(value) < 0) {
|
||||||
callback(new Error('不能小于 0'));
|
callback(new Error('不能小于 0'));
|
||||||
@@ -11,7 +11,7 @@ const validateNonNegative = (rule, value, callback) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const validateDeductPoints = (rule, value, callback) => {
|
const validateDeductPoints = (rule, value, callback) => {
|
||||||
if (value === undefined || value === null || value === '') {
|
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||||
callback();
|
callback();
|
||||||
} else if (!Number.isInteger(Number(value)) || Number(value) < 0 || Number(value) > 15) {
|
} else if (!Number.isInteger(Number(value)) || Number(value) < 0 || Number(value) > 15) {
|
||||||
callback(new Error('被扣分数范围为 0-15 分'));
|
callback(new Error('被扣分数范围为 0-15 分'));
|
||||||
@@ -20,6 +20,9 @@ const validateDeductPoints = (rule, value, callback) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatEmptySentinel = (row, value) =>
|
||||||
|
value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value;
|
||||||
|
|
||||||
export const vehicleTypeDic = [
|
export const vehicleTypeDic = [
|
||||||
{ label: '车辆', value: '车辆' },
|
{ label: '车辆', value: '车辆' },
|
||||||
{ label: '船舶', value: '船舶' },
|
{ label: '船舶', value: '船舶' },
|
||||||
@@ -150,6 +153,7 @@ export const option = {
|
|||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
span: 12,
|
span: 12,
|
||||||
placeholder: '请输入',
|
placeholder: '请输入',
|
||||||
|
formatter: formatEmptySentinel,
|
||||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -160,6 +164,7 @@ export const option = {
|
|||||||
minWidth: 110,
|
minWidth: 110,
|
||||||
span: 12,
|
span: 12,
|
||||||
placeholder: '请输入',
|
placeholder: '请输入',
|
||||||
|
formatter: formatEmptySentinel,
|
||||||
rules: [{ validator: validateDeductPoints, trigger: 'blur' }],
|
rules: [{ validator: validateDeductPoints, trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -216,6 +221,14 @@ export const option = {
|
|||||||
slot: true,
|
slot: true,
|
||||||
formslot: true,
|
formslot: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
display: false,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
prop: 'createTime',
|
prop: 'createTime',
|
||||||
@@ -294,6 +307,8 @@ export const exportColumns = [
|
|||||||
{ prop: 'processStatus', label: '状态' },
|
{ prop: 'processStatus', label: '状态' },
|
||||||
{ prop: 'processDescription', label: '过程描述' },
|
{ prop: 'processDescription', label: '过程描述' },
|
||||||
{ prop: 'processResult', label: '处理结果' },
|
{ prop: 'processResult', label: '处理结果' },
|
||||||
|
{ prop: 'createUserName', label: '创建人' },
|
||||||
|
{ prop: 'createTime', label: '创建时间' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const excelOption = {
|
export const excelOption = {
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loginForm: {
|
loginForm: {
|
||||||
username: 'admin',
|
username: '',
|
||||||
password: '123456',
|
password: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
-->
|
-->
|
||||||
<userLogin v-if="activeName === 'user'"></userLogin>
|
<userLogin v-if="activeName === 'user'"></userLogin>
|
||||||
<registerLogin v-else-if="activeName === 'register'"></registerLogin>
|
<registerLogin v-else-if="activeName === 'register'"></registerLogin>
|
||||||
|
<!-- 暂时默认账号密码登录,IAM 选择入口先隐藏
|
||||||
<div v-else class="iam-login">
|
<div v-else class="iam-login">
|
||||||
<el-button type="primary" class="login-submit" @click.prevent="handleIamLogin">
|
<el-button type="primary" class="login-submit" @click.prevent="handleIamLogin">
|
||||||
IAM统一身份认证
|
IAM统一身份认证
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
账号密码登录
|
账号密码登录
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +63,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
website: website,
|
website: website,
|
||||||
time: '',
|
time: '',
|
||||||
activeName: 'iam',
|
activeName: 'user',
|
||||||
socialForm: {
|
socialForm: {
|
||||||
tenantId: '000000',
|
tenantId: '000000',
|
||||||
source: '',
|
source: '',
|
||||||
|
|||||||
@@ -93,9 +93,9 @@ export default {
|
|||||||
//角色ID
|
//角色ID
|
||||||
roleId: '',
|
roleId: '',
|
||||||
//用户名
|
//用户名
|
||||||
username: 'admin',
|
username: '',
|
||||||
//密码
|
//密码
|
||||||
password: 'admin',
|
password: '',
|
||||||
//账号类型
|
//账号类型
|
||||||
type: 'account',
|
type: 'account',
|
||||||
//验证码的值
|
//验证码的值
|
||||||
|
|||||||
+38
-16
@@ -87,7 +87,7 @@ export default [
|
|||||||
{
|
{
|
||||||
path: '/business/project-apply/public-view',
|
path: '/business/project-apply/public-view',
|
||||||
name: '查看项目信息',
|
name: '查看项目信息',
|
||||||
component: () => import('@/views/mk/public-biz-view.vue'),
|
component: () => import('@/views/business/project-apply-public-view.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
keepAlive: false,
|
keepAlive: false,
|
||||||
isTab: false,
|
isTab: false,
|
||||||
@@ -98,7 +98,7 @@ export default [
|
|||||||
{
|
{
|
||||||
path: '/business/contract-manage/public-view',
|
path: '/business/contract-manage/public-view',
|
||||||
name: '查看合同信息',
|
name: '查看合同信息',
|
||||||
component: () => import('@/views/mk/public-biz-view.vue'),
|
component: () => import('@/views/business/contract-manage-public-view.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
keepAlive: false,
|
keepAlive: false,
|
||||||
isTab: false,
|
isTab: false,
|
||||||
@@ -106,21 +106,10 @@ export default [
|
|||||||
bizType: 'contract-manage',
|
bizType: 'contract-manage',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/business/waybill-manage/public-view',
|
|
||||||
name: '查看运单信息',
|
|
||||||
component: () => import('@/views/mk/public-biz-view.vue'),
|
|
||||||
meta: {
|
|
||||||
keepAlive: false,
|
|
||||||
isTab: false,
|
|
||||||
isAuth: false,
|
|
||||||
bizType: 'waybill-manage',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/settlement/pre-settlement/public-view',
|
path: '/settlement/pre-settlement/public-view',
|
||||||
name: '查看预结算信息',
|
name: '查看预结算信息',
|
||||||
component: () => import('@/views/mk/public-biz-view.vue'),
|
component: () => import('@/views/settlement/pre-settlement-public-view.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
keepAlive: false,
|
keepAlive: false,
|
||||||
isTab: false,
|
isTab: false,
|
||||||
@@ -131,7 +120,7 @@ export default [
|
|||||||
{
|
{
|
||||||
path: '/settlement/formal-settlement/public-view',
|
path: '/settlement/formal-settlement/public-view',
|
||||||
name: '查看正式结算信息',
|
name: '查看正式结算信息',
|
||||||
component: () => import('@/views/mk/public-biz-view.vue'),
|
component: () => import('@/views/settlement/formal-settlement-public-view.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
keepAlive: false,
|
keepAlive: false,
|
||||||
isTab: false,
|
isTab: false,
|
||||||
@@ -142,7 +131,7 @@ export default [
|
|||||||
{
|
{
|
||||||
path: '/payment/payment-application/public-view',
|
path: '/payment/payment-application/public-view',
|
||||||
name: '查看付款申请信息',
|
name: '查看付款申请信息',
|
||||||
component: () => import('@/views/mk/public-biz-view.vue'),
|
component: () => import('@/views/payment/payment-application-public-view.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
keepAlive: false,
|
keepAlive: false,
|
||||||
isTab: false,
|
isTab: false,
|
||||||
@@ -150,6 +139,39 @@ export default [
|
|||||||
bizType: 'payment-application',
|
bizType: 'payment-application',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/payment/invoice-application/public-view',
|
||||||
|
name: '查看开票申请信息',
|
||||||
|
component: () => import('@/views/payment/invoice-application-public-view.vue'),
|
||||||
|
meta: {
|
||||||
|
keepAlive: false,
|
||||||
|
isTab: false,
|
||||||
|
isAuth: false,
|
||||||
|
bizType: 'invoice-application',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/payment/invoice-receipt/public-view',
|
||||||
|
name: '查看收票登记信息',
|
||||||
|
component: () => import('@/views/payment/invoice-receipt-public-view.vue'),
|
||||||
|
meta: {
|
||||||
|
keepAlive: false,
|
||||||
|
isTab: false,
|
||||||
|
isAuth: false,
|
||||||
|
bizType: 'invoice-receipt',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/payment/bill-payment/public-view',
|
||||||
|
name: '查看汇票付款信息',
|
||||||
|
component: () => import('@/views/payment/bill-payment-public-view.vue'),
|
||||||
|
meta: {
|
||||||
|
keepAlive: false,
|
||||||
|
isTab: false,
|
||||||
|
isAuth: false,
|
||||||
|
bizType: 'bill-payment',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: '主页',
|
name: '主页',
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
import { isAdministrator as checkAdministrator } from '@/utils/auth';
|
||||||
|
|
||||||
const getters = {
|
const getters = {
|
||||||
tag: state => state.tags.tag,
|
tag: state => state.tags.tag,
|
||||||
language: state => state.common.language,
|
language: state => state.common.language,
|
||||||
setting: state => state.common.setting,
|
setting: state => state.common.setting,
|
||||||
userInfo: state => state.user.userInfo,
|
userInfo: state => state.user.userInfo,
|
||||||
|
// 超级管理员:角色别名中包含 administrator
|
||||||
|
isAdministrator: state => checkAdministrator(state.user.userInfo),
|
||||||
colorName: state => state.common.colorName,
|
colorName: state => state.common.colorName,
|
||||||
themeName: state => state.common.themeName,
|
themeName: state => state.common.themeName,
|
||||||
isMacOs: (state, getters) => getters.themeName === 'mac-os',
|
isMacOs: (state, getters) => getters.themeName === 'mac-os',
|
||||||
|
|||||||
+10
-3
@@ -150,12 +150,15 @@ a {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
.map-picker-content {
|
.map-picker-content {
|
||||||
display: flex;
|
position: relative;
|
||||||
gap: 12px;
|
display: block;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
> [class$='__map'],
|
> [class$='__map'],
|
||||||
> .address-map {
|
> .address-map {
|
||||||
flex: 1 1 auto;
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -165,4 +168,8 @@ a {
|
|||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .map-search-results {
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+103
-5
@@ -173,7 +173,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog .el-col {
|
.el-dialog .el-col {
|
||||||
|
// 取消列自身下边距,改由 form-item 统一控制行距;
|
||||||
|
// flow-root 避免子级 margin 折叠穿透导致换行后行距失效、校验提示被下一行遮挡
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
display: flow-root;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog .el-form-item,
|
.el-dialog .el-form-item,
|
||||||
@@ -181,7 +184,7 @@
|
|||||||
.el-dialog .el-form-item--small.el-form-item,
|
.el-dialog .el-form-item--small.el-form-item,
|
||||||
.el-dialog .el-form-item--large.el-form-item,
|
.el-dialog .el-form-item--large.el-form-item,
|
||||||
.el-dialog .el-form-item--medium.el-form-item {
|
.el-dialog .el-form-item--medium.el-form-item {
|
||||||
//margin-bottom: 20px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 全站弹窗:底部操作栏浮动效果(参照 business/project-apply)
|
/* 全站弹窗:底部操作栏浮动效果(参照 business/project-apply)
|
||||||
@@ -362,9 +365,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) 弹窗内表单项默认上下间距 16px(覆盖默认 18px / 22px,与 customer-archive 对齐)
|
// 4) 弹窗内表单项上下间距:预留绝对定位校验提示(约 14~18px)的可视空间
|
||||||
.el-dialog .el-form-item {
|
.el-dialog .el-form-item {
|
||||||
//margin-bottom: 16px;
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验提示浮在控件下方,提高层级避免被下一行输入框背景遮挡
|
||||||
|
.el-form-item__error {
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5) 通用分组白卡组件(<section-card>):标题融入卡头
|
// 5) 通用分组白卡组件(<section-card>):标题融入卡头
|
||||||
@@ -446,7 +454,7 @@
|
|||||||
.dialog-form--carded {
|
.dialog-form--carded {
|
||||||
.el-form-item {
|
.el-form-item {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-form-item__label {
|
.el-form-item__label {
|
||||||
@@ -644,6 +652,28 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 车辆调度新增/编辑/查看弹窗:使用部门 el-cascader 默认 display: inline-block、宽度按内容收缩,
|
||||||
|
// 会比同行的 el-input(--el-input-width 默认 100%)窄一大截,需显式拉满列宽。
|
||||||
|
// 两个必须注意的点(踩过):
|
||||||
|
// 1) avue-crud 弹窗默认 append-to-body 到 body,页面内 `.vehicle-dispatch-page :deep(...)`
|
||||||
|
// 这类以页面容器为祖先的选择器匹配不到弹窗内容,必须用弹窗自身 class(dialogCustomClass)在全局兜底;
|
||||||
|
// 2) avue 表单项在 el-form-item__content 与控件之间还有两层包裹 div
|
||||||
|
// (form/index3.js 渲染:content > div > [div|elTooltip] > 控件),
|
||||||
|
// 所以要用后代选择器,不能写 `>` 直接子级。
|
||||||
|
.vehicle-dispatch-dialog .el-form-item__content .el-cascader {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 车辆调度弹窗 label 宽度统一:option 用 labelWidth: 'auto' 时 label 宽度随字数变化
|
||||||
|
// (3 字 64px / 2 字 40px / 4 字带星号 78px),控件可用宽度随之变化,
|
||||||
|
// 于是同一行两列控件宽度不一致(如车牌号 486px vs 使用部门 472px)。
|
||||||
|
// 本弹窗左右两列最长 label 均为 4 个汉字(所属组织 / 使用部门 / 申请时间),故统一按 4em + 24px 取宽。
|
||||||
|
.vehicle-dispatch-dialog .el-form-item__label {
|
||||||
|
flex: 0 0 calc(4em + 24px) !important;
|
||||||
|
width: calc(4em + 24px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
// 过程配置新增/编辑/查看弹窗(手写 el-dialog,参照 vehicle/credit-score-quantification):
|
// 过程配置新增/编辑/查看弹窗(手写 el-dialog,参照 vehicle/credit-score-quantification):
|
||||||
// 灰底弹窗体(全站 .el-dialog__body 已统一)内堆叠 <section-card> 分组白卡,
|
// 灰底弹窗体(全站 .el-dialog__body 已统一)内堆叠 <section-card> 分组白卡,
|
||||||
// 卡间 12px 间距由 section-card 自身 margin-bottom 保证,与项目其他弹窗风格统一
|
// 卡间 12px 间距由 section-card 自身 margin-bottom 保证,与项目其他弹窗风格统一
|
||||||
@@ -995,6 +1025,66 @@ td.el-table-fixed-column--right {
|
|||||||
text-overflow: clip;
|
text-overflow: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 全站统一:内容型长文本列(文件名 / 单号 / 备注等)单元格内也放了 el-link / span.attachment-file-name,
|
||||||
|
// 会被上面两条「含 el-link 的单元格」规则一并命中,导致:
|
||||||
|
// ① 上面第 950 行的 display: flex 让链接成为 flex item,flex item 默认 min-width: auto 无法收缩,
|
||||||
|
// 超长文本(如未规范命名的附件名)会直接把列撑破;
|
||||||
|
// ② 豁免规则的 overflow: visible 让溢出内容不再被单元格裁切,直接压到相邻列上。
|
||||||
|
// 因此对「内容列」做显式兜底:单元格自身恢复块级 + 省略号截断,链接参与收缩并显示省略号。
|
||||||
|
// 用法:在 <el-table-column> 上配置 class-name="is-ellipsis-link"(动态列可 :class-name 绑定)。
|
||||||
|
// 操作列不要加此类,否则会失去自动换行能力。
|
||||||
|
.el-table .el-table__body .cell.is-ellipsis-link {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.el-link {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
// Element Plus 的 el-link__inner 是 inline-flex,flex 容器上的 text-overflow 不生效,
|
||||||
|
// 必须降级为块级容器,省略号才会真正渲染出来。
|
||||||
|
.el-link__inner {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-file-name {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 附件上传表格:文件名统一用 span,视觉与可点击行为对齐原 el-link primary
|
||||||
|
.attachment-file-name {
|
||||||
|
color: var(--el-color-primary, #409eff);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-disabled {
|
||||||
|
color: var(--el-text-color-placeholder, #c0c4cc);
|
||||||
|
cursor: not-allowed;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 全站统一:Avue 内置行操作按钮(查看 / 编辑 / 删除 / 复制等)视觉对齐为 el-link 链接样式。
|
// 全站统一:Avue 内置行操作按钮(查看 / 编辑 / 删除 / 复制等)视觉对齐为 el-link 链接样式。
|
||||||
// 选择器仅命中表格行操作列 .avue-crud__menu 内的 is-text 按钮,不误伤顶部工具栏(.avue-crud__header)
|
// 选择器仅命中表格行操作列 .avue-crud__menu 内的 is-text 按钮,不误伤顶部工具栏(.avue-crud__header)
|
||||||
// 与弹窗按钮(.avue-crud__dialog__menu)。事件(rowView/rowEdit/rowDel)与权限(permission 指令)
|
// 与弹窗按钮(.avue-crud__dialog__menu)。事件(rowView/rowEdit/rowDel)与权限(permission 指令)
|
||||||
@@ -1073,7 +1163,12 @@ td.el-table-fixed-column--right {
|
|||||||
align-items: normal;
|
align-items: normal;
|
||||||
}
|
}
|
||||||
.el-form-item--default {
|
.el-form-item--default {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 独立表单页(非弹窗)同样避免 el-col 折叠吃掉行距
|
||||||
|
.el-form .el-col {
|
||||||
|
display: flow-root;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 配套:星号必须保持「高度 auto + 继承行高」,stretch 才会按首行排布;
|
// 配套:星号必须保持「高度 auto + 继承行高」,stretch 才会按首行排布;
|
||||||
@@ -1248,3 +1343,6 @@ td.el-table-fixed-column--right {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.is-ellipsis-link {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,29 @@ const RefreshTokenKey = website.refreshTokenKey;
|
|||||||
const SessionId = 'JSESSIONID';
|
const SessionId = 'JSESSIONID';
|
||||||
const UserId = 'b-user-id';
|
const UserId = 'b-user-id';
|
||||||
|
|
||||||
|
/** 超级管理员角色别名 */
|
||||||
|
export const ADMINISTRATOR_ROLE_ALIAS = 'administrator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否超级管理员:角色别名中包含 administrator 即为超级管理员
|
||||||
|
*
|
||||||
|
* @param {object|string|string[]} [userInfoOrAuthority] 用户信息,或 authority / role_name 字符串/数组
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isAdministrator(userInfoOrAuthority) {
|
||||||
|
let authority = userInfoOrAuthority;
|
||||||
|
if (authority && typeof authority === 'object' && !Array.isArray(authority)) {
|
||||||
|
authority = authority.authority ?? authority.role_name ?? authority.roleName ?? '';
|
||||||
|
}
|
||||||
|
const roles = Array.isArray(authority)
|
||||||
|
? authority
|
||||||
|
: String(authority || '')
|
||||||
|
.split(/[,,]/)
|
||||||
|
.map(role => role.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
return roles.some(role => String(role).trim() === ADMINISTRATOR_ROLE_ALIAS);
|
||||||
|
}
|
||||||
|
|
||||||
export function getToken() {
|
export function getToken() {
|
||||||
return Cookies.get(TokenKey);
|
return Cookies.get(TokenKey);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { getDetail, getList as getCustomerList } from '@/api/vehicle/customer-archive';
|
||||||
|
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||||
|
import { getList as getVehicleList } from '@/api/transportCapacity/transport-vehicle';
|
||||||
|
|
||||||
|
const orgNameCache = new Map();
|
||||||
|
|
||||||
|
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;
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeOrgName = value => String(value || '').trim();
|
||||||
|
|
||||||
|
const uniqueOrgNames = (values = []) => {
|
||||||
|
const seen = new Set();
|
||||||
|
const result = [];
|
||||||
|
values.forEach(value => {
|
||||||
|
const name = normalizeOrgName(value);
|
||||||
|
if (!name || seen.has(name)) return;
|
||||||
|
seen.add(name);
|
||||||
|
result.push(name);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析承运商客商档案上联系人的所属组织(branchName),无联系人时回退客商所属组织。
|
||||||
|
*/
|
||||||
|
export const resolveCarrierOrganizationNames = async ({ carrierId, carrierName } = {}) => {
|
||||||
|
const id = String(carrierId || '').trim();
|
||||||
|
const name = normalizeOrgName(carrierName);
|
||||||
|
const cacheKey = id || name;
|
||||||
|
if (!cacheKey) return [];
|
||||||
|
if (orgNameCache.has(cacheKey)) return orgNameCache.get(cacheKey);
|
||||||
|
|
||||||
|
let detail = null;
|
||||||
|
if (id) {
|
||||||
|
try {
|
||||||
|
const res = await getDetail(id);
|
||||||
|
detail = res?.data?.data || res?.data || null;
|
||||||
|
} catch (error) {
|
||||||
|
detail = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!detail && name) {
|
||||||
|
try {
|
||||||
|
const res = await getCustomerList(1, 20, { fullName: name });
|
||||||
|
const records = extractRecords(res);
|
||||||
|
const matched =
|
||||||
|
records.find(item => normalizeOrgName(item.fullName || item.customerName) === name) ||
|
||||||
|
records[0];
|
||||||
|
if (matched?.id) {
|
||||||
|
const detailRes = await getDetail(matched.id);
|
||||||
|
detail = detailRes?.data?.data || detailRes?.data || matched;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
detail = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const contacts = Array.isArray(detail?.contacts) ? detail.contacts : [];
|
||||||
|
const orgNames = uniqueOrgNames([
|
||||||
|
...contacts.map(item => item.branchName),
|
||||||
|
detail?.deptName,
|
||||||
|
detail?.organizationName,
|
||||||
|
]);
|
||||||
|
orgNameCache.set(cacheKey, orgNames);
|
||||||
|
if (id && name) orgNameCache.set(name, orgNames);
|
||||||
|
return orgNames;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearCarrierOrganizationCache = (carrier = {}) => {
|
||||||
|
const id = String(carrier.carrierId || '').trim();
|
||||||
|
const name = normalizeOrgName(carrier.carrierName);
|
||||||
|
if (id) orgNameCache.delete(id);
|
||||||
|
if (name) orgNameCache.delete(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const matchOrganizationName = (value, orgNames = []) => {
|
||||||
|
const text = normalizeOrgName(value);
|
||||||
|
if (!text || !orgNames.length) return false;
|
||||||
|
return orgNames.some(org => text === org || text.includes(org) || org.includes(text));
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterByOrganizations = (records = [], orgNames = []) => {
|
||||||
|
if (!orgNames.length) return [];
|
||||||
|
return records.filter(item => matchOrganizationName(item.organizationName, orgNames));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按承运商联系人所属组织筛选司机。
|
||||||
|
* 未选择承运商时返回空列表(需先选承运商)。
|
||||||
|
*/
|
||||||
|
export const fetchDriversByCarrierOrganizations = async (
|
||||||
|
query = {},
|
||||||
|
{ carrierId, carrierName } = {}
|
||||||
|
) => {
|
||||||
|
if (!String(carrierId || '').trim() && !normalizeOrgName(carrierName)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const orgNames = await resolveCarrierOrganizationNames({ carrierId, carrierName });
|
||||||
|
if (!orgNames.length) return [];
|
||||||
|
|
||||||
|
const size = Math.max(Number(query.size) || 50, 50);
|
||||||
|
const params = { ...query };
|
||||||
|
delete params.size;
|
||||||
|
// 单组织时用后端模糊条件缩小范围;多组织再前端精确过滤
|
||||||
|
if (orgNames.length === 1) {
|
||||||
|
params.organizationName = orgNames[0];
|
||||||
|
}
|
||||||
|
const res = await getDriverList(1, Math.min(size * 5, 200), params);
|
||||||
|
return filterByOrganizations(extractRecords(res), orgNames).slice(0, size);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按承运商联系人所属组织筛选车辆。
|
||||||
|
* 未选择承运商时返回空列表。
|
||||||
|
*/
|
||||||
|
export const fetchVehiclesByCarrierOrganizations = async (
|
||||||
|
query = {},
|
||||||
|
{ carrierId, carrierName } = {}
|
||||||
|
) => {
|
||||||
|
if (!String(carrierId || '').trim() && !normalizeOrgName(carrierName)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const orgNames = await resolveCarrierOrganizationNames({ carrierId, carrierName });
|
||||||
|
if (!orgNames.length) return [];
|
||||||
|
|
||||||
|
const size = Math.max(Number(query.size) || 50, 50);
|
||||||
|
const params = { ...query };
|
||||||
|
delete params.size;
|
||||||
|
if (orgNames.length === 1) {
|
||||||
|
params.organizationName = orgNames[0];
|
||||||
|
}
|
||||||
|
const res = await getVehicleList(1, Math.min(size * 5, 200), params);
|
||||||
|
return filterByOrganizations(extractRecords(res), orgNames).slice(0, size);
|
||||||
|
};
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
/**
|
||||||
|
* 认证审核通用工具
|
||||||
|
*
|
||||||
|
* 认证对象(司机 / 车辆)每次提交产生一条不可变快照记录,「审核」针对的是**一条记录**。
|
||||||
|
* 记录分两侧:本次**待审快照**与该对象**当前生效快照**(最后一次审核通过的那条)。
|
||||||
|
* 待审内容在审核通过前不影响生效值,所以审核页必须逐字段对比两侧,而不是只看一条当前值。
|
||||||
|
*
|
||||||
|
* 本文件提供三件事:
|
||||||
|
* 1. 审核状态文案与标签(与后端 statusText 同一套词表);
|
||||||
|
* 2. 快照字段的展示元数据(中文名、控件类型、分组);
|
||||||
|
* 3. 两侧快照的字段级 diff 计算,含「从未认证通过、无对比基准」的处理。
|
||||||
|
*
|
||||||
|
* 两侧快照字段名完全一致(同一套结构),故可逐键比较。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const EMPTY_TEXT = '—';
|
||||||
|
|
||||||
|
const TYPE_IMAGE = 'image';
|
||||||
|
const TYPE_DATE = 'date';
|
||||||
|
const TYPE_LONG_TERM = 'longTerm';
|
||||||
|
|
||||||
|
/** 审核状态取值 */
|
||||||
|
export const CERT_RECORD_STATUS = {
|
||||||
|
PENDING: 0,
|
||||||
|
APPROVED: 1,
|
||||||
|
REJECTED: 2,
|
||||||
|
VOIDED: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 状态文案统一为:审核中 / 已通过 / 已驳回 / 已作废 */
|
||||||
|
const CERT_STATUS_TAG = {
|
||||||
|
0: { label: '审核中', type: 'warning' },
|
||||||
|
1: { label: '已通过', type: 'success' },
|
||||||
|
2: { label: '已驳回', type: 'danger' },
|
||||||
|
3: { label: '已作废', type: 'info' },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 变更类型 → 差异标签 */
|
||||||
|
export const CERT_CHANGE_TAG = {
|
||||||
|
added: { label: '新增', type: 'success' },
|
||||||
|
modified: { label: '已修改', type: 'warning' },
|
||||||
|
cleared: { label: '已清空', type: 'danger' },
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「最新提交状态」展示缓存 → 标签
|
||||||
|
*
|
||||||
|
* 缓存为空表示该对象从未提交过认证(即没有生效快照)。
|
||||||
|
* 注意它只是最近一次提交的状态,不等于「是否已认证」——后者由生效快照指针决定。
|
||||||
|
*/
|
||||||
|
export const certStatusTag = status => {
|
||||||
|
if (status === null || status === undefined || status === '') {
|
||||||
|
return { label: '未认证', type: 'info' };
|
||||||
|
}
|
||||||
|
return CERT_STATUS_TAG[status] || { label: EMPTY_TEXT, type: 'info' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const pad = value => String(value).padStart(2, '0');
|
||||||
|
|
||||||
|
/** 后端若改用时间戳毫秒序列化日期,也能落成日期文案 */
|
||||||
|
const EPOCH_PATTERN = /^\d{13}$/;
|
||||||
|
|
||||||
|
const isEpochText = text => EPOCH_PATTERN.test(text);
|
||||||
|
|
||||||
|
const toDate = text => {
|
||||||
|
const date = new Date(Number(text));
|
||||||
|
return Number.isNaN(date.getTime()) ? null : date;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 提交时间等日期时间展示,兼容 `yyyy-MM-dd HH:mm:ss`、ISO 串与时间戳毫秒 */
|
||||||
|
export const formatCertDateTime = value => {
|
||||||
|
if (value === null || value === undefined || value === '') return EMPTY_TEXT;
|
||||||
|
const text = String(value).trim();
|
||||||
|
const matched = text.match(
|
||||||
|
/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})[T ](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?/
|
||||||
|
);
|
||||||
|
if (matched) {
|
||||||
|
return (
|
||||||
|
`${matched[1]}-${pad(matched[2])}-${pad(matched[3])} ` +
|
||||||
|
`${pad(matched[4])}:${pad(matched[5])}:${pad(matched[6] || 0)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const date = isEpochText(text) ? toDate(text) : null;
|
||||||
|
if (!date) return text;
|
||||||
|
return (
|
||||||
|
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
|
||||||
|
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快照字段元数据
|
||||||
|
*
|
||||||
|
* 字段名与后端提交 DTO 的 JSON 键一致(司机 PersonCertSubmitRequest / 车辆 VehicleCertSubmitRequest),
|
||||||
|
* 两侧快照共用同一套结构,故同时用于标签与比较规则。
|
||||||
|
*
|
||||||
|
* `longTermKey` 只挂在**被长期有效作废掉的那个字段**上,也就是各组的「有效期止」与强制报废日期:
|
||||||
|
* 勾了长期有效,它们显示「长期有效」、比较时按同一个值算(避免每次审核都报「已变更」)。
|
||||||
|
* 「有效期起」不挂:起期是一个独立的日期,长期有效并不会让它失效,
|
||||||
|
* 挂上去等于把它自己的值藏起来、还让审核员看不到起期的改动。
|
||||||
|
*/
|
||||||
|
const CERT_SNAPSHOT_GROUPS = {
|
||||||
|
driver: [
|
||||||
|
{
|
||||||
|
title: '身份信息',
|
||||||
|
fields: [
|
||||||
|
{ key: 'realName', label: '真实姓名' },
|
||||||
|
{ key: 'idCardNo', label: '身份证号' },
|
||||||
|
{ key: 'idCardFrontUrl', label: '身份证正面照', type: TYPE_IMAGE },
|
||||||
|
{ key: 'idCardBackUrl', label: '身份证反面照', type: TYPE_IMAGE },
|
||||||
|
{ key: 'idCardValidStart', label: '身份证有效期起', type: TYPE_DATE },
|
||||||
|
{
|
||||||
|
key: 'idCardValidEnd',
|
||||||
|
label: '身份证有效期止',
|
||||||
|
type: TYPE_DATE,
|
||||||
|
longTermKey: 'idCardLongTerm',
|
||||||
|
},
|
||||||
|
{ key: 'idCardLongTerm', label: '身份证长期有效', type: TYPE_LONG_TERM },
|
||||||
|
{ key: 'headPhoto', label: '大头照', type: TYPE_IMAGE },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '驾驶证信息',
|
||||||
|
fields: [
|
||||||
|
{ key: 'driverFileNo', label: '驾驶证档案编号' },
|
||||||
|
{ key: 'driverClass', label: '准驾车型' },
|
||||||
|
{ key: 'driverLicenseFrontUrl', label: '驾驶证主页照片', type: TYPE_IMAGE },
|
||||||
|
{ key: 'driverLicenseBackUrl', label: '驾驶证副页照片', type: TYPE_IMAGE },
|
||||||
|
{ key: 'driverValidStart', label: '驾驶证有效期起', type: TYPE_DATE },
|
||||||
|
{
|
||||||
|
key: 'driverValidEnd',
|
||||||
|
label: '驾驶证有效期止',
|
||||||
|
type: TYPE_DATE,
|
||||||
|
longTermKey: 'driverLongTerm',
|
||||||
|
},
|
||||||
|
{ key: 'driverLongTerm', label: '驾驶证长期有效', type: TYPE_LONG_TERM },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '从业资格证信息',
|
||||||
|
fields: [
|
||||||
|
{ key: 'qualificationType', label: '从业资格认证类型' },
|
||||||
|
{ key: 'qualificationNo', label: '从业资格证号' },
|
||||||
|
{
|
||||||
|
key: 'qualificationFront',
|
||||||
|
label: '从业资格证封面页',
|
||||||
|
type: TYPE_IMAGE,
|
||||||
|
legacyKeys: ['qualificationUrl'],
|
||||||
|
},
|
||||||
|
{ key: 'qualificationBack', label: '从业资格证内容页', type: TYPE_IMAGE },
|
||||||
|
{ key: 'qualificationValidStart', label: '从业资格证有效期起', type: TYPE_DATE },
|
||||||
|
{
|
||||||
|
key: 'qualificationValidEnd',
|
||||||
|
label: '从业资格证有效期止',
|
||||||
|
type: TYPE_DATE,
|
||||||
|
longTermKey: 'qualificationLongTerm',
|
||||||
|
},
|
||||||
|
{ key: 'qualificationLongTerm', label: '从业资格证长期有效', type: TYPE_LONG_TERM },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
/*
|
||||||
|
* 车辆字段名以 VehicleCertSubmitRequest 为准,与 PersonCertSubmitRequest 不同。
|
||||||
|
* 键名对不上不报错——每个字段都算「没有值」,两组全显示「—」且 diff 不出变更。
|
||||||
|
*/
|
||||||
|
vehicle: [
|
||||||
|
{
|
||||||
|
title: '车辆基本信息',
|
||||||
|
fields: [
|
||||||
|
{ key: 'plateNo', label: '车牌号' },
|
||||||
|
{ key: 'vehicleType', label: '车辆类型' },
|
||||||
|
{ key: 'plateColor', label: '车牌颜色' },
|
||||||
|
{ key: 'ownerName', label: '所有人' },
|
||||||
|
{ key: 'vin', label: '车辆识别代号(车架号)' },
|
||||||
|
{ key: 'outerLength', label: '外廓长度(mm)' },
|
||||||
|
{ key: 'outerWidth', label: '外廓宽度(mm)' },
|
||||||
|
{ key: 'outerHeight', label: '外廓高度(mm)' },
|
||||||
|
{ key: 'approvedLoadKg', label: '核定载质量(KG)' },
|
||||||
|
{ key: 'tractionMassKg', label: '准牵引总质量(KG)' },
|
||||||
|
{ key: 'energyType', label: '能源类型' },
|
||||||
|
{ key: 'businessRelation', label: '业务关系' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '行驶证信息',
|
||||||
|
fields: [
|
||||||
|
{ key: 'compulsoryScrapDate', label: '强制报废日期', type: TYPE_DATE, longTermKey: 'compulsoryScrapLongTerm' },
|
||||||
|
{ key: 'compulsoryScrapLongTerm', label: '强制报废长期有效', type: TYPE_LONG_TERM },
|
||||||
|
{ key: 'drivingLicenseNo', label: '行驶证档案编号' },
|
||||||
|
{ key: 'drivingLicenseImage', label: '行驶证主页正面', type: TYPE_IMAGE },
|
||||||
|
{ key: 'drivingLicenseMainBack', label: '行驶证主页反面', type: TYPE_IMAGE },
|
||||||
|
{ key: 'drivingLicenseViceFront', label: '行驶证副页正面', type: TYPE_IMAGE },
|
||||||
|
{ key: 'drivingLicenseViceBack', label: '行驶证副页反面', type: TYPE_IMAGE },
|
||||||
|
{ key: 'drivingLicenseStartDate', label: '行驶证有效期起', type: TYPE_DATE },
|
||||||
|
{
|
||||||
|
key: 'drivingLicenseEndDate',
|
||||||
|
label: '行驶证有效期止',
|
||||||
|
type: TYPE_DATE,
|
||||||
|
longTermKey: 'drivingLicenseLongTerm',
|
||||||
|
},
|
||||||
|
{ key: 'drivingLicenseLongTerm', label: '行驶证长期有效', type: TYPE_LONG_TERM },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '道路运输证信息',
|
||||||
|
fields: [
|
||||||
|
{ key: 'roadTransportCertNo', label: '道路运输证号' },
|
||||||
|
{ key: 'roadTransportCertImage', label: '道路运输证照片', type: TYPE_IMAGE },
|
||||||
|
{ key: 'roadTransportCertStartDate', label: '道路运输证有效期起', type: TYPE_DATE },
|
||||||
|
{
|
||||||
|
key: 'roadTransportCertEndDate',
|
||||||
|
label: '道路运输证有效期止',
|
||||||
|
type: TYPE_DATE,
|
||||||
|
longTermKey: 'roadTransportCertLongTerm',
|
||||||
|
},
|
||||||
|
{ key: 'roadTransportCertLongTerm', label: '道路运输证长期有效', type: TYPE_LONG_TERM },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '机动车登记本信息',
|
||||||
|
fields: [
|
||||||
|
{ key: 'registrationNo', label: '机动车登记编号' },
|
||||||
|
{ key: 'registrationImage', label: '机动车登记本照片', type: TYPE_IMAGE },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 提交元信息与主键不参与对比(两侧必相同,展示出来只是噪音) */
|
||||||
|
const IGNORED_KEYS = [
|
||||||
|
'id',
|
||||||
|
'bizType',
|
||||||
|
'bizId',
|
||||||
|
'bizKey',
|
||||||
|
'certificationStatus',
|
||||||
|
'snapshotJson',
|
||||||
|
'pendingSnapshotJson',
|
||||||
|
'effectiveSnapshotJson',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析快照 JSON
|
||||||
|
*
|
||||||
|
* 后端把 null 序列化为空串,故空串 / 'null' 都按「没有快照」处理,不算解析失败。
|
||||||
|
*/
|
||||||
|
const readSnapshot = json => {
|
||||||
|
if (json && typeof json === 'object') {
|
||||||
|
return { value: Object.keys(json).length ? json : null, invalid: false };
|
||||||
|
}
|
||||||
|
const text = String(json === null || json === undefined ? '' : json).trim();
|
||||||
|
if (!text || text === 'null') {
|
||||||
|
return { value: null, invalid: false };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text);
|
||||||
|
if (parsed && typeof parsed === 'object') {
|
||||||
|
return { value: parsed, invalid: false };
|
||||||
|
}
|
||||||
|
return { value: null, invalid: true };
|
||||||
|
} catch (error) {
|
||||||
|
return { value: null, invalid: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 解析快照 JSON,无快照或解析失败时返回 null */
|
||||||
|
export const parseSnapshot = json => readSnapshot(json).value;
|
||||||
|
|
||||||
|
const isEmptyValue = value => value === null || value === undefined || value === '';
|
||||||
|
|
||||||
|
const isLongTermOn = value => value === 1 || value === '1' || value === true;
|
||||||
|
|
||||||
|
/** 日期归一化到 yyyy-MM-dd,便于 `2026-01-01` 与 `2026-01-01T00:00:00` 判等 */
|
||||||
|
const normalizeDate = value => {
|
||||||
|
if (isEmptyValue(value)) return '';
|
||||||
|
const text = String(value).trim();
|
||||||
|
const matched = text.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/);
|
||||||
|
if (matched) return `${matched[1]}-${pad(matched[2])}-${pad(matched[3])}`;
|
||||||
|
const date = isEpochText(text) ? toDate(text) : null;
|
||||||
|
if (date) {
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取字段值,兼容字段改名前的旧键
|
||||||
|
*
|
||||||
|
* 后端把某字段改名后,改名之前提交的记录里存的仍是旧键(例:从业资格证封面页
|
||||||
|
* 由 `qualificationUrl` 改为 `qualificationFront`)。只读新键会得到空,
|
||||||
|
* 审核页就把已填的封面页显示成「—」,误导审核员以为对方没传。
|
||||||
|
* 新旧键都存在时以新键为准。
|
||||||
|
*/
|
||||||
|
const readFieldValue = (snapshot, field) => {
|
||||||
|
if (!snapshot) return undefined;
|
||||||
|
const value = snapshot[field.key];
|
||||||
|
if (!isEmptyValue(value)) return value;
|
||||||
|
const legacyKeys = field.legacyKeys || [];
|
||||||
|
for (let i = 0; i < legacyKeys.length; i += 1) {
|
||||||
|
if (!isEmptyValue(snapshot[legacyKeys[i]])) return snapshot[legacyKeys[i]];
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 比较用的归一化取值:展示相同即视为未变更 */
|
||||||
|
const canonicalOf = (snapshot, field) => {
|
||||||
|
const value = readFieldValue(snapshot, field);
|
||||||
|
if (field.type === TYPE_LONG_TERM) {
|
||||||
|
if (isEmptyValue(value)) return '';
|
||||||
|
return isLongTermOn(value) ? '1' : '0';
|
||||||
|
}
|
||||||
|
if (field.type === TYPE_DATE) {
|
||||||
|
if (field.longTermKey && isLongTermOn(snapshot && snapshot[field.longTermKey])) {
|
||||||
|
return '长期有效';
|
||||||
|
}
|
||||||
|
return normalizeDate(value);
|
||||||
|
}
|
||||||
|
return isEmptyValue(value) ? '' : String(value).trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 展示文案:日期型受「长期有效」影响,图片型返回 URL 由组件渲染缩略图 */
|
||||||
|
const textOf = (snapshot, field) => {
|
||||||
|
const value = readFieldValue(snapshot, field);
|
||||||
|
if (field.type === TYPE_IMAGE) {
|
||||||
|
return isEmptyValue(value) ? '' : String(value);
|
||||||
|
}
|
||||||
|
if (field.type === TYPE_LONG_TERM) {
|
||||||
|
if (isEmptyValue(value)) return EMPTY_TEXT;
|
||||||
|
return isLongTermOn(value) ? '是' : '否';
|
||||||
|
}
|
||||||
|
if (field.type === TYPE_DATE) {
|
||||||
|
if (field.longTermKey && isLongTermOn(snapshot && snapshot[field.longTermKey])) {
|
||||||
|
return '长期有效';
|
||||||
|
}
|
||||||
|
return normalizeDate(value) || EMPTY_TEXT;
|
||||||
|
}
|
||||||
|
return isEmptyValue(value) ? EMPTY_TEXT : String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildRow = (field, pendingSnapshot, effectiveSnapshot, hasBaseline) => {
|
||||||
|
const type = field.type || 'text';
|
||||||
|
const target = { ...field, type };
|
||||||
|
const pendingCanonical = canonicalOf(pendingSnapshot, target);
|
||||||
|
const effectiveCanonical = canonicalOf(effectiveSnapshot, target);
|
||||||
|
let changeType = '';
|
||||||
|
if (hasBaseline && pendingCanonical !== effectiveCanonical) {
|
||||||
|
if (!effectiveCanonical) {
|
||||||
|
changeType = 'added';
|
||||||
|
} else if (!pendingCanonical) {
|
||||||
|
changeType = 'cleared';
|
||||||
|
} else {
|
||||||
|
changeType = 'modified';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key: field.key,
|
||||||
|
label: field.label,
|
||||||
|
type,
|
||||||
|
effective: {
|
||||||
|
raw: readFieldValue(effectiveSnapshot, target),
|
||||||
|
text: textOf(effectiveSnapshot, target),
|
||||||
|
},
|
||||||
|
pending: {
|
||||||
|
raw: readFieldValue(pendingSnapshot, target),
|
||||||
|
text: textOf(pendingSnapshot, target),
|
||||||
|
},
|
||||||
|
changed: Boolean(changeType),
|
||||||
|
changeType,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const unionKeys = (pendingSnapshot, effectiveSnapshot) => {
|
||||||
|
const keys = [];
|
||||||
|
[pendingSnapshot, effectiveSnapshot].forEach(snapshot => {
|
||||||
|
Object.keys(snapshot || {}).forEach(key => {
|
||||||
|
if (!keys.includes(key)) keys.push(key);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return keys;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造「待审快照 vs 当前生效值」的字段级 diff
|
||||||
|
*
|
||||||
|
* @param {string} bizType 认证对象类型:driver / vehicle
|
||||||
|
* @param {string} pendingJson 本次待审快照 JSON
|
||||||
|
* @param {string} effectiveJson 该对象当前生效快照 JSON,从未认证通过时为空
|
||||||
|
* @returns {Object} { hasBaseline, groups, totalCount, changedCount, ... }
|
||||||
|
* hasBaseline 为 false 表示无对比基准(从未认证通过),此时不产出任何变更标记
|
||||||
|
*/
|
||||||
|
export const buildSnapshotDiff = (bizType, pendingJson, effectiveJson) => {
|
||||||
|
const pending = readSnapshot(pendingJson);
|
||||||
|
const effective = readSnapshot(effectiveJson);
|
||||||
|
const hasBaseline = effective.value !== null;
|
||||||
|
const groupsConfig = CERT_SNAPSHOT_GROUPS[bizType] || [];
|
||||||
|
const knownKeys = [];
|
||||||
|
|
||||||
|
const groups = groupsConfig.map(group => {
|
||||||
|
const rows = group.fields.map(field => {
|
||||||
|
// 旧键已由所属字段并入展示,不能再作为独立字段出现,否则同一张封面页会出现两行
|
||||||
|
knownKeys.push(field.key, ...(field.legacyKeys || []));
|
||||||
|
return buildRow(field, pending.value, effective.value, hasBaseline);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
title: group.title,
|
||||||
|
rows,
|
||||||
|
changedCount: rows.filter(row => row.changed).length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 元数据未登记的字段照样展示,避免后端新增字段时审核员看不到变化
|
||||||
|
const extraKeys = unionKeys(pending.value, effective.value).filter(
|
||||||
|
key => !knownKeys.includes(key) && !IGNORED_KEYS.includes(key)
|
||||||
|
);
|
||||||
|
if (extraKeys.length) {
|
||||||
|
const rows = extraKeys.map(key =>
|
||||||
|
buildRow({ key, label: key }, pending.value, effective.value, hasBaseline)
|
||||||
|
);
|
||||||
|
groups.push({
|
||||||
|
title: '其他字段',
|
||||||
|
rows,
|
||||||
|
changedCount: rows.filter(row => row.changed).length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = groups.reduce((all, group) => all.concat(group.rows), []);
|
||||||
|
return {
|
||||||
|
hasBaseline,
|
||||||
|
pendingInvalid: pending.invalid,
|
||||||
|
effectiveInvalid: effective.invalid,
|
||||||
|
groups,
|
||||||
|
totalCount: rows.length,
|
||||||
|
changedCount: rows.filter(row => row.changed).length,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* 合同下拉/选择展示:合同编号/合同名称
|
||||||
|
*
|
||||||
|
* @param {Object} contract 合同对象
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function formatContractOptionLabel(contract = {}) {
|
||||||
|
const contractNo = String(contract?.contractNo ?? '').trim();
|
||||||
|
const contractName = String(contract?.contractName ?? '').trim();
|
||||||
|
if (contractNo && contractName) {
|
||||||
|
return `${contractNo}/${contractName}`;
|
||||||
|
}
|
||||||
|
return contractNo || contractName || '';
|
||||||
|
}
|
||||||
@@ -13,6 +13,92 @@ const coordinateRanges = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** WGS-84 → GCJ-02(高德)偏移算法常量 */
|
||||||
|
const WGS84_TO_GCJ02_PI = Math.PI;
|
||||||
|
const WGS84_TO_GCJ02_A = 6378245.0;
|
||||||
|
const WGS84_TO_GCJ02_EE = 0.00669342162296594323;
|
||||||
|
|
||||||
|
const isOutOfChina = (longitude, latitude) =>
|
||||||
|
longitude < 72.004 || longitude > 137.8347 || latitude < 0.8293 || latitude > 55.8271;
|
||||||
|
|
||||||
|
const transformLatitude = (longitude, latitude) => {
|
||||||
|
let result =
|
||||||
|
-100.0 +
|
||||||
|
2.0 * longitude +
|
||||||
|
3.0 * latitude +
|
||||||
|
0.2 * latitude * latitude +
|
||||||
|
0.1 * longitude * latitude +
|
||||||
|
0.2 * Math.sqrt(Math.abs(longitude));
|
||||||
|
result +=
|
||||||
|
((20.0 * Math.sin(6.0 * longitude * WGS84_TO_GCJ02_PI) +
|
||||||
|
20.0 * Math.sin(2.0 * longitude * WGS84_TO_GCJ02_PI)) *
|
||||||
|
2.0) /
|
||||||
|
3.0;
|
||||||
|
result +=
|
||||||
|
((20.0 * Math.sin(latitude * WGS84_TO_GCJ02_PI) +
|
||||||
|
40.0 * Math.sin((latitude / 3.0) * WGS84_TO_GCJ02_PI)) *
|
||||||
|
2.0) /
|
||||||
|
3.0;
|
||||||
|
result +=
|
||||||
|
((160.0 * Math.sin((latitude / 12.0) * WGS84_TO_GCJ02_PI) +
|
||||||
|
320 * Math.sin((latitude * WGS84_TO_GCJ02_PI) / 30.0)) *
|
||||||
|
2.0) /
|
||||||
|
3.0;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformLongitude = (longitude, latitude) => {
|
||||||
|
let result =
|
||||||
|
300.0 +
|
||||||
|
longitude +
|
||||||
|
2.0 * latitude +
|
||||||
|
0.1 * longitude * longitude +
|
||||||
|
0.1 * longitude * latitude +
|
||||||
|
0.1 * Math.sqrt(Math.abs(longitude));
|
||||||
|
result +=
|
||||||
|
((20.0 * Math.sin(6.0 * longitude * WGS84_TO_GCJ02_PI) +
|
||||||
|
20.0 * Math.sin(2.0 * longitude * WGS84_TO_GCJ02_PI)) *
|
||||||
|
2.0) /
|
||||||
|
3.0;
|
||||||
|
result +=
|
||||||
|
((20.0 * Math.sin(longitude * WGS84_TO_GCJ02_PI) +
|
||||||
|
40.0 * Math.sin((longitude / 3.0) * WGS84_TO_GCJ02_PI)) *
|
||||||
|
2.0) /
|
||||||
|
3.0;
|
||||||
|
result +=
|
||||||
|
((150.0 * Math.sin((longitude / 12.0) * WGS84_TO_GCJ02_PI) +
|
||||||
|
300.0 * Math.sin((longitude / 30.0) * WGS84_TO_GCJ02_PI)) *
|
||||||
|
2.0) /
|
||||||
|
3.0;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WGS-84(GPS / LBS)转 GCJ-02(高德/国测局),供高德地图绘制使用。
|
||||||
|
* 中国境外坐标不做偏移,原样返回。
|
||||||
|
* @param {number} longitude
|
||||||
|
* @param {number} latitude
|
||||||
|
* @returns {[number, number]} [经度, 纬度]
|
||||||
|
*/
|
||||||
|
export const wgs84ToGcj02 = (longitude, latitude) => {
|
||||||
|
const lng = Number(longitude);
|
||||||
|
const lat = Number(latitude);
|
||||||
|
if (!Number.isFinite(lng) || !Number.isFinite(lat) || isOutOfChina(lng, lat)) {
|
||||||
|
return [lng, lat];
|
||||||
|
}
|
||||||
|
let dLat = transformLatitude(lng - 105.0, lat - 35.0);
|
||||||
|
let dLng = transformLongitude(lng - 105.0, lat - 35.0);
|
||||||
|
const radLat = (lat / 180.0) * WGS84_TO_GCJ02_PI;
|
||||||
|
let magic = Math.sin(radLat);
|
||||||
|
magic = 1 - WGS84_TO_GCJ02_EE * magic * magic;
|
||||||
|
const sqrtMagic = Math.sqrt(magic);
|
||||||
|
dLat =
|
||||||
|
(dLat * 180.0) /
|
||||||
|
(((WGS84_TO_GCJ02_A * (1 - WGS84_TO_GCJ02_EE)) / (magic * sqrtMagic)) * WGS84_TO_GCJ02_PI);
|
||||||
|
dLng = (dLng * 180.0) / ((WGS84_TO_GCJ02_A / sqrtMagic) * Math.cos(radLat) * WGS84_TO_GCJ02_PI);
|
||||||
|
return [lng + dLng, lat + dLat];
|
||||||
|
};
|
||||||
|
|
||||||
export const normalizeCoordinateInput = value => {
|
export const normalizeCoordinateInput = value => {
|
||||||
const rawValue = String(value ?? '').replace(/[^\d.-]/g, '');
|
const rawValue = String(value ?? '').replace(/[^\d.-]/g, '');
|
||||||
const isNegative = rawValue.startsWith('-');
|
const isNegative = rawValue.startsWith('-');
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
const CURRENCY_NAME_MAP = {
|
||||||
|
CNY: '人民币',
|
||||||
|
RMB: '人民币',
|
||||||
|
USD: '美元',
|
||||||
|
EUR: '欧元',
|
||||||
|
JPY: '日元',
|
||||||
|
GBP: '英镑',
|
||||||
|
};
|
||||||
|
|
||||||
|
const CURRENCY_UNIT_MAP = {
|
||||||
|
CNY: '元',
|
||||||
|
RMB: '元',
|
||||||
|
USD: '美元',
|
||||||
|
EUR: '欧元',
|
||||||
|
JPY: '日元',
|
||||||
|
GBP: '英镑',
|
||||||
|
};
|
||||||
|
|
||||||
|
const CURRENCY_NAME_TO_UNIT = {
|
||||||
|
人民币: '元',
|
||||||
|
美元: '美元',
|
||||||
|
欧元: '欧元',
|
||||||
|
日元: '日元',
|
||||||
|
英镑: '英镑',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归一化币种代码(CNY → RMB)
|
||||||
|
*/
|
||||||
|
export function normalizeCurrencyCode(value, fallback = 'RMB') {
|
||||||
|
const raw =
|
||||||
|
typeof value === 'object'
|
||||||
|
? value?.dictKey || value?.value || value?.code || value?.dictValue || ''
|
||||||
|
: value;
|
||||||
|
const normalized = String(raw || fallback)
|
||||||
|
.trim()
|
||||||
|
.toUpperCase();
|
||||||
|
const code = normalized.includes(' - ') ? normalized.split(' - ')[0].trim() : normalized;
|
||||||
|
if (!code) return fallback;
|
||||||
|
return code === 'CNY' ? 'RMB' : code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 币种中文名称
|
||||||
|
*/
|
||||||
|
export function currencyName(value) {
|
||||||
|
const text = String(value || '');
|
||||||
|
if (text.includes(' - ')) return text.slice(text.indexOf(' - ') + 3);
|
||||||
|
return CURRENCY_NAME_MAP[text.toUpperCase()] || text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 金额单位文案:RMB/CNY → 元,USD → 美元
|
||||||
|
*/
|
||||||
|
export function currencyUnit(value) {
|
||||||
|
const text = String(value || '');
|
||||||
|
let code = text;
|
||||||
|
if (text.includes(' - ')) {
|
||||||
|
code = text.split(' - ')[0].trim();
|
||||||
|
}
|
||||||
|
const upperCode = code.toUpperCase();
|
||||||
|
if (CURRENCY_UNIT_MAP[upperCode]) {
|
||||||
|
return CURRENCY_UNIT_MAP[upperCode];
|
||||||
|
}
|
||||||
|
if (CURRENCY_NAME_TO_UNIT[code]) {
|
||||||
|
return CURRENCY_NAME_TO_UNIT[code];
|
||||||
|
}
|
||||||
|
return text || '元';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将计价单位前缀替换为当前币种单位,如 元/吨 → 美元/吨
|
||||||
|
*/
|
||||||
|
export function remapPriceUnitCurrency(priceUnit, currency) {
|
||||||
|
const text = String(priceUnit || '').trim();
|
||||||
|
if (!text || !text.includes('/')) return text;
|
||||||
|
const suffix = text.slice(text.indexOf('/'));
|
||||||
|
return `${currencyUnit(currency)}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按数量单位生成默认计价单位
|
||||||
|
*/
|
||||||
|
export function resolvePriceUnitByQuantityUnit(quantityUnit, currency = 'RMB') {
|
||||||
|
const unit = currencyUnit(currency);
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
吨: `${unit}/吨`,
|
||||||
|
方: `${unit}/立方`,
|
||||||
|
件: `${unit}/件`,
|
||||||
|
}[String(quantityUnit || '').trim()] || ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字典中的计价单位选项映射到当前币种
|
||||||
|
*/
|
||||||
|
export function mapFeeUnitOptions(options = [], currency = 'RMB') {
|
||||||
|
const unit = currencyUnit(currency);
|
||||||
|
return (options || []).map(item => {
|
||||||
|
const label = String(item.label || item.value || '');
|
||||||
|
const value = String(item.value || item.label || '');
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
label: label.replace(/^元/, unit).replace(/^人民币/, unit),
|
||||||
|
value: value.replace(/^元/, unit).replace(/^人民币/, unit),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
+99
-22
@@ -6,7 +6,31 @@ import 'nprogress/nprogress.css';
|
|||||||
|
|
||||||
const excelPattern = /\.(xls|xlsx)$/;
|
const excelPattern = /\.(xls|xlsx)$/;
|
||||||
|
|
||||||
|
/** 是否为浏览器 Blob(含跨 realm 的 Blob-like) */
|
||||||
|
const isBlobLike = value =>
|
||||||
|
value != null &&
|
||||||
|
typeof value === 'object' &&
|
||||||
|
typeof value.size === 'number' &&
|
||||||
|
typeof value.type === 'string' &&
|
||||||
|
(typeof value.text === 'function' || typeof value.arrayBuffer === 'function');
|
||||||
|
|
||||||
|
/** axios 拦截器已把 JSON Blob 解析成的业务对象 */
|
||||||
|
const isBusinessJson = value =>
|
||||||
|
value != null &&
|
||||||
|
typeof value === 'object' &&
|
||||||
|
!isBlobLike(value) &&
|
||||||
|
!(value instanceof ArrayBuffer) &&
|
||||||
|
('code' in value || 'success' in value || 'msg' in value);
|
||||||
|
|
||||||
const isExcelResponse = res => {
|
const isExcelResponse = res => {
|
||||||
|
// 已是业务 JSON(导入成功/失败提示)时,绝不能当失败明细 Excel 处理
|
||||||
|
if (isBusinessJson(res?.data)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 只有文件流才可能是失败明细;普通对象没有 .text/.arrayBuffer
|
||||||
|
if (!isBlobLike(res?.data)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const headers = res.headers || {};
|
const headers = res.headers || {};
|
||||||
const contentType = String(headers['content-type'] || res.data?.type || '').toLowerCase();
|
const contentType = String(headers['content-type'] || res.data?.type || '').toLowerCase();
|
||||||
const contentDisposition = String(headers['content-disposition'] || '').toLowerCase();
|
const contentDisposition = String(headers['content-disposition'] || '').toLowerCase();
|
||||||
@@ -23,8 +47,34 @@ const isExcelResponse = res => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseBlobJson = async blob => {
|
/**
|
||||||
const text = await blob.text();
|
* 解析导入接口响应体为 JSON 对象。
|
||||||
|
* <p>
|
||||||
|
* axios 拦截器在 Content-Type 为 JSON 时,会把 `responseType: 'blob'` 的 Blob
|
||||||
|
* 提前解析成普通对象;此处需同时兼容 Blob / 字符串 / 已解析对象,避免再调
|
||||||
|
* `blob.text()` 时出现「blob.text is not a function」。
|
||||||
|
*/
|
||||||
|
const parseBlobJson = async data => {
|
||||||
|
if (data == null) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (isBusinessJson(data)) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
if (typeof data === 'object' && !isBlobLike(data) && !(data instanceof ArrayBuffer)) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return data ? JSON.parse(data) : {};
|
||||||
|
}
|
||||||
|
let text = '';
|
||||||
|
if (typeof data.text === 'function') {
|
||||||
|
text = await data.text();
|
||||||
|
} else if (typeof data.arrayBuffer === 'function') {
|
||||||
|
text = new TextDecoder().decode(await data.arrayBuffer());
|
||||||
|
} else if (data instanceof ArrayBuffer) {
|
||||||
|
text = new TextDecoder().decode(data);
|
||||||
|
}
|
||||||
return text ? JSON.parse(text) : {};
|
return text ? JSON.parse(text) : {};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,18 +89,21 @@ const parseBlobJson = async blob => {
|
|||||||
* 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。
|
* 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。
|
||||||
*/
|
*/
|
||||||
const resolveErrorMessage = async error => {
|
const resolveErrorMessage = async error => {
|
||||||
const blob = error?.response?.data ?? error?.data;
|
const payload = error?.response?.data ?? error?.data;
|
||||||
if (blob instanceof Blob) {
|
if (isBusinessJson(payload) && payload.msg) {
|
||||||
|
return payload.msg;
|
||||||
|
}
|
||||||
|
if (isBlobLike(payload) || typeof payload === 'string') {
|
||||||
try {
|
try {
|
||||||
const result = await parseBlobJson(blob);
|
const result = await parseBlobJson(payload);
|
||||||
if (result?.msg) return result.msg;
|
if (result?.msg) return result.msg;
|
||||||
} catch (ignored) {
|
} catch (ignored) {
|
||||||
// 响应体不是 JSON(如网关返回的 HTML 错误页),退回通用文案
|
// 响应体不是 JSON(如网关返回的 HTML 错误页),退回通用文案
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const fallback = error?.response?.data?.msg || error?.message;
|
const fallback = payload?.msg || error?.message;
|
||||||
// axios 的英文通用文案对用户没有意义,统一换成中文提示
|
// axios 的英文通用文案对用户没有意义,统一换成中文提示
|
||||||
if (!fallback || /Request failed with status code/i.test(fallback)) {
|
if (!fallback || /Request failed with status code|\.text is not a function/i.test(fallback)) {
|
||||||
return '导入失败,请检查导入文件后重试';
|
return '导入失败,请检查导入文件后重试';
|
||||||
}
|
}
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -68,8 +121,11 @@ const resolveErrorMessage = async error => {
|
|||||||
*/
|
*/
|
||||||
const decorateFailExcel = async (blob, businessName, failDetailDecorator) => {
|
const decorateFailExcel = async (blob, businessName, failDetailDecorator) => {
|
||||||
if (typeof failDetailDecorator !== 'function') return blob;
|
if (typeof failDetailDecorator !== 'function') return blob;
|
||||||
|
if (!isBlobLike(blob) && !(blob instanceof ArrayBuffer)) {
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
const ExcelJS = await import('exceljs');
|
const ExcelJS = await import('exceljs');
|
||||||
const buf = await blob.arrayBuffer();
|
const buf = typeof blob.arrayBuffer === 'function' ? await blob.arrayBuffer() : blob;
|
||||||
const workbook = new ExcelJS.Workbook();
|
const workbook = new ExcelJS.Workbook();
|
||||||
await workbook.xlsx.load(buf);
|
await workbook.xlsx.load(buf);
|
||||||
await failDetailDecorator(workbook, { businessName });
|
await failDetailDecorator(workbook, { businessName });
|
||||||
@@ -77,6 +133,31 @@ const decorateFailExcel = async (blob, businessName, failDetailDecorator) => {
|
|||||||
return new Blob([out], { type: 'application/vnd.ms-excel' });
|
return new Blob([out], { type: 'application/vnd.ms-excel' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 通知上传组件成功:只传业务响应体,避免其误调 axios 响应上的 .text() */
|
||||||
|
const notifyUploadSuccess = (option, res) => {
|
||||||
|
if (typeof option?.onSuccess !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const body = isBusinessJson(res?.data) ? res.data : { code: 200, success: true };
|
||||||
|
option.onSuccess(body);
|
||||||
|
} catch (callbackError) {
|
||||||
|
console.warn('upload onSuccess callback failed', callbackError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 通知上传组件失败 */
|
||||||
|
const notifyUploadError = (option, error) => {
|
||||||
|
if (typeof option?.onError !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
option.onError(error instanceof Error ? error : new Error(String(error?.message || error || '导入失败')));
|
||||||
|
} catch (callbackError) {
|
||||||
|
console.warn('upload onError callback failed', callbackError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开导入弹窗
|
* 打开导入弹窗
|
||||||
* @param {Object} vm 组件实例
|
* @param {Object} vm 组件实例
|
||||||
@@ -108,9 +189,7 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
|||||||
const fileName = file && file.name ? file.name.toLowerCase() : '';
|
const fileName = file && file.name ? file.name.toLowerCase() : '';
|
||||||
if (!excelPattern.test(fileName)) {
|
if (!excelPattern.test(fileName)) {
|
||||||
vm.$message.error('请上传 .xls,.xlsx 标准格式文件');
|
vm.$message.error('请上传 .xls,.xlsx 标准格式文件');
|
||||||
if (typeof option.onError === 'function') {
|
notifyUploadError(option, new Error('请上传 .xls,.xlsx 标准格式文件'));
|
||||||
option.onError(new Error('请上传 .xls,.xlsx 标准格式文件'));
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,12 +209,11 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
|||||||
);
|
);
|
||||||
vm.$message.warning('部分数据导入失败,已下载失败明细');
|
vm.$message.warning('部分数据导入失败,已下载失败明细');
|
||||||
} else {
|
} else {
|
||||||
const result = await parseBlobJson(res.data);
|
// 成功体常被 axios 拦截器提前解析成对象;优先走业务 JSON,避免再调 Blob.text
|
||||||
if (result.code !== 200) {
|
const result = isBusinessJson(res?.data) ? res.data : await parseBlobJson(res?.data);
|
||||||
|
if (Number(result.code) !== 200) {
|
||||||
vm.$message.error(result.msg || '导入失败');
|
vm.$message.error(result.msg || '导入失败');
|
||||||
if (typeof option.onError === 'function') {
|
notifyUploadError(option, new Error(result.msg || '导入失败'));
|
||||||
option.onError(new Error(result.msg || '导入失败'));
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
vm.$message.success('导入完成');
|
vm.$message.success('导入完成');
|
||||||
@@ -149,18 +227,17 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
|||||||
} else if (typeof vm.initTree === 'function') {
|
} else if (typeof vm.initTree === 'function') {
|
||||||
vm.initTree();
|
vm.initTree();
|
||||||
}
|
}
|
||||||
if (typeof option.onSuccess === 'function') {
|
// 上传组件 onSuccess 约定接收「响应体」,不是完整 axios 响应。
|
||||||
option.onSuccess(res);
|
// 若传入 axios 响应对象,部分环境下会按 Fetch Response 调用 .text(),
|
||||||
}
|
// 从而在导入已成功后仍弹出「e.text is not a function」。
|
||||||
|
notifyUploadSuccess(option, res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次,
|
// 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次,
|
||||||
// 只保留一次清晰的中文提示。
|
// 只保留一次清晰的中文提示。
|
||||||
if (!error?._notified) {
|
if (!error?._notified) {
|
||||||
vm.$message.error(await resolveErrorMessage(error));
|
vm.$message.error(await resolveErrorMessage(error));
|
||||||
}
|
}
|
||||||
if (typeof option.onError === 'function') {
|
notifyUploadError(option, error);
|
||||||
option.onError(error);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.close();
|
loading.close();
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
/** 地点搜索 v5/place/text 使用的「Web 服务」类型 Key */
|
||||||
|
export const AMAP_WEB_SERVICE_KEY = 'b43016a0d1f5b0c32211560392059708';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 高德地点关键词搜索(Web 服务 v5)。
|
||||||
|
* @see https://restapi.amap.com/v5/place/text
|
||||||
|
* @param {string} keyword 地点关键字
|
||||||
|
* @param {{ key?: string, pageSize?: number, pageNum?: number, region?: string, cityLimit?: boolean }} options
|
||||||
|
* @returns {Promise<Array>}
|
||||||
|
*/
|
||||||
|
export function searchAmapPlaceText(keyword, options = {}) {
|
||||||
|
const key = options.key || AMAP_WEB_SERVICE_KEY;
|
||||||
|
const pageSize = options.pageSize || 20;
|
||||||
|
const pageNum = options.pageNum || 1;
|
||||||
|
if (!key) {
|
||||||
|
return Promise.reject(new Error('高德地图 Key 未配置'));
|
||||||
|
}
|
||||||
|
const trimmed = String(keyword || '').trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return Promise.reject(new Error('请输入地址关键词'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
key,
|
||||||
|
keywords: trimmed,
|
||||||
|
page_size: String(pageSize),
|
||||||
|
page_num: String(pageNum),
|
||||||
|
show_fields: 'photos',
|
||||||
|
});
|
||||||
|
if (options.region) {
|
||||||
|
params.set('region', options.region);
|
||||||
|
}
|
||||||
|
if (options.cityLimit != null) {
|
||||||
|
params.set('city_limit', options.cityLimit ? 'true' : 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(`https://restapi.amap.com/v5/place/text?${params.toString()}`)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('高德地点搜索请求失败');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (String(data?.status) === '1') {
|
||||||
|
return Array.isArray(data.pois) ? data.pois : [];
|
||||||
|
}
|
||||||
|
const error = new Error(data?.info || '高德地点搜索失败');
|
||||||
|
error.code = data?.infocode;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将高德 Geocoder / place/text 结果规范化为地图选址浮层列表数据。
|
||||||
|
* @param {Array|Object} geocodesOrResult geocodes/pois 数组,或含 geocodes/pois/location 的结果对象
|
||||||
|
* @param {string} keyword 搜索关键词兜底文案
|
||||||
|
* @returns {Array<{id: string|number, name: string, address: string, location: any, photo: string}>}
|
||||||
|
*/
|
||||||
|
export function normalizeMapSearchResults(geocodesOrResult, keyword = '') {
|
||||||
|
const fallback = String(keyword || '').trim();
|
||||||
|
let list = [];
|
||||||
|
if (Array.isArray(geocodesOrResult)) {
|
||||||
|
list = geocodesOrResult;
|
||||||
|
} else if (geocodesOrResult && typeof geocodesOrResult === 'object') {
|
||||||
|
if (Array.isArray(geocodesOrResult.pois)) {
|
||||||
|
list = geocodesOrResult.pois;
|
||||||
|
} else if (Array.isArray(geocodesOrResult.geocodes)) {
|
||||||
|
list = geocodesOrResult.geocodes;
|
||||||
|
} else if (geocodesOrResult.location) {
|
||||||
|
list = [geocodesOrResult];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
.map((item, index) => {
|
||||||
|
if (!item) return null;
|
||||||
|
if (isPlacePoi(item)) {
|
||||||
|
const address = buildPoiAddress(item) || fallback || '-';
|
||||||
|
return {
|
||||||
|
id: item.id || index,
|
||||||
|
name: String(item.name || '').trim() || address,
|
||||||
|
address,
|
||||||
|
location: parseMapLocation(item.location),
|
||||||
|
photo: pickPoiPhoto(item),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildingName = pickNamedField(item?.building);
|
||||||
|
const neighborhoodName = pickNamedField(item?.neighborhood);
|
||||||
|
const address = item?.formattedAddress || item?.address || fallback || '-';
|
||||||
|
return {
|
||||||
|
id: item?.id || index,
|
||||||
|
name: buildingName || neighborhoodName || address,
|
||||||
|
address,
|
||||||
|
location: parseMapLocation(item?.location),
|
||||||
|
photo: item?.photo || item?.image || '',
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(item => item && item.location);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseMapLocation(location) {
|
||||||
|
if (!location) return null;
|
||||||
|
if (typeof location.getLng === 'function' && typeof location.getLat === 'function') {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
if (typeof location === 'string') {
|
||||||
|
const parts = location.split(',');
|
||||||
|
const lng = Number(parts[0]);
|
||||||
|
const lat = Number(parts[1]);
|
||||||
|
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null;
|
||||||
|
return { lng, lat };
|
||||||
|
}
|
||||||
|
if (Number.isFinite(Number(location.lng)) && Number.isFinite(Number(location.lat))) {
|
||||||
|
return { lng: Number(location.lng), lat: Number(location.lat) };
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
Number.isFinite(Number(location.longitude)) &&
|
||||||
|
Number.isFinite(Number(location.latitude))
|
||||||
|
) {
|
||||||
|
return { lng: Number(location.longitude), lat: Number(location.latitude) };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlacePoi(item) {
|
||||||
|
return Boolean(item && item.name && item.location && (item.id || item.typecode || item.adname));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPoiAddress(item) {
|
||||||
|
const detail = String(item.address || '').trim();
|
||||||
|
const region = [item.pname, item.cityname, item.adname]
|
||||||
|
.map(value => String(value || '').trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
// 直辖市等场景省市名可能重复
|
||||||
|
const uniqueRegion = region.filter((value, index) => region.indexOf(value) === index);
|
||||||
|
const prefix = uniqueRegion.join('');
|
||||||
|
if (prefix && detail) {
|
||||||
|
if (detail.startsWith(prefix)) return detail;
|
||||||
|
return `${prefix}${detail}`;
|
||||||
|
}
|
||||||
|
return detail || prefix || String(item.name || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickPoiPhoto(item) {
|
||||||
|
const photos = item?.photos;
|
||||||
|
if (Array.isArray(photos) && photos.length) {
|
||||||
|
return photos[0]?.url || '';
|
||||||
|
}
|
||||||
|
if (photos && typeof photos === 'object') {
|
||||||
|
return photos.url || '';
|
||||||
|
}
|
||||||
|
return item?.photo || item?.image || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickNamedField(value) {
|
||||||
|
if (!value) return '';
|
||||||
|
if (typeof value === 'string') return value.trim();
|
||||||
|
if (typeof value === 'object' && value.name) return String(value.name).trim();
|
||||||
|
return '';
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { getList as getMeasurementUnitList } from '@/api/base/measurement-unit';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载启用状态的计量单位选项,展示/取值均为「计量单位」字段(unitName)。
|
||||||
|
* @returns {Promise<Array<{ label: string, value: string, dimension: string, id: string|number }>>}
|
||||||
|
*/
|
||||||
|
export const loadMeasurementUnitOptions = () => {
|
||||||
|
return getMeasurementUnitList(1, 9999, { status: 1 }).then(res => {
|
||||||
|
const data = res?.data?.data || res?.data || {};
|
||||||
|
const records = Array.isArray(data) ? data : data.records || [];
|
||||||
|
const names = new Set();
|
||||||
|
return records
|
||||||
|
.filter(item => item.status === undefined || item.status === null || Number(item.status) === 1)
|
||||||
|
.map(item => {
|
||||||
|
const name = String(item.unitName || '').trim();
|
||||||
|
if (!name || names.has(name)) return null;
|
||||||
|
names.add(name);
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
label: name,
|
||||||
|
value: name,
|
||||||
|
dimension: String(item.dimension || '').trim(),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
});
|
||||||
|
};
|
||||||
+86
-15
@@ -1,5 +1,5 @@
|
|||||||
import { getDictionary } from '@/api/system/dictbiz';
|
import { getDictionary } from '@/api/system/dictbiz';
|
||||||
import { processDelete, processSubmit } from '@/api/system/business-process';
|
import { hasMkProcessInstance, processDelete, processSubmit } from '@/api/system/business-process';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
export const MK_BIZ = {
|
export const MK_BIZ = {
|
||||||
@@ -7,43 +7,80 @@ export const MK_BIZ = {
|
|||||||
dictName: '提交客商审核流',
|
dictName: '提交客商审核流',
|
||||||
subjectPrefix: '客商准入审批',
|
subjectPrefix: '客商准入审批',
|
||||||
rejected: ['rejected'],
|
rejected: ['rejected'],
|
||||||
|
approved: ['approved'],
|
||||||
},
|
},
|
||||||
'project-apply': {
|
'project-apply': {
|
||||||
dictName: '提交项目审核流',
|
dictName: '提交项目审核流',
|
||||||
subjectPrefix: '项目审批',
|
subjectPrefix: '项目审批',
|
||||||
rejected: ['rejected', 'change_rejected', 'withdrawn'],
|
rejected: ['rejected', 'change_rejected', 'withdrawn'],
|
||||||
|
approved: ['approved', 'change_approved'],
|
||||||
},
|
},
|
||||||
'contract-manage': {
|
'contract-manage': {
|
||||||
dictName: '提交合同审核流',
|
dictName: '提交合同审核流',
|
||||||
subjectPrefix: '合同审批',
|
subjectPrefix: '合同审批',
|
||||||
rejected: ['rejected', 'change_rejected', 'withdrawn'],
|
rejected: ['rejected', 'change_rejected', 'withdrawn'],
|
||||||
},
|
approved: ['approved', 'change_approved'],
|
||||||
'waybill-manage': {
|
|
||||||
dictName: '提交运单审核流',
|
|
||||||
subjectPrefix: '运单审批',
|
|
||||||
rejected: ['rejected', 'returned'],
|
|
||||||
},
|
},
|
||||||
'pre-settlement': {
|
'pre-settlement': {
|
||||||
dictName: '提交预结算审核流',
|
dictName: '提交预结算审核流',
|
||||||
subjectPrefix: '预结算审批',
|
subjectPrefix: '预结算审批',
|
||||||
rejected: ['returned'],
|
rejected: ['returned'],
|
||||||
|
approved: ['approved'],
|
||||||
},
|
},
|
||||||
'formal-settlement': {
|
'formal-settlement': {
|
||||||
dictName: '提交正式结算审核流',
|
dictName: '提交正式结算审核流',
|
||||||
subjectPrefix: '正式结算审批',
|
subjectPrefix: '正式结算审批',
|
||||||
rejected: ['returned'],
|
rejected: ['returned'],
|
||||||
|
approved: ['approved'],
|
||||||
},
|
},
|
||||||
'payment-application': {
|
'payment-application': {
|
||||||
dictName: '提交付款审核流',
|
dictName: '提交付款审核流',
|
||||||
subjectPrefix: '付款审批',
|
subjectPrefix: '付款审批',
|
||||||
rejected: ['returned'],
|
rejected: ['returned'],
|
||||||
|
approved: ['approved'],
|
||||||
|
},
|
||||||
|
'invoice-application': {
|
||||||
|
dictName: '提交开票审核流',
|
||||||
|
subjectPrefix: '开票审批',
|
||||||
|
rejected: ['returned'],
|
||||||
|
approved: ['approved'],
|
||||||
|
},
|
||||||
|
'invoice-receipt': {
|
||||||
|
dictName: '提交收票审核流',
|
||||||
|
subjectPrefix: '收票审批',
|
||||||
|
rejected: ['returned'],
|
||||||
|
approved: ['approved'],
|
||||||
|
},
|
||||||
|
'bill-payment': {
|
||||||
|
dictName: '提交汇票付款审核流',
|
||||||
|
subjectPrefix: '汇票付款审批',
|
||||||
|
rejected: ['returned'],
|
||||||
|
approved: ['approved'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function resolveMkTemplateCode(dictName) {
|
const MK_SWITCH_NAME = '开启MK';
|
||||||
|
|
||||||
|
async function loadMkTemplateList() {
|
||||||
const res = await getDictionary({ code: 'mk_template' });
|
const res = await getDictionary({ code: 'mk_template' });
|
||||||
const list = res?.data?.data || [];
|
return res?.data?.data || [];
|
||||||
const matched = list.find(item => String(item.dictValue || '').trim() === dictName);
|
}
|
||||||
|
|
||||||
|
/** 业务字典 mk_template:名称「开启MK」且键值为 1 时才请求 MK */
|
||||||
|
export function isMkEnabled(list = []) {
|
||||||
|
const matched = list.find(item => String(item.dictValue || '').trim() === MK_SWITCH_NAME);
|
||||||
|
return String(matched?.dictKey ?? '').trim() === '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载业务字典并判断是否开启 MK */
|
||||||
|
export async function loadMkEnabled() {
|
||||||
|
const list = await loadMkTemplateList();
|
||||||
|
return isMkEnabled(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveMkTemplateCode(dictName, list) {
|
||||||
|
const dictList = list || (await loadMkTemplateList());
|
||||||
|
const matched = dictList.find(item => String(item.dictValue || '').trim() === dictName);
|
||||||
const templateCode = matched?.dictKey;
|
const templateCode = matched?.dictKey;
|
||||||
if (!templateCode) {
|
if (!templateCode) {
|
||||||
ElMessage.warning(`未配置业务字典 mk_template「${dictName}」,无法提交审核流`);
|
ElMessage.warning(`未配置业务字典 mk_template「${dictName}」,无法提交审核流`);
|
||||||
@@ -52,9 +89,26 @@ export async function resolveMkTemplateCode(dictName) {
|
|||||||
return String(templateCode);
|
return String(templateCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否属于「审核通过后再提交」需要新建表单实例的状态
|
||||||
|
*/
|
||||||
|
export function isMkApprovedStatus(bizType, approvalStatus) {
|
||||||
|
const conf = MK_BIZ[bizType];
|
||||||
|
if (!conf) return false;
|
||||||
|
return (conf.approved || []).includes(approvalStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交 MK 审核流
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {string} options.bizType 业务类型
|
||||||
|
* @param {string|number} options.bizId 业务主键(后端据此生成/复用 formInstanceId)
|
||||||
|
* @param {string} [options.subjectName] 标题后缀
|
||||||
|
* @param {string} [options.approvalStatus] 提交前的审批状态(须在业务 submit 改状态前取值;通过态强制新建实例)
|
||||||
|
*/
|
||||||
export async function submitMkApprovalFlow({
|
export async function submitMkApprovalFlow({
|
||||||
bizType,
|
bizType,
|
||||||
formInstanceId,
|
bizId,
|
||||||
subjectName = '',
|
subjectName = '',
|
||||||
approvalStatus = '',
|
approvalStatus = '',
|
||||||
}) {
|
}) {
|
||||||
@@ -62,17 +116,34 @@ export async function submitMkApprovalFlow({
|
|||||||
if (!conf) {
|
if (!conf) {
|
||||||
return Promise.reject(new Error(`不支持的MK业务类型:${bizType}`));
|
return Promise.reject(new Error(`不支持的MK业务类型:${bizType}`));
|
||||||
}
|
}
|
||||||
if ((conf.rejected || []).includes(approvalStatus)) {
|
if (bizId == null || bizId === '') {
|
||||||
await processDelete({ formInstanceId: String(formInstanceId) });
|
return Promise.reject(new Error('业务id不能为空'));
|
||||||
}
|
}
|
||||||
const templateCode = await resolveMkTemplateCode(conf.dictName);
|
const list = await loadMkTemplateList();
|
||||||
|
if (!isMkEnabled(list)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bizIdText = String(bizId);
|
||||||
|
const forceNewFormInstance = isMkApprovedStatus(bizType, approvalStatus);
|
||||||
|
// 审核通过后再提交必须新建流程;其余场景已有有效实例则跳过
|
||||||
|
if (!forceNewFormInstance) {
|
||||||
|
const existsRes = await hasMkProcessInstance({ bizId: bizIdText });
|
||||||
|
if (existsRes?.data?.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((conf.rejected || []).includes(approvalStatus)) {
|
||||||
|
await processDelete({ bizId: bizIdText });
|
||||||
|
}
|
||||||
|
const templateCode = await resolveMkTemplateCode(conf.dictName, list);
|
||||||
const subject = subjectName
|
const subject = subjectName
|
||||||
? `${conf.subjectPrefix}:${subjectName}`
|
? `${conf.subjectPrefix}:${subjectName}`
|
||||||
: `${conf.subjectPrefix}:${formInstanceId}`;
|
: `${conf.subjectPrefix}:${bizIdText}`;
|
||||||
return processSubmit({
|
return processSubmit({
|
||||||
templateCode,
|
templateCode,
|
||||||
formInstanceId: String(formInstanceId),
|
bizId: bizIdText,
|
||||||
subject,
|
subject,
|
||||||
bizType,
|
bizType,
|
||||||
|
forceNewFormInstance,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -283,3 +283,31 @@ export function validatejson(val) {
|
|||||||
// 非对象、非数组、非字符串,或者字符串不是 JSON
|
// 非对象、非数组、非字符串,或者字符串不是 JSON
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 登录密码规则提示 */
|
||||||
|
export const LOGIN_PASSWORD_RULE_MESSAGE =
|
||||||
|
'密码须大于8位,且同时包含字母、数字和特殊字符(.!@#$%^&*)';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验登录密码强度:大于8位,同时包含字母、数字、特殊字符(.!@#$%^&*)
|
||||||
|
* @param {string} password
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isValidLoginPassword(password) {
|
||||||
|
return /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.!@#$%^&*]).{9,}$/.test(String(password || ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Element Plus / Avue 表单校验器:登录密码强度
|
||||||
|
*/
|
||||||
|
export function validateLoginPassword(rule, value, callback) {
|
||||||
|
if (value === undefined || value === null || String(value).trim() === '') {
|
||||||
|
callback(new Error('请输入登录密码'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isValidLoginPassword(value)) {
|
||||||
|
callback(new Error(LOGIN_PASSWORD_RULE_MESSAGE));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
type="danger"
|
type="danger"
|
||||||
icon="el-icon-delete"
|
icon="el-icon-delete"
|
||||||
plain
|
plain
|
||||||
v-if="userInfo.authority.includes('admin')"
|
v-if="isAdministrator"
|
||||||
@click="handleDelete"
|
@click="handleDelete"
|
||||||
>删除
|
>删除
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -43,19 +43,19 @@
|
|||||||
<template #menu="{ row }">
|
<template #menu="{ row }">
|
||||||
<el-link
|
<el-link
|
||||||
type="primary"
|
type="primary"
|
||||||
v-if="row.status === -1 && userInfo.authority.includes('admin')"
|
v-if="row.status === -1 && isAdministrator"
|
||||||
@click.stop="handleEnable(row)"
|
@click.stop="handleEnable(row)"
|
||||||
>启用
|
>启用
|
||||||
</el-link>
|
</el-link>
|
||||||
<el-link
|
<el-link
|
||||||
type="primary"
|
type="primary"
|
||||||
v-if="row.status === 1 && userInfo.authority.includes('admin')"
|
v-if="row.status === 1 && isAdministrator"
|
||||||
@click.stop="handleDisable(row)"
|
@click.stop="handleDisable(row)"
|
||||||
>禁用
|
>禁用
|
||||||
</el-link>
|
</el-link>
|
||||||
<el-link
|
<el-link
|
||||||
type="primary"
|
type="primary"
|
||||||
v-if="userInfo.authority.includes('admin')"
|
v-if="isAdministrator"
|
||||||
@click.stop="handleRowLog(row)"
|
@click.stop="handleRowLog(row)"
|
||||||
>日志
|
>日志
|
||||||
</el-link>
|
</el-link>
|
||||||
@@ -522,7 +522,7 @@ sortable: true, span: 24, width: 180 },
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['userInfo']),
|
...mapGetters(['userInfo', 'isAdministrator']),
|
||||||
ids() {
|
ids() {
|
||||||
let ids = [];
|
let ids = [];
|
||||||
this.selectionList.forEach(ele => {
|
this.selectionList.forEach(ele => {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
type="danger"
|
type="danger"
|
||||||
icon="el-icon-delete"
|
icon="el-icon-delete"
|
||||||
plain
|
plain
|
||||||
v-if="userInfo.authority.includes('administrator')"
|
v-if="isAdministrator"
|
||||||
@click="handleDelete"
|
@click="handleDelete"
|
||||||
>删除
|
>删除
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -54,19 +54,19 @@
|
|||||||
<template #menu="{ row }">
|
<template #menu="{ row }">
|
||||||
<el-link
|
<el-link
|
||||||
type="primary"
|
type="primary"
|
||||||
v-if="row.status !== 1 && userInfo.authority.includes('administrator')"
|
v-if="row.status !== 1 && isAdministrator"
|
||||||
@click.stop="handleEnable(row)"
|
@click.stop="handleEnable(row)"
|
||||||
>启用
|
>启用
|
||||||
</el-link>
|
</el-link>
|
||||||
<el-link
|
<el-link
|
||||||
type="primary"
|
type="primary"
|
||||||
v-if="row.status === 1 && userInfo.authority.includes('administrator')"
|
v-if="row.status === 1 && isAdministrator"
|
||||||
@click.stop="handleDisable(row)"
|
@click.stop="handleDisable(row)"
|
||||||
>禁用
|
>禁用
|
||||||
</el-link>
|
</el-link>
|
||||||
<el-link
|
<el-link
|
||||||
type="primary"
|
type="primary"
|
||||||
v-if="userInfo.authority.includes('administrator')"
|
v-if="isAdministrator"
|
||||||
@click.stop="handleRowLog(row)"
|
@click.stop="handleRowLog(row)"
|
||||||
>日志
|
>日志
|
||||||
</el-link>
|
</el-link>
|
||||||
@@ -401,7 +401,7 @@ sortable: true, span: 24, width: 170 },
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['userInfo']),
|
...mapGetters(['userInfo', 'isAdministrator']),
|
||||||
ids() {
|
ids() {
|
||||||
let ids = [];
|
let ids = [];
|
||||||
this.selectionList.forEach(ele => {
|
this.selectionList.forEach(ele => {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<el-button
|
<el-button
|
||||||
icon="el-icon-setting"
|
icon="el-icon-setting"
|
||||||
@click="handleRole"
|
@click="handleRole"
|
||||||
v-if="userInfo.authority.includes('admin')"
|
v-if="isAdministrator"
|
||||||
plain
|
plain
|
||||||
>权限设置
|
>权限设置
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<template #menu="{ row }">
|
<template #menu="{ row }">
|
||||||
<el-link
|
<el-link
|
||||||
type="primary"
|
type="primary"
|
||||||
v-if="userInfo.authority.includes('admin')"
|
v-if="isAdministrator"
|
||||||
style="border: 0; background-color: transparent !important"
|
style="border: 0; background-color: transparent !important"
|
||||||
@click.stop="handleRowRole(row)"
|
@click.stop="handleRowRole(row)"
|
||||||
>权限设置
|
>权限设置
|
||||||
@@ -354,7 +354,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['userInfo', 'permission']),
|
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
|
||||||
permissionList() {
|
permissionList() {
|
||||||
return {
|
return {
|
||||||
addBtn: this.validData(this.permission.role_add, false),
|
addBtn: this.validData(this.permission.role_add, false),
|
||||||
|
|||||||
@@ -94,6 +94,17 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
|
<template #detailAddress-form>
|
||||||
|
<el-input
|
||||||
|
v-model="form.detailAddress"
|
||||||
|
class="detail-address-input"
|
||||||
|
readonly
|
||||||
|
maxlength="255"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="点击选择地图地址"
|
||||||
|
@click="openAddressMapPicker"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<template #longitude-form>
|
<template #longitude-form>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.longitude"
|
v-model="form.longitude"
|
||||||
@@ -121,19 +132,27 @@
|
|||||||
</template>
|
</template>
|
||||||
</avue-form>
|
</avue-form>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
<address-map-picker
|
||||||
|
v-model="addressMapPickerVisible"
|
||||||
|
:address="form.detailAddress"
|
||||||
|
:longitude="form.longitude"
|
||||||
|
:latitude="form.latitude"
|
||||||
|
@confirm="handleAddressMapConfirm"
|
||||||
|
/>
|
||||||
</basic-container>
|
</basic-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/airport-master';
|
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/airport-master';
|
||||||
import { getLazyTree } from '@/api/base/region';
|
import { getDetail as getRegionDetail, getLazyTree } from '@/api/base/region';
|
||||||
import { exportBlob } from '@/api/common';
|
import { exportBlob } from '@/api/common';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
import { getToken } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
||||||
|
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||||
import NProgress from 'nprogress';
|
import NProgress from 'nprogress';
|
||||||
import 'nprogress/nprogress.css';
|
import 'nprogress/nprogress.css';
|
||||||
|
|
||||||
@@ -181,6 +200,9 @@ const addExcelRequiredHeaderMarks = async blob => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
AddressMapPicker,
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
const validateIataCode = (rule, value, callback) => {
|
const validateIataCode = (rule, value, callback) => {
|
||||||
if (!/^[A-Z]{3}$/.test(String(value || '').toUpperCase())) {
|
if (!/^[A-Z]{3}$/.test(String(value || '').toUpperCase())) {
|
||||||
@@ -220,6 +242,7 @@ export default {
|
|||||||
loading: true,
|
loading: true,
|
||||||
data: [],
|
data: [],
|
||||||
excelBox: false,
|
excelBox: false,
|
||||||
|
addressMapPickerVisible: false,
|
||||||
excelForm: {},
|
excelForm: {},
|
||||||
provinceOptions: [],
|
provinceOptions: [],
|
||||||
cityOptions: [],
|
cityOptions: [],
|
||||||
@@ -299,6 +322,20 @@ export default {
|
|||||||
prop: 'shortName',
|
prop: 'shortName',
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '详细地址',
|
||||||
|
prop: 'detailAddress',
|
||||||
|
formslot: true,
|
||||||
|
span: 24,
|
||||||
|
minWidth: 220,
|
||||||
|
overHidden: false,
|
||||||
|
maxlength: 255,
|
||||||
|
showWordLimit: true,
|
||||||
|
rules: [
|
||||||
|
{ required: true, message: '请选择详细地址', trigger: 'change' },
|
||||||
|
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '所属省份',
|
label: '所属省份',
|
||||||
prop: 'provinceCode',
|
prop: 'provinceCode',
|
||||||
@@ -365,21 +402,6 @@ export default {
|
|||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: '详细地址',
|
|
||||||
prop: 'detailAddress',
|
|
||||||
type: 'textarea',
|
|
||||||
minRows: 2,
|
|
||||||
span: 24,
|
|
||||||
minWidth: 220,
|
|
||||||
overHidden: false,
|
|
||||||
maxlength: 255,
|
|
||||||
showWordLimit: true,
|
|
||||||
rules: [
|
|
||||||
{ required: true, message: '请输入详细地址', trigger: 'blur' },
|
|
||||||
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: '经度',
|
label: '经度',
|
||||||
prop: 'longitude',
|
prop: 'longitude',
|
||||||
@@ -438,6 +460,13 @@ export default {
|
|||||||
showWordLimit: true,
|
showWordLimit: true,
|
||||||
rules: [{ max: 200, message: '备注不能超过200字', trigger: 'blur' }],
|
rules: [{ max: 200, message: '备注不能超过200字', trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '更新人',
|
label: '更新人',
|
||||||
prop: 'updateUserName',
|
prop: 'updateUserName',
|
||||||
@@ -526,10 +555,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
isAdmin() {
|
isAdmin() {
|
||||||
const authority = this.userInfo && this.userInfo.authority;
|
return isAdministrator(this.userInfo);
|
||||||
return Array.isArray(authority)
|
|
||||||
? authority.includes('admin')
|
|
||||||
: String(authority || '').includes('admin');
|
|
||||||
},
|
},
|
||||||
ids() {
|
ids() {
|
||||||
let ids = [];
|
let ids = [];
|
||||||
@@ -552,33 +578,51 @@ export default {
|
|||||||
return targetColumns.find(item => item.prop === targetProp) || null;
|
return targetColumns.find(item => item.prop === targetProp) || null;
|
||||||
},
|
},
|
||||||
loadProvinceOptions() {
|
loadProvinceOptions() {
|
||||||
getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
|
return getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
|
||||||
this.provinceOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
this.provinceOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||||
this.findColumn('provinceCode').dicData = this.provinceOptions;
|
const provinceColumn = this.findColumn('provinceCode');
|
||||||
|
if (provinceColumn) {
|
||||||
|
provinceColumn.dicData = this.provinceOptions;
|
||||||
|
}
|
||||||
|
return this.provinceOptions;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
loadCityOptions(provinceCode) {
|
loadCityOptions(provinceCode) {
|
||||||
if (!provinceCode) {
|
if (!provinceCode) {
|
||||||
this.cityOptions = [];
|
this.cityOptions = [];
|
||||||
this.findColumn('cityCode').dicData = [];
|
const cityColumn = this.findColumn('cityCode');
|
||||||
return Promise.resolve();
|
if (cityColumn) {
|
||||||
|
cityColumn.dicData = [];
|
||||||
|
}
|
||||||
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
return getLazyTree(provinceCode).then(res => {
|
return getLazyTree(provinceCode).then(res => {
|
||||||
this.cityOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
this.cityOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||||
this.findColumn('cityCode').dicData = this.cityOptions;
|
const cityColumn = this.findColumn('cityCode');
|
||||||
|
if (cityColumn) {
|
||||||
|
cityColumn.dicData = this.cityOptions;
|
||||||
|
}
|
||||||
|
return this.cityOptions;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
loadDistrictOptions(cityCode) {
|
loadDistrictOptions(cityCode) {
|
||||||
if (!cityCode) {
|
if (!cityCode) {
|
||||||
this.districtOptions = [];
|
this.districtOptions = [];
|
||||||
this.findColumn('districtCode').dicData = [];
|
const districtColumn = this.findColumn('districtCode');
|
||||||
return Promise.resolve();
|
if (districtColumn) {
|
||||||
|
districtColumn.dicData = [];
|
||||||
|
}
|
||||||
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
this.districtLoading = true;
|
this.districtLoading = true;
|
||||||
return getLazyTree(cityCode)
|
return getLazyTree(cityCode)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.districtOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
this.districtOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||||
this.findColumn('districtCode').dicData = this.districtOptions;
|
const districtColumn = this.findColumn('districtCode');
|
||||||
|
if (districtColumn) {
|
||||||
|
districtColumn.dicData = this.districtOptions;
|
||||||
|
}
|
||||||
|
return this.districtOptions;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.districtLoading = false;
|
this.districtLoading = false;
|
||||||
@@ -605,6 +649,22 @@ export default {
|
|||||||
: String(row.districtCode),
|
: String(row.districtCode),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
ensureCityOption(row) {
|
||||||
|
if (!row.cityCode || !row.cityName) return;
|
||||||
|
const exists = this.cityOptions.some(item => item.id === row.cityCode);
|
||||||
|
if (exists) return;
|
||||||
|
this.cityOptions = [
|
||||||
|
...this.cityOptions,
|
||||||
|
{
|
||||||
|
id: row.cityCode,
|
||||||
|
title: row.cityName,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const cityColumn = this.findColumn('cityCode');
|
||||||
|
if (cityColumn) {
|
||||||
|
cityColumn.dicData = this.cityOptions;
|
||||||
|
}
|
||||||
|
},
|
||||||
ensureDistrictOption(row) {
|
ensureDistrictOption(row) {
|
||||||
if (!row.districtCode || !row.districtName) return;
|
if (!row.districtCode || !row.districtName) return;
|
||||||
const exists = this.districtOptions.some(item => item.id === row.districtCode);
|
const exists = this.districtOptions.some(item => item.id === row.districtCode);
|
||||||
@@ -616,7 +676,10 @@ export default {
|
|||||||
title: row.districtName,
|
title: row.districtName,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
this.findColumn('districtCode').dicData = this.districtOptions;
|
const districtColumn = this.findColumn('districtCode');
|
||||||
|
if (districtColumn) {
|
||||||
|
districtColumn.dicData = this.districtOptions;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
handleIataChange(value) {
|
handleIataChange(value) {
|
||||||
this.form.iataCode = String(value || '')
|
this.form.iataCode = String(value || '')
|
||||||
@@ -669,6 +732,269 @@ export default {
|
|||||||
handleCoordinateInput(prop, value) {
|
handleCoordinateInput(prop, value) {
|
||||||
this.form[prop] = normalizeCoordinateInput(value);
|
this.form[prop] = normalizeCoordinateInput(value);
|
||||||
},
|
},
|
||||||
|
openAddressMapPicker() {
|
||||||
|
this.addressMapPickerVisible = true;
|
||||||
|
},
|
||||||
|
formatMapCoordinate(value) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const number = Number(value);
|
||||||
|
if (!Number.isFinite(number)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return normalizeCoordinateInput(number.toFixed(6));
|
||||||
|
},
|
||||||
|
patchDialogForm(patch = {}) {
|
||||||
|
Object.assign(this.form, patch);
|
||||||
|
const tableForm = this.$refs.crud?.tableForm;
|
||||||
|
if (tableForm && tableForm !== this.form) {
|
||||||
|
Object.assign(tableForm, patch);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleAddressMapConfirm(payload) {
|
||||||
|
const selection = typeof payload === 'string' ? { address: payload } : payload || {};
|
||||||
|
const longitude = this.formatMapCoordinate(selection.longitude);
|
||||||
|
const latitude = this.formatMapCoordinate(selection.latitude);
|
||||||
|
const patch = {};
|
||||||
|
if (selection.address) {
|
||||||
|
patch.detailAddress = selection.address;
|
||||||
|
}
|
||||||
|
if (longitude !== '' && latitude !== '') {
|
||||||
|
patch.longitude = longitude;
|
||||||
|
patch.latitude = latitude;
|
||||||
|
}
|
||||||
|
if (Object.keys(patch).length) {
|
||||||
|
this.patchDialogForm(patch);
|
||||||
|
}
|
||||||
|
this.applyMapRegionSelection(selection);
|
||||||
|
},
|
||||||
|
normalizeAmapCityName(city, province) {
|
||||||
|
if (Array.isArray(city)) {
|
||||||
|
return String(province || '').trim();
|
||||||
|
}
|
||||||
|
const cityName = String(city || '').trim();
|
||||||
|
return cityName || String(province || '').trim();
|
||||||
|
},
|
||||||
|
buildRegionLookupCodes(adcode) {
|
||||||
|
const code = String(adcode || '').trim();
|
||||||
|
if (!code) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const codes = [code];
|
||||||
|
if (/^\d{6}$/.test(code)) {
|
||||||
|
if (code.endsWith('00')) {
|
||||||
|
const cityCode = code.slice(0, 4);
|
||||||
|
if (cityCode && !codes.includes(cityCode)) {
|
||||||
|
codes.push(cityCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (code.endsWith('0000')) {
|
||||||
|
const provinceCode = code.slice(0, 2);
|
||||||
|
if (provinceCode && !codes.includes(provinceCode)) {
|
||||||
|
codes.push(provinceCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return codes;
|
||||||
|
},
|
||||||
|
fetchRegionDetailByCodes(codes = []) {
|
||||||
|
const tryNext = index => {
|
||||||
|
if (index >= codes.length) {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
return getRegionDetail(codes[index])
|
||||||
|
.then(res => {
|
||||||
|
const detail = res.data && res.data.data;
|
||||||
|
if (detail && detail.code) {
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
return tryNext(index + 1);
|
||||||
|
})
|
||||||
|
.catch(() => tryNext(index + 1));
|
||||||
|
};
|
||||||
|
return tryNext(0);
|
||||||
|
},
|
||||||
|
resolveProvinceCode(provinceName) {
|
||||||
|
const name = String(provinceName || '').trim();
|
||||||
|
if (!name) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const province = this.provinceOptions.find(
|
||||||
|
item => item.title === name || item.id === name || String(item.title).includes(name)
|
||||||
|
);
|
||||||
|
return province ? province.id : '';
|
||||||
|
},
|
||||||
|
resolveCityCode(cityName, regionCode) {
|
||||||
|
const name = String(cityName || '').trim();
|
||||||
|
const code = String(regionCode || '').trim();
|
||||||
|
const city = this.cityOptions.find(
|
||||||
|
item => item.title === name || item.id === code || item.id === name
|
||||||
|
);
|
||||||
|
return city ? city.id : '';
|
||||||
|
},
|
||||||
|
resolveDistrictCode(districtName, regionCode) {
|
||||||
|
const name = String(districtName || '').trim();
|
||||||
|
const code = String(regionCode || '').trim();
|
||||||
|
const district = this.districtOptions.find(
|
||||||
|
item => item.title === name || item.id === code || item.id === name
|
||||||
|
);
|
||||||
|
return district ? district.id : code || '';
|
||||||
|
},
|
||||||
|
applyRegionDetail(detail = {}) {
|
||||||
|
const provinceName = detail.provinceName || '';
|
||||||
|
const cityName = detail.cityName || '';
|
||||||
|
const districtName = detail.districtName || detail.name || '';
|
||||||
|
return this.loadProvinceOptions()
|
||||||
|
.then(() => {
|
||||||
|
const provinceCode =
|
||||||
|
String(detail.provinceCode || '').trim() || this.resolveProvinceCode(provinceName);
|
||||||
|
const province = this.provinceOptions.find(item => item.id === provinceCode);
|
||||||
|
this.patchDialogForm({
|
||||||
|
provinceCode: provinceCode || undefined,
|
||||||
|
provinceName: province ? province.title : provinceName,
|
||||||
|
});
|
||||||
|
if (!provinceCode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.loadCityOptions(provinceCode);
|
||||||
|
})
|
||||||
|
.then(cities => {
|
||||||
|
if (!cities) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const cityCode =
|
||||||
|
String(detail.cityCode || '').trim() ||
|
||||||
|
this.resolveCityCode(cityName, detail.districtCode || detail.code);
|
||||||
|
const city = this.cityOptions.find(item => item.id === cityCode);
|
||||||
|
this.patchDialogForm({
|
||||||
|
cityCode: cityCode || undefined,
|
||||||
|
cityName: city ? city.title : cityName,
|
||||||
|
});
|
||||||
|
this.ensureCityOption({
|
||||||
|
cityCode,
|
||||||
|
cityName: city ? city.title : cityName,
|
||||||
|
});
|
||||||
|
if (!cityCode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.loadDistrictOptions(cityCode);
|
||||||
|
})
|
||||||
|
.then(districts => {
|
||||||
|
if (!districts) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const districtCode = this.resolveDistrictCode(
|
||||||
|
districtName,
|
||||||
|
detail.districtCode || detail.code
|
||||||
|
);
|
||||||
|
this.patchDialogForm({
|
||||||
|
districtCode: districtCode || undefined,
|
||||||
|
});
|
||||||
|
this.handleDistrictChange(districtCode);
|
||||||
|
this.ensureDistrictOption({
|
||||||
|
districtCode,
|
||||||
|
districtName: this.form.districtName || districtName,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
applyMapRegionByNames(selection = {}) {
|
||||||
|
const provinceName = String(selection.province || selection.provinceName || '').trim();
|
||||||
|
const cityName = this.normalizeAmapCityName(
|
||||||
|
selection.city || selection.cityName,
|
||||||
|
provinceName
|
||||||
|
);
|
||||||
|
const districtName = String(selection.district || selection.districtName || '').trim();
|
||||||
|
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||||
|
// 本页无国家字段:国内 adcode 走中国省份树;无匹配则不强制回填
|
||||||
|
if (
|
||||||
|
!provinceName &&
|
||||||
|
!cityName &&
|
||||||
|
!districtName &&
|
||||||
|
!(/^\d{6}$/.test(adcode) || String(selection.country || selection.countryName || '').includes('中国'))
|
||||||
|
) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return this.loadProvinceOptions()
|
||||||
|
.then(() => {
|
||||||
|
const provinceCode = this.resolveProvinceCode(provinceName);
|
||||||
|
const province = this.provinceOptions.find(item => item.id === provinceCode);
|
||||||
|
this.patchDialogForm({
|
||||||
|
provinceCode: provinceCode || undefined,
|
||||||
|
provinceName: province ? province.title : provinceName,
|
||||||
|
});
|
||||||
|
if (!provinceCode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.loadCityOptions(provinceCode);
|
||||||
|
})
|
||||||
|
.then(cities => {
|
||||||
|
if (!cities) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const cityCode = this.resolveCityCode(cityName, adcode);
|
||||||
|
const city = this.cityOptions.find(item => item.id === cityCode);
|
||||||
|
this.patchDialogForm({
|
||||||
|
cityCode: cityCode || undefined,
|
||||||
|
cityName: city ? city.title : cityName,
|
||||||
|
});
|
||||||
|
this.ensureCityOption({
|
||||||
|
cityCode,
|
||||||
|
cityName: city ? city.title : cityName,
|
||||||
|
});
|
||||||
|
if (!cityCode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.loadDistrictOptions(cityCode);
|
||||||
|
})
|
||||||
|
.then(districts => {
|
||||||
|
if (!districts) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const districtCode = this.resolveDistrictCode(districtName, adcode);
|
||||||
|
this.patchDialogForm({
|
||||||
|
districtCode: districtCode || undefined,
|
||||||
|
});
|
||||||
|
this.handleDistrictChange(districtCode);
|
||||||
|
this.ensureDistrictOption({
|
||||||
|
districtCode,
|
||||||
|
districtName: this.form.districtName || districtName,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
applyMapRegionSelection(selection = {}) {
|
||||||
|
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||||
|
const hasNameHint = Boolean(
|
||||||
|
selection.country ||
|
||||||
|
selection.countryName ||
|
||||||
|
selection.province ||
|
||||||
|
selection.provinceName ||
|
||||||
|
selection.city ||
|
||||||
|
selection.cityName ||
|
||||||
|
selection.district ||
|
||||||
|
selection.districtName
|
||||||
|
);
|
||||||
|
if (!adcode && !hasNameHint) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
this.regionInitializing = true;
|
||||||
|
const lookupCodes = this.buildRegionLookupCodes(adcode);
|
||||||
|
return this.fetchRegionDetailByCodes(lookupCodes)
|
||||||
|
.then(detail => {
|
||||||
|
if (detail) {
|
||||||
|
return this.applyRegionDetail(detail);
|
||||||
|
}
|
||||||
|
return this.applyMapRegionByNames(selection);
|
||||||
|
})
|
||||||
|
.catch(() => this.applyMapRegionByNames(selection))
|
||||||
|
.finally(() => {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.regionInitializing = false;
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
normalizeRow(row) {
|
normalizeRow(row) {
|
||||||
row.code = row.iataCode ? `JC-${row.iataCode}` : row.code;
|
row.code = row.iataCode ? `JC-${row.iataCode}` : row.code;
|
||||||
row.regionCode = String(row.regionCode || '').trim();
|
row.regionCode = String(row.regionCode || '').trim();
|
||||||
@@ -917,4 +1243,13 @@ export default {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-address-input {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper),
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -182,8 +182,8 @@
|
|||||||
<el-input
|
<el-input
|
||||||
v-model="form.regionName"
|
v-model="form.regionName"
|
||||||
readonly
|
readonly
|
||||||
|
disabled
|
||||||
placeholder="行政区划自动带出"
|
placeholder="行政区划自动带出"
|
||||||
:disabled="dialogReadonly"
|
|
||||||
/>
|
/>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.detailAddress"
|
v-model="form.detailAddress"
|
||||||
@@ -233,8 +233,8 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="map-picker-content">
|
<div class="map-picker-content">
|
||||||
<map-search-results :results="mapSearchResults" @select="selectMapSearchResult" />
|
|
||||||
<div ref="amap" class="common-address-page__map"></div>
|
<div ref="amap" class="common-address-page__map"></div>
|
||||||
|
<map-search-results :results="mapSearchResults" @select="selectMapSearchResult" />
|
||||||
</div>
|
</div>
|
||||||
<div class="common-address-page__map-info">
|
<div class="common-address-page__map-info">
|
||||||
<span>{{ mapStatus }}</span>
|
<span>{{ mapStatus }}</span>
|
||||||
@@ -269,7 +269,8 @@ import { exportBlob } from '@/api/common';
|
|||||||
import { createOption } from '@/option/base/common-address';
|
import { createOption } from '@/option/base/common-address';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import { getToken } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
|
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||||
import { isMobile } from '@/utils/validate';
|
import { isMobile } from '@/utils/validate';
|
||||||
import NProgress from 'nprogress';
|
import NProgress from 'nprogress';
|
||||||
import 'nprogress/nprogress.css';
|
import 'nprogress/nprogress.css';
|
||||||
@@ -323,16 +324,16 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
isAdmin() {
|
isAdmin() {
|
||||||
const authority = this.userInfo && this.userInfo.authority;
|
return isAdministrator(this.userInfo);
|
||||||
return Array.isArray(authority)
|
|
||||||
? authority.includes('admin')
|
|
||||||
: String(authority || '').includes('admin');
|
|
||||||
},
|
},
|
||||||
ids() {
|
ids() {
|
||||||
return this.selectionList.map(item => item.id).join(',');
|
return this.selectionList.map(item => item.id).join(',');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
if (this.isAdmin) {
|
||||||
|
this.allDept = 1;
|
||||||
|
}
|
||||||
this.initDeptTree();
|
this.initDeptTree();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -343,15 +344,21 @@ export default {
|
|||||||
getDeptTree(this.userInfo.tenantId).then(res => {
|
getDeptTree(this.userInfo.tenantId).then(res => {
|
||||||
this.deptOptions = this.flattenDept(res.data.data || []);
|
this.deptOptions = this.flattenDept(res.data.data || []);
|
||||||
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
|
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
|
||||||
|
if (deptColumn) {
|
||||||
deptColumn.dicData = this.deptOptions;
|
deptColumn.dicData = this.deptOptions;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
flattenDept(tree, level = 0) {
|
flattenDept(tree, level = 0) {
|
||||||
const result = [];
|
const result = [];
|
||||||
tree.forEach(item => {
|
tree.forEach(item => {
|
||||||
|
// 雪花 ID 必须保持字符串,避免 JS Number 精度丢失导致筛错组织。
|
||||||
|
const id =
|
||||||
|
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
|
||||||
|
if (!id) return;
|
||||||
result.push({
|
result.push({
|
||||||
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
||||||
value: item.id,
|
value: id,
|
||||||
rawLabel: item.title || item.deptName || item.name,
|
rawLabel: item.title || item.deptName || item.name,
|
||||||
});
|
});
|
||||||
if (item.children && item.children.length) {
|
if (item.children && item.children.length) {
|
||||||
@@ -360,6 +367,19 @@ export default {
|
|||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
normalizeListQuery(params = {}) {
|
||||||
|
// 搜索条件优先于缓存的 query,避免旧组织 ID 覆盖用户新选择的组织。
|
||||||
|
const query = { ...this.query, ...params };
|
||||||
|
const rawDeptId = query.deptId ?? query.searchDeptId;
|
||||||
|
delete query.searchDeptId;
|
||||||
|
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
|
||||||
|
query.deptId = String(rawDeptId).split(',')[0].trim();
|
||||||
|
} else {
|
||||||
|
delete query.deptId;
|
||||||
|
}
|
||||||
|
query.allDept = this.isAdmin ? 1 : 0;
|
||||||
|
return query;
|
||||||
|
},
|
||||||
getRecords(res) {
|
getRecords(res) {
|
||||||
const data = res && res.data && res.data.data;
|
const data = res && res.data && res.data.data;
|
||||||
if (Array.isArray(data)) return data;
|
if (Array.isArray(data)) return data;
|
||||||
@@ -966,13 +986,15 @@ export default {
|
|||||||
},
|
},
|
||||||
searchReset() {
|
searchReset() {
|
||||||
this.query = {};
|
this.query = {};
|
||||||
|
this.allDept = this.isAdmin ? 1 : 0;
|
||||||
this.page.currentPage = 1;
|
this.page.currentPage = 1;
|
||||||
this.onLoad(this.page);
|
this.onLoad(this.page);
|
||||||
},
|
},
|
||||||
searchChange(params, done) {
|
searchChange(params, done) {
|
||||||
this.query = params;
|
this.query = this.normalizeListQuery(params);
|
||||||
|
this.allDept = this.query.allDept;
|
||||||
this.page.currentPage = 1;
|
this.page.currentPage = 1;
|
||||||
this.onLoad(this.page, params);
|
this.onLoad(this.page, this.query);
|
||||||
done();
|
done();
|
||||||
},
|
},
|
||||||
selectionChange(list) {
|
selectionChange(list) {
|
||||||
@@ -993,7 +1015,10 @@ export default {
|
|||||||
},
|
},
|
||||||
onLoad(page, params = {}) {
|
onLoad(page, params = {}) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: this.allDept })
|
const query = this.normalizeListQuery(params);
|
||||||
|
this.query = query;
|
||||||
|
this.allDept = query.allDept;
|
||||||
|
getList(page.currentPage, page.pageSize, query)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const data = res.data.data;
|
const data = res.data.data;
|
||||||
this.page.total = data.total;
|
this.page.total = data.total;
|
||||||
@@ -1026,8 +1051,7 @@ export default {
|
|||||||
},
|
},
|
||||||
buildExportParams() {
|
buildExportParams() {
|
||||||
return {
|
return {
|
||||||
...this.query,
|
...this.normalizeListQuery(this.query),
|
||||||
allDept: this.allDept,
|
|
||||||
ids: this.ids,
|
ids: this.ids,
|
||||||
[this.website.tokenHeader]: getToken(),
|
[this.website.tokenHeader]: getToken(),
|
||||||
};
|
};
|
||||||
@@ -1111,36 +1135,32 @@ export default {
|
|||||||
this.$message.warning('请输入地址关键词');
|
this.$message.warning('请输入地址关键词');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loadAmap()
|
if (this.mapLoading) return;
|
||||||
.then(() => {
|
this.mapSearchResults = [];
|
||||||
this.mapLoading = true;
|
this.mapLoading = true;
|
||||||
this.ensureAmapGeocoder()
|
this.loadAmap()
|
||||||
.then(() => this.runAmapGeocode('location', keyword))
|
.then(() => searchAmapPlaceText(keyword))
|
||||||
.then(result => {
|
.then(pois => {
|
||||||
this.mapSearchResults = (result.geocodes || []).map((item, index) => ({
|
this.mapSearchResults = normalizeMapSearchResults(pois, keyword);
|
||||||
id: item.id || index,
|
const first = this.mapSearchResults[0];
|
||||||
name: item.formattedAddress || keyword,
|
if (!first?.location) {
|
||||||
address: item.formattedAddress || keyword,
|
|
||||||
location: item.location,
|
|
||||||
}));
|
|
||||||
const point = this.resolveMapPoint(result);
|
|
||||||
if (!point) {
|
|
||||||
this.mapStatus = '未找到匹配地址';
|
this.mapStatus = '未找到匹配地址';
|
||||||
this.$message.warning('地图搜索无匹配地址');
|
this.$message.warning('地图搜索无匹配地址');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.pickMapPoint(point, keyword);
|
return this.$nextTick().then(() => {
|
||||||
})
|
if (typeof this.amap?.resize === 'function') {
|
||||||
.catch(() => {
|
this.amap.resize();
|
||||||
this.mapStatus = '地图搜索失败';
|
}
|
||||||
this.$message.error('地图搜索失败,请稍后重试');
|
this.pickMapPoint(first.location, first.address || first.name || keyword);
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.mapLoading = false;
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
|
this.mapStatus = '未找到匹配地址';
|
||||||
|
this.$message.warning('地图搜索无匹配地址');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.mapLoading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
selectMapSearchResult(item) {
|
selectMapSearchResult(item) {
|
||||||
|
|||||||
@@ -238,6 +238,13 @@ export default {
|
|||||||
value: 'value',
|
value: 'value',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
minWidth: 120,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '更新人',
|
label: '更新人',
|
||||||
prop: 'updateUserName',
|
prop: 'updateUserName',
|
||||||
|
|||||||
@@ -143,6 +143,7 @@
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { getDetail, getList, remove, submit } from '@/api/base/insurance-ocr-template';
|
import { getDetail, getList, remove, submit } from '@/api/base/insurance-ocr-template';
|
||||||
import SectionCard from '@/components/section-card/main.vue';
|
import SectionCard from '@/components/section-card/main.vue';
|
||||||
|
import { isAdministrator } from '@/utils/auth';
|
||||||
|
|
||||||
const insuranceFieldKeys = [
|
const insuranceFieldKeys = [
|
||||||
'保险类型',
|
'保险类型',
|
||||||
@@ -203,7 +204,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['permission', 'userInfo']),
|
...mapGetters(['permission', 'userInfo']),
|
||||||
isAdmin() {
|
isAdmin() {
|
||||||
return String(this.userInfo.authority || '').includes('admin');
|
return isAdministrator(this.userInfo);
|
||||||
},
|
},
|
||||||
selectionIds() {
|
selectionIds() {
|
||||||
return this.selectionList.map(item => item.id).join(',');
|
return this.selectionList.map(item => item.id).join(',');
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { changeStatus, getDetail, getList, remove, submit } from '@/api/base/measurement-unit';
|
import { changeStatus, getDetail, getList, remove, submit } from '@/api/base/measurement-unit';
|
||||||
import { createOption } from '@/option/base/measurement-unit';
|
import { createOption } from '@/option/base/measurement-unit';
|
||||||
|
import { isAdministrator } from '@/utils/auth';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
@@ -111,7 +112,7 @@ export default {
|
|||||||
return this.selectionList.map(item => item.id).join(',');
|
return this.selectionList.map(item => item.id).join(',');
|
||||||
},
|
},
|
||||||
isAdmin() {
|
isAdmin() {
|
||||||
return String(this.userInfo?.authority || '').includes('admin');
|
return isAdministrator(this.userInfo);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -194,10 +194,11 @@
|
|||||||
<el-input
|
<el-input
|
||||||
v-model="form.detailAddress"
|
v-model="form.detailAddress"
|
||||||
class="detail-address-input"
|
class="detail-address-input"
|
||||||
clearable
|
readonly
|
||||||
maxlength="255"
|
maxlength="255"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
placeholder="请输入"
|
placeholder="点击选择地图地址"
|
||||||
|
@click="openAddressMapPicker"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #remark-form>
|
<template #remark-form>
|
||||||
@@ -244,6 +245,13 @@
|
|||||||
</template>
|
</template>
|
||||||
</avue-form>
|
</avue-form>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
<address-map-picker
|
||||||
|
v-model="addressMapPickerVisible"
|
||||||
|
:address="form.detailAddress"
|
||||||
|
:longitude="form.longitude"
|
||||||
|
:latitude="form.latitude"
|
||||||
|
@confirm="handleAddressMapConfirm"
|
||||||
|
/>
|
||||||
</basic-container>
|
</basic-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -256,20 +264,24 @@ import {
|
|||||||
changeStatus,
|
changeStatus,
|
||||||
getPortSelect,
|
getPortSelect,
|
||||||
} from '@/api/base/port-terminal';
|
} from '@/api/base/port-terminal';
|
||||||
import { getLazyTree } from '@/api/base/region';
|
import { getDetail as getRegionDetail, getLazyTree } from '@/api/base/region';
|
||||||
import { exportBlob } from '@/api/common';
|
import { exportBlob } from '@/api/common';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
import { getToken } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
||||||
|
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||||
import NProgress from 'nprogress';
|
import NProgress from 'nprogress';
|
||||||
import 'nprogress/nprogress.css';
|
import 'nprogress/nprogress.css';
|
||||||
|
|
||||||
const DEFAULT_COUNTRY_CODE = '+86';
|
const DEFAULT_COUNTRY_CODE = '+86';
|
||||||
const newLocal = '请选择类型';
|
const newLocal = '请选择类型';
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
AddressMapPicker,
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
const validateCode = (rule, value, callback) => {
|
const validateCode = (rule, value, callback) => {
|
||||||
const code = String(value || '').toUpperCase();
|
const code = String(value || '').toUpperCase();
|
||||||
@@ -315,6 +327,7 @@ export default {
|
|||||||
loading: true,
|
loading: true,
|
||||||
data: [],
|
data: [],
|
||||||
excelBox: false,
|
excelBox: false,
|
||||||
|
addressMapPickerVisible: false,
|
||||||
excelForm: {},
|
excelForm: {},
|
||||||
portOptions: [],
|
portOptions: [],
|
||||||
countryOptions: [],
|
countryOptions: [],
|
||||||
@@ -571,12 +584,12 @@ export default {
|
|||||||
span: 12,
|
span: 12,
|
||||||
minWidth: 220,
|
minWidth: 220,
|
||||||
overHidden: false,
|
overHidden: false,
|
||||||
order: 85,
|
order: 90,
|
||||||
placeholder: '请输入',
|
placeholder: '点击选择地图地址',
|
||||||
maxlength: 255,
|
maxlength: 255,
|
||||||
showWordLimit: true,
|
showWordLimit: true,
|
||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: '请输入详细地址', trigger: 'blur' },
|
{ required: true, message: '请选择详细地址', trigger: 'change' },
|
||||||
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
|
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -608,6 +621,13 @@ export default {
|
|||||||
showWordLimit: true,
|
showWordLimit: true,
|
||||||
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '更新人',
|
label: '更新人',
|
||||||
prop: 'updateUserName',
|
prop: 'updateUserName',
|
||||||
@@ -695,10 +715,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
isAdmin() {
|
isAdmin() {
|
||||||
const authority = this.userInfo && this.userInfo.authority;
|
return isAdministrator(this.userInfo);
|
||||||
return Array.isArray(authority)
|
|
||||||
? authority.includes('admin')
|
|
||||||
: String(authority || '').includes('admin');
|
|
||||||
},
|
},
|
||||||
isParentRegionLocked() {
|
isParentRegionLocked() {
|
||||||
return this.form.category === '码头';
|
return this.form.category === '码头';
|
||||||
@@ -824,10 +841,60 @@ export default {
|
|||||||
},
|
},
|
||||||
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.setCityOptions([]);
|
||||||
this.setDistrictOptions([]);
|
this.setDistrictOptions([]);
|
||||||
this.loadProvinceOptions(country ? country.id : '');
|
this.clearSearchCityAndDistrict();
|
||||||
|
// 搜索栏为国家 → 城市 → 区县(无省份),选中国家后需直接加载该国下全部城市
|
||||||
|
this.loadCityOptionsByCountry(country ? country.id : '');
|
||||||
|
},
|
||||||
|
clearSearchCityAndDistrict() {
|
||||||
|
const searchForm = this.$refs.crud && this.$refs.crud.searchForm;
|
||||||
|
if (searchForm) {
|
||||||
|
searchForm.city = undefined;
|
||||||
|
searchForm.districtName = undefined;
|
||||||
|
}
|
||||||
|
if (this.query) {
|
||||||
|
this.query.city = undefined;
|
||||||
|
this.query.districtName = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadCityOptionsByCountry(countryCode) {
|
||||||
|
if (!countryCode) {
|
||||||
|
this.setCityOptions([]);
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
this.cityLoading = true;
|
||||||
|
return this.loadProvinceOptions(countryCode)
|
||||||
|
.then(provinces =>
|
||||||
|
Promise.all((provinces || []).map(province => this.fetchCityOptions(province.id)))
|
||||||
|
)
|
||||||
|
.then(cityGroups => {
|
||||||
|
const cityMap = new Map();
|
||||||
|
cityGroups.flat().forEach(city => {
|
||||||
|
if (city && city.id && !cityMap.has(city.id)) {
|
||||||
|
cityMap.set(city.id, city);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const cities = Array.from(cityMap.values());
|
||||||
|
this.setCityOptions(cities);
|
||||||
|
return cities;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.cityLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchCityOptions(provinceCode) {
|
||||||
|
if (!provinceCode) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
if (this.cityOptionCache[provinceCode]) {
|
||||||
|
return Promise.resolve(this.cityOptionCache[provinceCode]);
|
||||||
|
}
|
||||||
|
return getLazyTree(provinceCode).then(res => {
|
||||||
|
const options = (res.data.data || []).map(this.normalizeRegionOption);
|
||||||
|
this.cityOptionCache[provinceCode] = options;
|
||||||
|
return options;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
syncCountryName(countryCode) {
|
syncCountryName(countryCode) {
|
||||||
const country = this.countryOptions.find(item => item.id === countryCode);
|
const country = this.countryOptions.find(item => item.id === countryCode);
|
||||||
@@ -908,10 +975,8 @@ export default {
|
|||||||
return Promise.resolve(this.cityOptionCache[provinceCode]);
|
return Promise.resolve(this.cityOptionCache[provinceCode]);
|
||||||
}
|
}
|
||||||
this.cityLoading = true;
|
this.cityLoading = true;
|
||||||
return getLazyTree(provinceCode)
|
return this.fetchCityOptions(provinceCode)
|
||||||
.then(res => {
|
.then(options => {
|
||||||
const options = (res.data.data || []).map(this.normalizeRegionOption);
|
|
||||||
this.cityOptionCache[provinceCode] = options;
|
|
||||||
this.setCityOptions(options);
|
this.setCityOptions(options);
|
||||||
return options;
|
return options;
|
||||||
})
|
})
|
||||||
@@ -1030,6 +1095,13 @@ export default {
|
|||||||
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([]);
|
||||||
|
const searchForm = this.$refs.crud && this.$refs.crud.searchForm;
|
||||||
|
if (searchForm) {
|
||||||
|
searchForm.districtName = undefined;
|
||||||
|
}
|
||||||
|
if (this.query) {
|
||||||
|
this.query.districtName = undefined;
|
||||||
|
}
|
||||||
this.loadDistrictOptions(city ? city.id : '');
|
this.loadDistrictOptions(city ? city.id : '');
|
||||||
},
|
},
|
||||||
handleDistrictChange(value) {
|
handleDistrictChange(value) {
|
||||||
@@ -1040,6 +1112,228 @@ export default {
|
|||||||
handleCoordinateInput(prop, value) {
|
handleCoordinateInput(prop, value) {
|
||||||
this.form[prop] = normalizeCoordinateInput(value);
|
this.form[prop] = normalizeCoordinateInput(value);
|
||||||
},
|
},
|
||||||
|
openAddressMapPicker() {
|
||||||
|
this.addressMapPickerVisible = true;
|
||||||
|
},
|
||||||
|
formatMapCoordinate(value) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const number = Number(value);
|
||||||
|
if (!Number.isFinite(number)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return normalizeCoordinateInput(number.toFixed(6));
|
||||||
|
},
|
||||||
|
patchDialogForm(patch = {}) {
|
||||||
|
// 同步页面 form 与 Avue 弹窗内部 tableForm,避免只改了一边导致经纬度不刷新
|
||||||
|
Object.assign(this.form, patch);
|
||||||
|
const tableForm = this.$refs.crud?.tableForm;
|
||||||
|
if (tableForm && tableForm !== this.form) {
|
||||||
|
Object.assign(tableForm, patch);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleAddressMapConfirm(payload) {
|
||||||
|
const selection = typeof payload === 'string' ? { address: payload } : payload || {};
|
||||||
|
const longitude = this.formatMapCoordinate(selection.longitude);
|
||||||
|
const latitude = this.formatMapCoordinate(selection.latitude);
|
||||||
|
const patch = {};
|
||||||
|
if (selection.address) {
|
||||||
|
patch.detailAddress = selection.address;
|
||||||
|
}
|
||||||
|
// 换地址时必须同步覆盖经纬度,不能保留上一次选点
|
||||||
|
if (longitude !== '' && latitude !== '') {
|
||||||
|
patch.longitude = longitude;
|
||||||
|
patch.latitude = latitude;
|
||||||
|
}
|
||||||
|
if (Object.keys(patch).length) {
|
||||||
|
this.patchDialogForm(patch);
|
||||||
|
}
|
||||||
|
this.applyMapRegionSelection(selection);
|
||||||
|
},
|
||||||
|
normalizeAmapCityName(city, province) {
|
||||||
|
if (Array.isArray(city)) {
|
||||||
|
return String(province || '').trim();
|
||||||
|
}
|
||||||
|
const cityName = String(city || '').trim();
|
||||||
|
return cityName || String(province || '').trim();
|
||||||
|
},
|
||||||
|
buildRegionLookupCodes(adcode) {
|
||||||
|
const code = String(adcode || '').trim();
|
||||||
|
if (!code) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const codes = [code];
|
||||||
|
if (/^\d{6}$/.test(code)) {
|
||||||
|
if (code.endsWith('00')) {
|
||||||
|
const cityCode = code.slice(0, 4);
|
||||||
|
if (cityCode && !codes.includes(cityCode)) {
|
||||||
|
codes.push(cityCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (code.endsWith('0000')) {
|
||||||
|
const provinceCode = code.slice(0, 2);
|
||||||
|
if (provinceCode && !codes.includes(provinceCode)) {
|
||||||
|
codes.push(provinceCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return codes;
|
||||||
|
},
|
||||||
|
fetchRegionDetailByCodes(codes = []) {
|
||||||
|
const tryNext = index => {
|
||||||
|
if (index >= codes.length) {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
return getRegionDetail(codes[index])
|
||||||
|
.then(res => {
|
||||||
|
const detail = res.data && res.data.data;
|
||||||
|
if (detail && detail.code) {
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
return tryNext(index + 1);
|
||||||
|
})
|
||||||
|
.catch(() => tryNext(index + 1));
|
||||||
|
};
|
||||||
|
return tryNext(0);
|
||||||
|
},
|
||||||
|
resolveCountryFromRegionDetail(detail = {}) {
|
||||||
|
const ancestors = String(detail.ancestors || '')
|
||||||
|
.split(',')
|
||||||
|
.map(item => String(item || '').trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
for (let index = 0; index < ancestors.length; index += 1) {
|
||||||
|
const ancestorCode = ancestors[index];
|
||||||
|
if (this.countryOptions.some(item => item.id === ancestorCode)) {
|
||||||
|
return ancestorCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
detail.parentCode &&
|
||||||
|
Number(detail.regionLevel) === 1 &&
|
||||||
|
this.countryOptions.some(item => item.id === String(detail.parentCode))
|
||||||
|
) {
|
||||||
|
return String(detail.parentCode);
|
||||||
|
}
|
||||||
|
if (/^\d{6}$/.test(String(detail.code || ''))) {
|
||||||
|
return DEFAULT_COUNTRY_CODE;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
applyRegionDetail(detail = {}) {
|
||||||
|
const countryHint = this.resolveCountryFromRegionDetail(detail);
|
||||||
|
return this.resolveCountryCode(countryHint || DEFAULT_COUNTRY_CODE)
|
||||||
|
.then(countryCode => {
|
||||||
|
this.form.countryCode = countryCode;
|
||||||
|
this.syncCountryName(countryCode);
|
||||||
|
return this.loadProvinceOptions(countryCode);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.form.provinceCode =
|
||||||
|
String(detail.provinceCode || '') || this.resolveProvinceCode(detail.provinceName);
|
||||||
|
const province = this.provinceOptions.find(item => item.id === this.form.provinceCode);
|
||||||
|
this.form.provinceName = province ? province.title : detail.provinceName || '';
|
||||||
|
return this.loadCityOptions(this.form.provinceCode);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.form.cityCode =
|
||||||
|
String(detail.cityCode || '') ||
|
||||||
|
this.resolveCityCode(detail.cityName, detail.districtCode || detail.code);
|
||||||
|
const city = this.cityOptions.find(item => item.id === this.form.cityCode);
|
||||||
|
this.form.city = city ? city.title : detail.cityName || '';
|
||||||
|
return this.loadDistrictOptions(this.form.cityCode);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.form.districtCode = this.resolveDistrictCode(
|
||||||
|
detail.districtName || detail.name,
|
||||||
|
detail.districtCode || detail.code
|
||||||
|
);
|
||||||
|
this.handleDistrictChange(this.form.districtCode);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
applyMapRegionByNames(selection = {}) {
|
||||||
|
const provinceName = String(selection.province || selection.provinceName || '').trim();
|
||||||
|
const cityName = this.normalizeAmapCityName(
|
||||||
|
selection.city || selection.cityName,
|
||||||
|
provinceName
|
||||||
|
);
|
||||||
|
const districtName = String(selection.district || selection.districtName || '').trim();
|
||||||
|
const countryName = String(selection.country || selection.countryName || '').trim();
|
||||||
|
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||||
|
const countryPromise = countryName
|
||||||
|
? this.resolveCountryCode(countryName)
|
||||||
|
: Promise.resolve(/^\d{6}$/.test(adcode) ? DEFAULT_COUNTRY_CODE : '');
|
||||||
|
return countryPromise
|
||||||
|
.then(countryCode => {
|
||||||
|
if (!countryCode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
this.form.countryCode = countryCode;
|
||||||
|
this.syncCountryName(countryCode);
|
||||||
|
return this.loadProvinceOptions(countryCode);
|
||||||
|
})
|
||||||
|
.then(provinces => {
|
||||||
|
if (!provinces) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
this.form.provinceCode = this.resolveProvinceCode(provinceName);
|
||||||
|
const province = this.provinceOptions.find(item => item.id === this.form.provinceCode);
|
||||||
|
this.form.provinceName = province ? province.title : provinceName;
|
||||||
|
if (!this.form.provinceCode) {
|
||||||
|
this.clearCityValue();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.loadCityOptions(this.form.provinceCode);
|
||||||
|
})
|
||||||
|
.then(cities => {
|
||||||
|
if (!cities) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
this.form.cityCode = this.resolveCityCode(cityName, adcode);
|
||||||
|
const city = this.cityOptions.find(item => item.id === this.form.cityCode);
|
||||||
|
this.form.city = city ? city.title : cityName;
|
||||||
|
if (!this.form.cityCode) {
|
||||||
|
this.clearDistrictValue();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.loadDistrictOptions(this.form.cityCode);
|
||||||
|
})
|
||||||
|
.then(districts => {
|
||||||
|
if (!districts) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.form.districtCode = this.resolveDistrictCode(districtName, adcode);
|
||||||
|
this.handleDistrictChange(this.form.districtCode);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
applyMapRegionSelection(selection = {}) {
|
||||||
|
if (this.isParentRegionLocked) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||||
|
const hasNameHint = Boolean(
|
||||||
|
selection.country ||
|
||||||
|
selection.countryName ||
|
||||||
|
selection.province ||
|
||||||
|
selection.provinceName ||
|
||||||
|
selection.city ||
|
||||||
|
selection.cityName ||
|
||||||
|
selection.district ||
|
||||||
|
selection.districtName
|
||||||
|
);
|
||||||
|
if (!adcode && !hasNameHint) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
const lookupCodes = this.buildRegionLookupCodes(adcode);
|
||||||
|
return this.fetchRegionDetailByCodes(lookupCodes)
|
||||||
|
.then(detail => {
|
||||||
|
if (detail) {
|
||||||
|
return this.applyRegionDetail(detail);
|
||||||
|
}
|
||||||
|
return this.applyMapRegionByNames(selection);
|
||||||
|
})
|
||||||
|
.catch(() => this.applyMapRegionByNames(selection));
|
||||||
|
},
|
||||||
resolveCountryCode(country) {
|
resolveCountryCode(country) {
|
||||||
if (!country) {
|
if (!country) {
|
||||||
return Promise.resolve('');
|
return Promise.resolve('');
|
||||||
@@ -1107,7 +1401,7 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isBlank(row.detailAddress)) {
|
if (isBlank(row.detailAddress)) {
|
||||||
this.$message.warning('请输入详细地址');
|
this.$message.warning('请选择详细地址');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isBlank(row.longitude)) {
|
if (isBlank(row.longitude)) {
|
||||||
@@ -1219,13 +1513,50 @@ export default {
|
|||||||
this.$message({ type: 'success', message: '操作成功!' });
|
this.$message({ type: 'success', message: '操作成功!' });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
createEmptyPortTerminalForm() {
|
||||||
|
return {
|
||||||
|
category: '港口',
|
||||||
|
dataSource: '手动录入',
|
||||||
|
status: 1,
|
||||||
|
parentId: undefined,
|
||||||
|
parentCode: '',
|
||||||
|
parentName: '',
|
||||||
|
name: '',
|
||||||
|
code: '',
|
||||||
|
terminalCode: '',
|
||||||
|
country: '',
|
||||||
|
countryCode: '',
|
||||||
|
provinceCode: undefined,
|
||||||
|
provinceName: '',
|
||||||
|
cityCode: undefined,
|
||||||
|
city: '',
|
||||||
|
districtCode: undefined,
|
||||||
|
districtName: '',
|
||||||
|
regionCode: '',
|
||||||
|
detailAddress: '',
|
||||||
|
longitude: '',
|
||||||
|
latitude: '',
|
||||||
|
remark: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
resetAddForm() {
|
||||||
|
// 新增必须整表重置,避免沿用上一次编辑/查看残留
|
||||||
|
this.form = this.createEmptyPortTerminalForm();
|
||||||
|
this.setProvinceOptions([]);
|
||||||
|
this.setCityOptions([]);
|
||||||
|
this.setDistrictOptions([]);
|
||||||
|
},
|
||||||
beforeOpen(done, type) {
|
beforeOpen(done, type) {
|
||||||
if (type === 'add') {
|
if (type === 'add') {
|
||||||
this.form.category = this.form.category || '港口';
|
this.resetAddForm();
|
||||||
this.form.dataSource = this.form.dataSource || '手动录入';
|
this.handleCategoryChange('港口', false);
|
||||||
this.form.status = this.form.status || 1;
|
done();
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.crud?.clearValidate?.();
|
||||||
|
setTimeout(() => this.disableDialogAutocomplete(), 50);
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
this.handleCategoryChange(this.form.category || '港口');
|
|
||||||
if (['edit', 'view'].includes(type)) {
|
if (['edit', 'view'].includes(type)) {
|
||||||
getDetail(this.form.id).then(res => {
|
getDetail(this.form.id).then(res => {
|
||||||
this.form = res.data.data;
|
this.form = res.data.data;
|
||||||
@@ -1260,35 +1591,7 @@ export default {
|
|||||||
setTimeout(() => this.disableDialogAutocomplete(), 50);
|
setTimeout(() => this.disableDialogAutocomplete(), 50);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
this.resolveCountryCode(this.form.country)
|
|
||||||
.then(countryCode => {
|
|
||||||
this.form.countryCode = countryCode;
|
|
||||||
this.syncCountryName(countryCode);
|
|
||||||
return this.loadProvinceOptions(countryCode);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.form.provinceCode =
|
|
||||||
String(this.form.provinceCode || '') ||
|
|
||||||
this.resolveProvinceCode(this.form.provinceName);
|
|
||||||
return this.loadCityOptions(this.form.provinceCode);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.form.cityCode = this.resolveCityCode(this.form.city, this.form.regionCode);
|
|
||||||
return this.loadDistrictOptions(this.form.cityCode);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.form.districtCode = this.resolveDistrictCode(
|
|
||||||
this.form.districtName,
|
|
||||||
this.form.regionCode
|
|
||||||
);
|
|
||||||
if (!this.form.regionCode) {
|
|
||||||
this.handleDistrictChange(this.form.districtCode);
|
|
||||||
}
|
|
||||||
done();
|
|
||||||
setTimeout(() => this.disableDialogAutocomplete(), 50);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
disableDialogAutocomplete() {
|
disableDialogAutocomplete() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -1399,45 +1702,26 @@ export default {
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-address-input {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper),
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
/* 港口码头编辑/新增弹窗:对齐 railway-station 统一风格(灰底 + 白卡表单 + 浮动底栏,padding 24px 与全局弹窗标准一致) */
|
/* 港口码头编辑/新增弹窗:与全局表单项行距保持一致 */
|
||||||
//.port-terminal-edit-dialog {
|
.port-terminal-edit-dialog {
|
||||||
// /* 灰底内容区 */
|
.el-form-item,
|
||||||
// .el-dialog__body {
|
.el-form-item--default.el-form-item,
|
||||||
// background-color: #f2f2f2;
|
.el-form-item--small.el-form-item,
|
||||||
// padding: 8px;
|
.el-form-item--large.el-form-item,
|
||||||
// }
|
.el-form-item--medium.el-form-item {
|
||||||
// /* 表单白卡(与全局 .el-dialog .avue-form 一致,padding 24px 对齐 customer-archive / railway-station) */
|
margin-bottom: 24px;
|
||||||
// .avue-form {
|
}
|
||||||
// background: #fff;
|
}
|
||||||
// border-radius: 6px;
|
|
||||||
// box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
||||||
// padding: 24px !important;
|
|
||||||
// margin-bottom: 0;
|
|
||||||
// }
|
|
||||||
// /* 底部按钮区白卡(Avue 弹窗 footer 在 body 内,接续白卡) */
|
|
||||||
// .avue-dialog__footer {
|
|
||||||
// background: #fff;
|
|
||||||
// border-radius: 0 0 6px 6px;
|
|
||||||
// box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
||||||
// padding: 16px 24px !important;
|
|
||||||
// display: flex;
|
|
||||||
// justify-content: flex-end;
|
|
||||||
// }
|
|
||||||
// /* label 宽度统一 108px(对齐 railway-station,标签对齐更整齐) */
|
|
||||||
// .el-form-item__label {
|
|
||||||
// width: 108px !important;
|
|
||||||
// }
|
|
||||||
// /* 所有 input 文本框、select 下拉控件统一宽度 250px */
|
|
||||||
// .el-input,
|
|
||||||
// .el-select {
|
|
||||||
// width: 250px !important;
|
|
||||||
// }
|
|
||||||
// /* 备注宽度 85% */
|
|
||||||
// .remark-input {
|
|
||||||
// width: 85% !important;
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -94,6 +94,17 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
|
<template #detailAddress-form>
|
||||||
|
<el-input
|
||||||
|
v-model="form.detailAddress"
|
||||||
|
class="detail-address-input"
|
||||||
|
readonly
|
||||||
|
maxlength="255"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="点击选择地图地址"
|
||||||
|
@click="openAddressMapPicker"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<template #longitude-form>
|
<template #longitude-form>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.longitude"
|
v-model="form.longitude"
|
||||||
@@ -121,25 +132,36 @@
|
|||||||
</template>
|
</template>
|
||||||
</avue-form>
|
</avue-form>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
<address-map-picker
|
||||||
|
v-model="addressMapPickerVisible"
|
||||||
|
:address="form.detailAddress"
|
||||||
|
:longitude="form.longitude"
|
||||||
|
:latitude="form.latitude"
|
||||||
|
@confirm="handleAddressMapConfirm"
|
||||||
|
/>
|
||||||
</basic-container>
|
</basic-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/railway-station';
|
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/railway-station';
|
||||||
import { getLazyTree } from '@/api/base/region';
|
import { getDetail as getRegionDetail, getLazyTree } from '@/api/base/region';
|
||||||
import { exportBlob } from '@/api/common';
|
import { exportBlob } from '@/api/common';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
import { getToken } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
||||||
|
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||||
import NProgress from 'nprogress';
|
import NProgress from 'nprogress';
|
||||||
import 'nprogress/nprogress.css';
|
import 'nprogress/nprogress.css';
|
||||||
|
|
||||||
const DEFAULT_COUNTRY_CODE = '+86';
|
const DEFAULT_COUNTRY_CODE = '+86';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
AddressMapPicker,
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
const validateTmisCode = (rule, value, callback) => {
|
const validateTmisCode = (rule, value, callback) => {
|
||||||
if (!/^\d{5}$/.test(value || '')) {
|
if (!/^\d{5}$/.test(value || '')) {
|
||||||
@@ -149,7 +171,12 @@ export default {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const validateTelegraphCode = (rule, value, callback) => {
|
const validateTelegraphCode = (rule, value, callback) => {
|
||||||
if (!/^[A-Z]{3}$/.test(String(value || '').toUpperCase())) {
|
const telegraphCode = String(value || '').trim();
|
||||||
|
if (!telegraphCode) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!/^[A-Z]{3}$/.test(telegraphCode.toUpperCase())) {
|
||||||
callback(new Error('电报略码格式不正确或已存在'));
|
callback(new Error('电报略码格式不正确或已存在'));
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
@@ -185,6 +212,7 @@ export default {
|
|||||||
loading: true,
|
loading: true,
|
||||||
data: [],
|
data: [],
|
||||||
excelBox: false,
|
excelBox: false,
|
||||||
|
addressMapPickerVisible: false,
|
||||||
excelForm: {},
|
excelForm: {},
|
||||||
provinceOptions: [],
|
provinceOptions: [],
|
||||||
cityOptions: [],
|
cityOptions: [],
|
||||||
@@ -250,10 +278,7 @@ export default {
|
|||||||
prop: 'telegraphCode',
|
prop: 'telegraphCode',
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
maxlength: 3,
|
maxlength: 3,
|
||||||
rules: [
|
rules: [{ validator: validateTelegraphCode, trigger: 'blur' }],
|
||||||
{ required: true, message: '电报略码格式不正确或已存在', trigger: 'blur' },
|
|
||||||
{ validator: validateTelegraphCode, trigger: 'blur' },
|
|
||||||
],
|
|
||||||
change: ({ value }) => this.handleTelegraphChange(value),
|
change: ({ value }) => this.handleTelegraphChange(value),
|
||||||
blur: ({ value }) => this.handleTelegraphChange(value),
|
blur: ({ value }) => this.handleTelegraphChange(value),
|
||||||
},
|
},
|
||||||
@@ -270,6 +295,20 @@ export default {
|
|||||||
// prop: 'railwayLine',
|
// prop: 'railwayLine',
|
||||||
// minWidth: 130,
|
// minWidth: 130,
|
||||||
// },
|
// },
|
||||||
|
{
|
||||||
|
label: '详细地址',
|
||||||
|
prop: 'detailAddress',
|
||||||
|
formslot: true,
|
||||||
|
span: 24,
|
||||||
|
minWidth: 220,
|
||||||
|
overHidden: false,
|
||||||
|
maxlength: 255,
|
||||||
|
showWordLimit: true,
|
||||||
|
rules: [
|
||||||
|
{ required: true, message: '请选择详细地址', trigger: 'change' },
|
||||||
|
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '所属省份',
|
label: '所属省份',
|
||||||
prop: 'provinceCode',
|
prop: 'provinceCode',
|
||||||
@@ -337,21 +376,6 @@ export default {
|
|||||||
addDisplay: false,
|
addDisplay: false,
|
||||||
editDisplay: false,
|
editDisplay: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: '详细地址',
|
|
||||||
prop: 'detailAddress',
|
|
||||||
type: 'textarea',
|
|
||||||
minRows: 2,
|
|
||||||
span: 24,
|
|
||||||
minWidth: 220,
|
|
||||||
overHidden: false,
|
|
||||||
maxlength: 255,
|
|
||||||
showWordLimit: true,
|
|
||||||
rules: [
|
|
||||||
{ required: true, message: '请输入详细地址', trigger: 'blur' },
|
|
||||||
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: '经度',
|
label: '经度',
|
||||||
prop: 'longitude',
|
prop: 'longitude',
|
||||||
@@ -411,6 +435,13 @@ export default {
|
|||||||
showWordLimit: true,
|
showWordLimit: true,
|
||||||
rules: [{ max: 200, message: '备注不能超过200字', trigger: 'blur' }],
|
rules: [{ max: 200, message: '备注不能超过200字', trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '创建人',
|
||||||
|
prop: 'createUserName',
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '更新人',
|
label: '更新人',
|
||||||
prop: 'updateUserName',
|
prop: 'updateUserName',
|
||||||
@@ -497,10 +528,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
isAdmin() {
|
isAdmin() {
|
||||||
const authority = this.userInfo && this.userInfo.authority;
|
return isAdministrator(this.userInfo);
|
||||||
return Array.isArray(authority)
|
|
||||||
? authority.includes('admin')
|
|
||||||
: String(authority || '').includes('admin');
|
|
||||||
},
|
},
|
||||||
ids() {
|
ids() {
|
||||||
let ids = [];
|
let ids = [];
|
||||||
@@ -523,33 +551,51 @@ export default {
|
|||||||
return targetColumns.find(item => item.prop === targetProp) || null;
|
return targetColumns.find(item => item.prop === targetProp) || null;
|
||||||
},
|
},
|
||||||
loadProvinceOptions() {
|
loadProvinceOptions() {
|
||||||
getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
|
return getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
|
||||||
this.provinceOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
this.provinceOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||||
this.findColumn('provinceCode').dicData = this.provinceOptions;
|
const provinceColumn = this.findColumn('provinceCode');
|
||||||
|
if (provinceColumn) {
|
||||||
|
provinceColumn.dicData = this.provinceOptions;
|
||||||
|
}
|
||||||
|
return this.provinceOptions;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
loadCityOptions(provinceCode) {
|
loadCityOptions(provinceCode) {
|
||||||
if (!provinceCode) {
|
if (!provinceCode) {
|
||||||
this.cityOptions = [];
|
this.cityOptions = [];
|
||||||
this.findColumn('cityCode').dicData = [];
|
const cityColumn = this.findColumn('cityCode');
|
||||||
return Promise.resolve();
|
if (cityColumn) {
|
||||||
|
cityColumn.dicData = [];
|
||||||
|
}
|
||||||
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
return getLazyTree(provinceCode).then(res => {
|
return getLazyTree(provinceCode).then(res => {
|
||||||
this.cityOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
this.cityOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||||
this.findColumn('cityCode').dicData = this.cityOptions;
|
const cityColumn = this.findColumn('cityCode');
|
||||||
|
if (cityColumn) {
|
||||||
|
cityColumn.dicData = this.cityOptions;
|
||||||
|
}
|
||||||
|
return this.cityOptions;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
loadDistrictOptions(cityCode) {
|
loadDistrictOptions(cityCode) {
|
||||||
if (!cityCode) {
|
if (!cityCode) {
|
||||||
this.districtOptions = [];
|
this.districtOptions = [];
|
||||||
this.findColumn('districtCode').dicData = [];
|
const districtColumn = this.findColumn('districtCode');
|
||||||
return Promise.resolve();
|
if (districtColumn) {
|
||||||
|
districtColumn.dicData = [];
|
||||||
|
}
|
||||||
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
this.districtLoading = true;
|
this.districtLoading = true;
|
||||||
return getLazyTree(cityCode)
|
return getLazyTree(cityCode)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.districtOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
this.districtOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||||
this.findColumn('districtCode').dicData = this.districtOptions;
|
const districtColumn = this.findColumn('districtCode');
|
||||||
|
if (districtColumn) {
|
||||||
|
districtColumn.dicData = this.districtOptions;
|
||||||
|
}
|
||||||
|
return this.districtOptions;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.districtLoading = false;
|
this.districtLoading = false;
|
||||||
@@ -652,8 +698,272 @@ export default {
|
|||||||
handleCoordinateInput(prop, value) {
|
handleCoordinateInput(prop, value) {
|
||||||
this.form[prop] = normalizeCoordinateInput(value);
|
this.form[prop] = normalizeCoordinateInput(value);
|
||||||
},
|
},
|
||||||
|
openAddressMapPicker() {
|
||||||
|
this.addressMapPickerVisible = true;
|
||||||
|
},
|
||||||
|
formatMapCoordinate(value) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const number = Number(value);
|
||||||
|
if (!Number.isFinite(number)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return normalizeCoordinateInput(number.toFixed(6));
|
||||||
|
},
|
||||||
|
patchDialogForm(patch = {}) {
|
||||||
|
Object.assign(this.form, patch);
|
||||||
|
const tableForm = this.$refs.crud?.tableForm;
|
||||||
|
if (tableForm && tableForm !== this.form) {
|
||||||
|
Object.assign(tableForm, patch);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleAddressMapConfirm(payload) {
|
||||||
|
const selection = typeof payload === 'string' ? { address: payload } : payload || {};
|
||||||
|
const longitude = this.formatMapCoordinate(selection.longitude);
|
||||||
|
const latitude = this.formatMapCoordinate(selection.latitude);
|
||||||
|
const patch = {};
|
||||||
|
if (selection.address) {
|
||||||
|
patch.detailAddress = selection.address;
|
||||||
|
}
|
||||||
|
if (longitude !== '' && latitude !== '') {
|
||||||
|
patch.longitude = longitude;
|
||||||
|
patch.latitude = latitude;
|
||||||
|
}
|
||||||
|
if (Object.keys(patch).length) {
|
||||||
|
this.patchDialogForm(patch);
|
||||||
|
}
|
||||||
|
this.applyMapRegionSelection(selection);
|
||||||
|
},
|
||||||
|
normalizeAmapCityName(city, province) {
|
||||||
|
if (Array.isArray(city)) {
|
||||||
|
return String(province || '').trim();
|
||||||
|
}
|
||||||
|
const cityName = String(city || '').trim();
|
||||||
|
return cityName || String(province || '').trim();
|
||||||
|
},
|
||||||
|
buildRegionLookupCodes(adcode) {
|
||||||
|
const code = String(adcode || '').trim();
|
||||||
|
if (!code) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const codes = [code];
|
||||||
|
if (/^\d{6}$/.test(code)) {
|
||||||
|
if (code.endsWith('00')) {
|
||||||
|
const cityCode = code.slice(0, 4);
|
||||||
|
if (cityCode && !codes.includes(cityCode)) {
|
||||||
|
codes.push(cityCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (code.endsWith('0000')) {
|
||||||
|
const provinceCode = code.slice(0, 2);
|
||||||
|
if (provinceCode && !codes.includes(provinceCode)) {
|
||||||
|
codes.push(provinceCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return codes;
|
||||||
|
},
|
||||||
|
fetchRegionDetailByCodes(codes = []) {
|
||||||
|
const tryNext = index => {
|
||||||
|
if (index >= codes.length) {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
return getRegionDetail(codes[index])
|
||||||
|
.then(res => {
|
||||||
|
const detail = res.data && res.data.data;
|
||||||
|
if (detail && detail.code) {
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
return tryNext(index + 1);
|
||||||
|
})
|
||||||
|
.catch(() => tryNext(index + 1));
|
||||||
|
};
|
||||||
|
return tryNext(0);
|
||||||
|
},
|
||||||
|
resolveProvinceCode(provinceName) {
|
||||||
|
const name = String(provinceName || '').trim();
|
||||||
|
if (!name) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const province = this.provinceOptions.find(
|
||||||
|
item => item.title === name || item.id === name || String(item.title).includes(name)
|
||||||
|
);
|
||||||
|
return province ? province.id : '';
|
||||||
|
},
|
||||||
|
resolveCityCode(cityName, regionCode) {
|
||||||
|
const name = String(cityName || '').trim();
|
||||||
|
const code = String(regionCode || '').trim();
|
||||||
|
const city = this.cityOptions.find(
|
||||||
|
item => item.title === name || item.id === code || item.id === name
|
||||||
|
);
|
||||||
|
return city ? city.id : '';
|
||||||
|
},
|
||||||
|
resolveDistrictCode(districtName, regionCode) {
|
||||||
|
const name = String(districtName || '').trim();
|
||||||
|
const code = String(regionCode || '').trim();
|
||||||
|
const district = this.districtOptions.find(
|
||||||
|
item => item.title === name || item.id === code || item.id === name
|
||||||
|
);
|
||||||
|
return district ? district.id : code || '';
|
||||||
|
},
|
||||||
|
applyRegionDetail(detail = {}) {
|
||||||
|
const provinceName = detail.provinceName || '';
|
||||||
|
const cityName = detail.cityName || '';
|
||||||
|
const districtName = detail.districtName || detail.name || '';
|
||||||
|
return this.loadProvinceOptions()
|
||||||
|
.then(() => {
|
||||||
|
const provinceCode =
|
||||||
|
String(detail.provinceCode || '').trim() || this.resolveProvinceCode(provinceName);
|
||||||
|
const province = this.provinceOptions.find(item => item.id === provinceCode);
|
||||||
|
this.patchDialogForm({
|
||||||
|
provinceCode: provinceCode || undefined,
|
||||||
|
provinceName: province ? province.title : provinceName,
|
||||||
|
});
|
||||||
|
if (!provinceCode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.loadCityOptions(provinceCode);
|
||||||
|
})
|
||||||
|
.then(cities => {
|
||||||
|
if (!cities) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const cityCode =
|
||||||
|
String(detail.cityCode || '').trim() ||
|
||||||
|
this.resolveCityCode(cityName, detail.districtCode || detail.code);
|
||||||
|
const city = this.cityOptions.find(item => item.id === cityCode);
|
||||||
|
this.patchDialogForm({
|
||||||
|
cityCode: cityCode || undefined,
|
||||||
|
cityName: city ? city.title : cityName,
|
||||||
|
});
|
||||||
|
this.ensureCityOption({
|
||||||
|
cityCode,
|
||||||
|
cityName: city ? city.title : cityName,
|
||||||
|
});
|
||||||
|
if (!cityCode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.loadDistrictOptions(cityCode);
|
||||||
|
})
|
||||||
|
.then(districts => {
|
||||||
|
if (!districts) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const districtCode = this.resolveDistrictCode(
|
||||||
|
districtName,
|
||||||
|
detail.districtCode || detail.code
|
||||||
|
);
|
||||||
|
this.patchDialogForm({
|
||||||
|
districtCode: districtCode || undefined,
|
||||||
|
});
|
||||||
|
this.handleDistrictChange(districtCode);
|
||||||
|
this.ensureDistrictOption({
|
||||||
|
districtCode,
|
||||||
|
districtName: this.form.districtName || districtName,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
applyMapRegionByNames(selection = {}) {
|
||||||
|
const provinceName = String(selection.province || selection.provinceName || '').trim();
|
||||||
|
const cityName = this.normalizeAmapCityName(
|
||||||
|
selection.city || selection.cityName,
|
||||||
|
provinceName
|
||||||
|
);
|
||||||
|
const districtName = String(selection.district || selection.districtName || '').trim();
|
||||||
|
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||||
|
// 本页无国家字段:国内 adcode 走中国省份树;无匹配则不强制回填
|
||||||
|
if (
|
||||||
|
!provinceName &&
|
||||||
|
!cityName &&
|
||||||
|
!districtName &&
|
||||||
|
!(/^\d{6}$/.test(adcode) || String(selection.country || selection.countryName || '').includes('中国'))
|
||||||
|
) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return this.loadProvinceOptions()
|
||||||
|
.then(() => {
|
||||||
|
const provinceCode = this.resolveProvinceCode(provinceName);
|
||||||
|
const province = this.provinceOptions.find(item => item.id === provinceCode);
|
||||||
|
this.patchDialogForm({
|
||||||
|
provinceCode: provinceCode || undefined,
|
||||||
|
provinceName: province ? province.title : provinceName,
|
||||||
|
});
|
||||||
|
if (!provinceCode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.loadCityOptions(provinceCode);
|
||||||
|
})
|
||||||
|
.then(cities => {
|
||||||
|
if (!cities) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const cityCode = this.resolveCityCode(cityName, adcode);
|
||||||
|
const city = this.cityOptions.find(item => item.id === cityCode);
|
||||||
|
this.patchDialogForm({
|
||||||
|
cityCode: cityCode || undefined,
|
||||||
|
cityName: city ? city.title : cityName,
|
||||||
|
});
|
||||||
|
this.ensureCityOption({
|
||||||
|
cityCode,
|
||||||
|
cityName: city ? city.title : cityName,
|
||||||
|
});
|
||||||
|
if (!cityCode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.loadDistrictOptions(cityCode);
|
||||||
|
})
|
||||||
|
.then(districts => {
|
||||||
|
if (!districts) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const districtCode = this.resolveDistrictCode(districtName, adcode);
|
||||||
|
this.patchDialogForm({
|
||||||
|
districtCode: districtCode || undefined,
|
||||||
|
});
|
||||||
|
this.handleDistrictChange(districtCode);
|
||||||
|
this.ensureDistrictOption({
|
||||||
|
districtCode,
|
||||||
|
districtName: this.form.districtName || districtName,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
applyMapRegionSelection(selection = {}) {
|
||||||
|
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||||
|
const hasNameHint = Boolean(
|
||||||
|
selection.country ||
|
||||||
|
selection.countryName ||
|
||||||
|
selection.province ||
|
||||||
|
selection.provinceName ||
|
||||||
|
selection.city ||
|
||||||
|
selection.cityName ||
|
||||||
|
selection.district ||
|
||||||
|
selection.districtName
|
||||||
|
);
|
||||||
|
if (!adcode && !hasNameHint) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
this.regionInitializing = true;
|
||||||
|
const lookupCodes = this.buildRegionLookupCodes(adcode);
|
||||||
|
return this.fetchRegionDetailByCodes(lookupCodes)
|
||||||
|
.then(detail => {
|
||||||
|
if (detail) {
|
||||||
|
return this.applyRegionDetail(detail);
|
||||||
|
}
|
||||||
|
return this.applyMapRegionByNames(selection);
|
||||||
|
})
|
||||||
|
.catch(() => this.applyMapRegionByNames(selection))
|
||||||
|
.finally(() => {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.regionInitializing = false;
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
normalizeRow(row) {
|
normalizeRow(row) {
|
||||||
row.code = row.tmisCode ? `TL${row.tmisCode}` : row.code;
|
row.code = row.tmisCode ? `TL${row.tmisCode}` : row.code;
|
||||||
|
row.telegraphCode = String(row.telegraphCode || '').trim() || undefined;
|
||||||
row.regionCode = String(row.regionCode || '').trim();
|
row.regionCode = String(row.regionCode || '').trim();
|
||||||
row.regionName = String(row.regionName || '').trim();
|
row.regionName = String(row.regionName || '').trim();
|
||||||
row.districtCode = String(row.districtCode || '').trim();
|
row.districtCode = String(row.districtCode || '').trim();
|
||||||
@@ -686,14 +996,15 @@ export default {
|
|||||||
return records.some(item => item[prop] === row[prop] && String(item.id || '') !== rowId);
|
return records.some(item => item[prop] === row[prop] && String(item.id || '') !== rowId);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return Promise.all([
|
const uniqueChecks = [hasDuplicate({ tmisCode: row.tmisCode }, 'tmisCode')];
|
||||||
hasDuplicate({ tmisCode: row.tmisCode }, 'tmisCode'),
|
if (row.telegraphCode) {
|
||||||
hasDuplicate({ telegraphCode: row.telegraphCode }, 'telegraphCode'),
|
uniqueChecks.push(hasDuplicate({ telegraphCode: row.telegraphCode }, 'telegraphCode'));
|
||||||
]).then(([tmisExists, telegraphExists]) => {
|
}
|
||||||
if (tmisExists) {
|
return Promise.all(uniqueChecks).then(results => {
|
||||||
|
if (results[0]) {
|
||||||
return Promise.reject(new Error('该TMIS编码已存在'));
|
return Promise.reject(new Error('该TMIS编码已存在'));
|
||||||
}
|
}
|
||||||
if (telegraphExists) {
|
if (results[1]) {
|
||||||
return Promise.reject(new Error('电报略码格式不正确或已存在'));
|
return Promise.reject(new Error('电报略码格式不正确或已存在'));
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@@ -910,9 +1221,16 @@ export default {
|
|||||||
:deep(.el-table td .cell) {
|
:deep(.el-table td .cell) {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-address-input {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper),
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
+55
-42
@@ -26,6 +26,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="区域层级">
|
<el-form-item label="区域层级">
|
||||||
<el-select v-model="searchForm.regionLevel" clearable placeholder="请选择" style="width: 200px;">
|
<el-select v-model="searchForm.regionLevel" clearable placeholder="请选择" style="width: 200px;">
|
||||||
|
<el-option label="国家" :value="0" />
|
||||||
<el-option label="省份" :value="1" />
|
<el-option label="省份" :value="1" />
|
||||||
<el-option label="省市" :value="2" />
|
<el-option label="省市" :value="2" />
|
||||||
<el-option label="区县" :value="3" />
|
<el-option label="区县" :value="3" />
|
||||||
@@ -42,6 +43,13 @@
|
|||||||
</section-card>
|
</section-card>
|
||||||
|
|
||||||
<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"
|
||||||
@@ -112,14 +120,7 @@
|
|||||||
placeholder="请输入国家编码,如 +86"
|
placeholder="请输入国家编码,如 +86"
|
||||||
v-model="regionForm.code"
|
v-model="regionForm.code"
|
||||||
/>
|
/>
|
||||||
<el-input
|
<el-input v-else placeholder="请输入区划编号" v-model="regionForm.subCode" />
|
||||||
v-else-if="isProvinceRegion"
|
|
||||||
placeholder="请输入省级区划编号"
|
|
||||||
v-model="regionForm.subCode"
|
|
||||||
/>
|
|
||||||
<el-input v-else placeholder="请输入 区划子编号" v-model="regionForm.subCode">
|
|
||||||
<template #prepend>{{ regionForm.parentCode }}</template>
|
|
||||||
</el-input>
|
|
||||||
</template>
|
</template>
|
||||||
</avue-form>
|
</avue-form>
|
||||||
<el-dialog title="行政区划数据导入" append-to-body v-model="excelBox" width="555px">
|
<el-dialog title="行政区划数据导入" append-to-body v-model="excelBox" width="555px">
|
||||||
@@ -253,9 +254,10 @@ export default {
|
|||||||
dicUrl: '/blade-system/dict/dictionary?code=region',
|
dicUrl: '/blade-system/dict/dictionary?code=region',
|
||||||
dicFormatter: response => {
|
dicFormatter: response => {
|
||||||
const list = Array.isArray(response) ? response : response?.data;
|
const list = Array.isArray(response) ? response : response?.data;
|
||||||
return (Array.isArray(list) ? list : [])
|
return (Array.isArray(list) ? list : []).map(item => ({
|
||||||
.filter(item => Number(item.dictKey) !== 0)
|
...item,
|
||||||
.map(item => ({ ...item, dictKey: Number(item.dictKey) }));
|
dictKey: Number(item.dictKey),
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
label: 'dictValue',
|
label: 'dictValue',
|
||||||
@@ -410,9 +412,6 @@ export default {
|
|||||||
isCountryRegion() {
|
isCountryRegion() {
|
||||||
return Number(this.regionForm.regionLevel) === 0;
|
return Number(this.regionForm.regionLevel) === 0;
|
||||||
},
|
},
|
||||||
isProvinceRegion() {
|
|
||||||
return Number(this.regionForm.regionLevel) === 1;
|
|
||||||
},
|
|
||||||
isEditRegion() {
|
isEditRegion() {
|
||||||
return !validatenull(this.regionForm.originalCode);
|
return !validatenull(this.regionForm.originalCode);
|
||||||
},
|
},
|
||||||
@@ -476,10 +475,7 @@ export default {
|
|||||||
getDetail(this.treeCode).then(res => {
|
getDetail(this.treeCode).then(res => {
|
||||||
this.regionForm = res.data.data;
|
this.regionForm = res.data.data;
|
||||||
this.regionForm.originalCode = this.regionForm.code;
|
this.regionForm.originalCode = this.regionForm.code;
|
||||||
this.regionForm.subCode =
|
this.regionForm.subCode = this.regionForm.code;
|
||||||
this.regionForm.parentCode === this.topCode
|
|
||||||
? this.regionForm.code
|
|
||||||
: this.regionForm.code.replace(this.regionForm.parentCode, '');
|
|
||||||
this.loadParentOptions(this.regionForm.parentCode, this.regionForm.parentName);
|
this.loadParentOptions(this.regionForm.parentCode, this.regionForm.parentName);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -500,25 +496,31 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
addChildren() {
|
addChildren() {
|
||||||
if (validatenull(this.regionForm.code) || validatenull(this.regionForm.name)) {
|
const current = this.regionForm || {};
|
||||||
|
const parentCode = current.originalCode || current.code;
|
||||||
|
if (validatenull(parentCode) || validatenull(current.name)) {
|
||||||
this.$message.warning('请先选择一项区划');
|
this.$message.warning('请先选择一项区划');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Number(this.regionForm.regionLevel) >= 3) {
|
if (Number(current.regionLevel) >= 3) {
|
||||||
this.$message.warning('区县已是末级,不能新增下级');
|
this.$message.warning('区县已是末级,不能新增下级');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parentCode = this.regionForm.code;
|
const parentName = current.name;
|
||||||
const parentName = this.regionForm.name;
|
const parentGroupCode = current.parentCode || this.topCode;
|
||||||
const parentGroupCode = this.regionForm.parentCode || this.topCode;
|
|
||||||
this.setRegionColumnDisplay('code', true);
|
this.setRegionColumnDisplay('code', true);
|
||||||
this.regionForm.parentCode = parentCode;
|
this.regionForm = {
|
||||||
this.regionForm.parentName = parentName;
|
parentCode,
|
||||||
this.regionForm.code = '';
|
parentName,
|
||||||
this.regionForm.subCode = '';
|
code: '',
|
||||||
this.regionForm.originalCode = '';
|
subCode: '',
|
||||||
this.regionForm.name = '';
|
originalCode: '',
|
||||||
this.regionForm.regionLevel = Number(this.regionForm.regionLevel) + 1;
|
name: '',
|
||||||
|
regionLevel: Number(current.regionLevel) + 1,
|
||||||
|
sort: 1,
|
||||||
|
remark: '',
|
||||||
|
status: 1,
|
||||||
|
};
|
||||||
this.loadParentOptionsByGroup(parentGroupCode, parentCode, parentName);
|
this.loadParentOptionsByGroup(parentGroupCode, parentCode, parentName);
|
||||||
},
|
},
|
||||||
setRegionColumnDisplay(prop, display) {
|
setRegionColumnDisplay(prop, display) {
|
||||||
@@ -536,8 +538,7 @@ export default {
|
|||||||
if (this.isCountryRegion) {
|
if (this.isCountryRegion) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parentCode = this.isProvinceRegion ? '' : this.regionForm.parentCode || '';
|
this.regionForm.code = this.regionForm.subCode || '';
|
||||||
this.regionForm.code = parentCode + (this.regionForm.subCode || '');
|
|
||||||
},
|
},
|
||||||
getRootParentOptions() {
|
getRootParentOptions() {
|
||||||
return [
|
return [
|
||||||
@@ -586,16 +587,28 @@ export default {
|
|||||||
this.parentOptions = this.ensureSelectedParent(options, selectedCode, selectedName);
|
this.parentOptions = this.ensureSelectedParent(options, selectedCode, selectedName);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleSubmit(form, done) {
|
buildRegionPayload(form) {
|
||||||
if (Number(form.regionLevel) === 0) {
|
const source = validatenull(this.regionForm) ? form || {} : this.regionForm;
|
||||||
form.parentCode = this.topCode;
|
const regionLevel = Number(source.regionLevel);
|
||||||
form.ancestors = this.topCode;
|
const isNew = validatenull(source.originalCode);
|
||||||
form.subCode = form.code;
|
const payload = {
|
||||||
} else {
|
code: regionLevel === 0 ? source.code : source.subCode,
|
||||||
const parentCode = Number(form.regionLevel) === 1 ? '' : form.parentCode;
|
parentCode: regionLevel === 0 ? this.topCode : source.parentCode,
|
||||||
form.code = parentCode + form.subCode;
|
name: source.name,
|
||||||
|
regionLevel,
|
||||||
|
sort: source.sort,
|
||||||
|
remark: source.remark,
|
||||||
|
status: source.status == null || source.status === '' ? 1 : source.status,
|
||||||
|
};
|
||||||
|
if (!isNew) {
|
||||||
|
payload.originalCode = source.originalCode;
|
||||||
|
payload.dataSource = source.dataSource;
|
||||||
}
|
}
|
||||||
submit(form).then(
|
return payload;
|
||||||
|
},
|
||||||
|
handleSubmit(form, done) {
|
||||||
|
const payload = this.buildRegionPayload(form);
|
||||||
|
submit(payload).then(
|
||||||
() => {
|
() => {
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -774,6 +787,6 @@ export default {
|
|||||||
margin-bottom: 12px !important;
|
margin-bottom: 12px !important;
|
||||||
}
|
}
|
||||||
.el-form-item{
|
.el-form-item{
|
||||||
margin-bottom: 9px !important;
|
margin-bottom: 24px !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ import { exportBlob } from '@/api/common';
|
|||||||
import { getDeptTree } from '@/api/system/dept';
|
import { getDeptTree } from '@/api/system/dept';
|
||||||
import { config, excelOption, formatCargoValue, option } from '@/option/business/common-cargo';
|
import { config, excelOption, formatCargoValue, option } from '@/option/business/common-cargo';
|
||||||
import { getDictionary } from '@/api/system/dictbiz';
|
import { getDictionary } from '@/api/system/dictbiz';
|
||||||
import { getToken } from '@/utils/auth';
|
import { getToken, isAdministrator } 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';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
@@ -250,16 +250,14 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
isAdmin() {
|
isAdmin() {
|
||||||
const authority = this.userInfo && this.userInfo.authority;
|
return isAdministrator(this.userInfo);
|
||||||
return Array.isArray(authority)
|
|
||||||
? authority.includes('admin')
|
|
||||||
: String(authority || '').includes('admin');
|
|
||||||
},
|
},
|
||||||
ids() {
|
ids() {
|
||||||
return this.selectionList.map(item => item.id).join(',');
|
return this.selectionList.map(item => item.id).join(',');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.applyDefaultDeptSearch();
|
||||||
this.initDeptTree();
|
this.initDeptTree();
|
||||||
this.loadPriceUnitOptions();
|
this.loadPriceUnitOptions();
|
||||||
},
|
},
|
||||||
@@ -273,6 +271,26 @@ export default {
|
|||||||
if (Array.isArray(data)) return data;
|
if (Array.isArray(data)) return data;
|
||||||
return (data && data.records) || [];
|
return (data && data.records) || [];
|
||||||
},
|
},
|
||||||
|
defaultDeptId() {
|
||||||
|
// 用户可能绑定多个组织(逗号分隔),搜索下拉是单选,只取当前会话第一个组织 ID。
|
||||||
|
const raw = (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
|
||||||
|
return String(raw)
|
||||||
|
.split(',')
|
||||||
|
.map(item => item.trim())
|
||||||
|
.filter(Boolean)[0] || '';
|
||||||
|
},
|
||||||
|
applyDefaultDeptSearch() {
|
||||||
|
const deptId = this.defaultDeptId();
|
||||||
|
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
|
||||||
|
if (!deptId || !deptColumn) return;
|
||||||
|
deptColumn.searchValue = deptId;
|
||||||
|
this.query = { ...this.query, deptId };
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.crud?.searchForm) {
|
||||||
|
this.$refs.crud.searchForm.searchDeptId = deptId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
initDeptTree() {
|
initDeptTree() {
|
||||||
getDeptTree(this.userInfo.tenantId).then(res => {
|
getDeptTree(this.userInfo.tenantId).then(res => {
|
||||||
this.deptOptions = this.flattenDept(res.data.data || []);
|
this.deptOptions = this.flattenDept(res.data.data || []);
|
||||||
@@ -280,6 +298,18 @@ export default {
|
|||||||
if (deptColumn) {
|
if (deptColumn) {
|
||||||
deptColumn.dicData = this.deptOptions;
|
deptColumn.dicData = this.deptOptions;
|
||||||
}
|
}
|
||||||
|
// dicData 异步到位后再回填,避免下拉选项为空时清空已选组织,导致请求丢失 deptId。
|
||||||
|
const deptId = String(this.query.deptId || this.defaultDeptId() || '');
|
||||||
|
if (!deptId) return;
|
||||||
|
if (deptColumn) {
|
||||||
|
deptColumn.searchValue = deptId;
|
||||||
|
}
|
||||||
|
this.query = { ...this.query, deptId };
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.crud?.searchForm) {
|
||||||
|
this.$refs.crud.searchForm.searchDeptId = deptId;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
loadPriceUnitOptions() {
|
loadPriceUnitOptions() {
|
||||||
@@ -307,9 +337,13 @@ export default {
|
|||||||
flattenDept(tree, level = 0) {
|
flattenDept(tree, level = 0) {
|
||||||
const result = [];
|
const result = [];
|
||||||
tree.forEach(item => {
|
tree.forEach(item => {
|
||||||
|
// 雪花 ID 必须保持字符串,避免 JS Number 精度丢失导致筛错组织。
|
||||||
|
const id =
|
||||||
|
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
|
||||||
|
if (!id) return;
|
||||||
result.push({
|
result.push({
|
||||||
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
||||||
value: item.id,
|
value: id,
|
||||||
});
|
});
|
||||||
if (item.children && item.children.length) {
|
if (item.children && item.children.length) {
|
||||||
result.push(...this.flattenDept(item.children, level + 1));
|
result.push(...this.flattenDept(item.children, level + 1));
|
||||||
@@ -317,6 +351,19 @@ export default {
|
|||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
normalizeListQuery(params = {}) {
|
||||||
|
// 搜索条件优先于缓存的 query,避免默认组织 ID 覆盖用户新选择的组织。
|
||||||
|
const query = { ...this.query, ...params };
|
||||||
|
const rawDeptId = query.deptId ?? query.searchDeptId;
|
||||||
|
delete query.searchDeptId;
|
||||||
|
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
|
||||||
|
query.deptId = String(rawDeptId).split(',')[0].trim();
|
||||||
|
} else {
|
||||||
|
delete query.deptId;
|
||||||
|
}
|
||||||
|
query.allDept = this.isAdmin ? 1 : 0;
|
||||||
|
return query;
|
||||||
|
},
|
||||||
normalizeCargoTypeOptions(list = []) {
|
normalizeCargoTypeOptions(list = []) {
|
||||||
return list
|
return list
|
||||||
.map(item => ({
|
.map(item => ({
|
||||||
@@ -485,7 +532,7 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (row.cargoValue && !/^\d+(\.\d{1,2})?$/.test(String(row.cargoValue))) {
|
if (row.cargoValue && !/^\d+(\.\d{1,2})?$/.test(String(row.cargoValue))) {
|
||||||
this.$message.warning('货值只能输入数字,最多保留2位小数');
|
this.$message.warning('单价只能输入数字,最多保留2位小数');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!row.model) {
|
if (!row.model) {
|
||||||
@@ -619,13 +666,14 @@ export default {
|
|||||||
},
|
},
|
||||||
searchReset() {
|
searchReset() {
|
||||||
this.query = {};
|
this.query = {};
|
||||||
|
this.applyDefaultDeptSearch();
|
||||||
this.page.currentPage = 1;
|
this.page.currentPage = 1;
|
||||||
this.onLoad(this.page);
|
this.onLoad(this.page, this.query);
|
||||||
},
|
},
|
||||||
searchChange(params, done) {
|
searchChange(params, done) {
|
||||||
this.query = params;
|
this.query = this.normalizeListQuery(params);
|
||||||
this.page.currentPage = 1;
|
this.page.currentPage = 1;
|
||||||
this.onLoad(this.page, params);
|
this.onLoad(this.page, this.query);
|
||||||
done();
|
done();
|
||||||
},
|
},
|
||||||
selectionChange(list) {
|
selectionChange(list) {
|
||||||
@@ -648,8 +696,10 @@ export default {
|
|||||||
},
|
},
|
||||||
onLoad(page, params = {}) {
|
onLoad(page, params = {}) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
const query = this.normalizeListQuery(params);
|
||||||
|
this.query = query;
|
||||||
api
|
api
|
||||||
.getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: 0 })
|
.getList(page.currentPage, page.pageSize, query)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const result = res.data.data;
|
const result = res.data.data;
|
||||||
this.page.total = result.total;
|
this.page.total = result.total;
|
||||||
@@ -662,8 +712,7 @@ export default {
|
|||||||
},
|
},
|
||||||
buildExportParams() {
|
buildExportParams() {
|
||||||
return {
|
return {
|
||||||
...this.query,
|
...this.normalizeListQuery(this.query),
|
||||||
allDept: 0,
|
|
||||||
ids: this.ids,
|
ids: this.ids,
|
||||||
[this.website.tokenHeader]: getToken(),
|
[this.website.tokenHeader]: getToken(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -289,8 +289,8 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="map-picker-content">
|
<div class="map-picker-content">
|
||||||
<map-search-results :results="routeMapSearchResults" @select="selectRouteMapSearchResult" />
|
|
||||||
<div ref="routeAmap" class="common-route-page__map"></div>
|
<div ref="routeAmap" class="common-route-page__map"></div>
|
||||||
|
<map-search-results :results="routeMapSearchResults" @select="selectRouteMapSearchResult" />
|
||||||
</div>
|
</div>
|
||||||
<div class="common-route-page__map-info">
|
<div class="common-route-page__map-info">
|
||||||
<span>{{ routeMapStatus }}</span>
|
<span>{{ routeMapStatus }}</span>
|
||||||
@@ -321,8 +321,9 @@ import { exportBlob } from '@/api/common';
|
|||||||
import { getDeptTree } from '@/api/system/dept';
|
import { getDeptTree } from '@/api/system/dept';
|
||||||
import { addressTypeOptions } from '@/option/base/common-address';
|
import { addressTypeOptions } from '@/option/base/common-address';
|
||||||
import { config, excelOption, option } from '@/option/business/common-route';
|
import { config, excelOption, option } from '@/option/business/common-route';
|
||||||
import { getToken } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
|
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import { isMobile } from '@/utils/validate';
|
import { isMobile } from '@/utils/validate';
|
||||||
import { List, Search } from '@element-plus/icons-vue';
|
import { List, Search } from '@element-plus/icons-vue';
|
||||||
@@ -389,10 +390,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
isAdmin() {
|
isAdmin() {
|
||||||
const authority = this.userInfo && this.userInfo.authority;
|
return isAdministrator(this.userInfo);
|
||||||
return Array.isArray(authority)
|
|
||||||
? authority.includes('admin')
|
|
||||||
: String(authority || '').includes('admin');
|
|
||||||
},
|
},
|
||||||
ids() {
|
ids() {
|
||||||
return this.selectionList.map(item => item.id).join(',');
|
return this.selectionList.map(item => item.id).join(',');
|
||||||
@@ -417,19 +415,27 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
buildDeptTree(tree) {
|
buildDeptTree(tree) {
|
||||||
return tree.map(item => ({
|
return tree.map(item => {
|
||||||
|
// 雪花 ID 必须保持字符串,避免 JS Number 精度丢失导致筛错组织。
|
||||||
|
const id =
|
||||||
|
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
|
||||||
|
return {
|
||||||
label: item.title || item.deptName || item.name,
|
label: item.title || item.deptName || item.name,
|
||||||
value: item.id,
|
value: id,
|
||||||
children:
|
children:
|
||||||
item.children && item.children.length ? this.buildDeptTree(item.children) : undefined,
|
item.children && item.children.length ? this.buildDeptTree(item.children) : undefined,
|
||||||
}));
|
};
|
||||||
|
}).filter(item => item.value);
|
||||||
},
|
},
|
||||||
flattenDept(tree, level = 0) {
|
flattenDept(tree, level = 0) {
|
||||||
const result = [];
|
const result = [];
|
||||||
tree.forEach(item => {
|
tree.forEach(item => {
|
||||||
|
const id =
|
||||||
|
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
|
||||||
|
if (!id) return;
|
||||||
result.push({
|
result.push({
|
||||||
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
||||||
value: item.id,
|
value: id,
|
||||||
});
|
});
|
||||||
if (item.children && item.children.length) {
|
if (item.children && item.children.length) {
|
||||||
result.push(...this.flattenDept(item.children, level + 1));
|
result.push(...this.flattenDept(item.children, level + 1));
|
||||||
@@ -438,7 +444,26 @@ export default {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
defaultDeptId() {
|
defaultDeptId() {
|
||||||
return (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
|
const raw = (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
|
||||||
|
return (
|
||||||
|
String(raw)
|
||||||
|
.split(',')
|
||||||
|
.map(item => item.trim())
|
||||||
|
.filter(Boolean)[0] || ''
|
||||||
|
);
|
||||||
|
},
|
||||||
|
normalizeListQuery(params = {}) {
|
||||||
|
// 搜索条件优先于缓存的 query,避免默认组织 ID 覆盖用户新选择的组织。
|
||||||
|
const query = { ...this.query, ...params };
|
||||||
|
const rawDeptId = query.deptId ?? query.searchDeptId;
|
||||||
|
delete query.searchDeptId;
|
||||||
|
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
|
||||||
|
query.deptId = String(rawDeptId).split(',')[0].trim();
|
||||||
|
} else {
|
||||||
|
delete query.deptId;
|
||||||
|
}
|
||||||
|
query.allDept = this.isAdmin ? 1 : 0;
|
||||||
|
return query;
|
||||||
},
|
},
|
||||||
applyDefaultDeptSearch() {
|
applyDefaultDeptSearch() {
|
||||||
const deptId = this.defaultDeptId();
|
const deptId = this.defaultDeptId();
|
||||||
@@ -489,7 +514,7 @@ export default {
|
|||||||
this.addressLoading = true;
|
this.addressLoading = true;
|
||||||
getCommonAddressList(this.addressPage.currentPage, this.addressPage.pageSize, {
|
getCommonAddressList(this.addressPage.currentPage, this.addressPage.pageSize, {
|
||||||
...this.addressQuery,
|
...this.addressQuery,
|
||||||
allDept: 0,
|
allDept: this.isAdmin ? 1 : 0,
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const data = res.data.data || {};
|
const data = res.data.data || {};
|
||||||
@@ -609,30 +634,29 @@ export default {
|
|||||||
this.$message.warning('请输入地址关键词');
|
this.$message.warning('请输入地址关键词');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loadAmap()
|
if (this.routeMapLoading) return;
|
||||||
.then(() => {
|
this.routeMapSearchResults = [];
|
||||||
this.routeMapLoading = true;
|
this.routeMapLoading = true;
|
||||||
return this.ensureRouteAmapGeocoder();
|
this.loadAmap()
|
||||||
})
|
.then(() => searchAmapPlaceText(keyword))
|
||||||
.then(() => this.runAmapGeocode('location', keyword))
|
.then(pois => {
|
||||||
.then(result => {
|
this.routeMapSearchResults = normalizeMapSearchResults(pois, keyword);
|
||||||
this.routeMapSearchResults = (result.geocodes || []).map((item, index) => ({
|
const first = this.routeMapSearchResults[0];
|
||||||
id: item.id || index,
|
if (!first?.location) {
|
||||||
name: item.formattedAddress || keyword,
|
|
||||||
address: item.formattedAddress || keyword,
|
|
||||||
location: item.location,
|
|
||||||
}));
|
|
||||||
const point = this.resolveMapPoint(result);
|
|
||||||
if (!point) {
|
|
||||||
this.routeMapStatus = '未找到匹配地址';
|
this.routeMapStatus = '未找到匹配地址';
|
||||||
this.$message.warning('地图搜索无匹配地址');
|
this.$message.warning('地图搜索无匹配地址');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.pickRouteMapPoint(point, keyword);
|
return this.$nextTick().then(() => {
|
||||||
|
if (typeof this.routeAmap?.resize === 'function') {
|
||||||
|
this.routeAmap.resize();
|
||||||
|
}
|
||||||
|
this.pickRouteMapPoint(first.location, first.address || first.name || keyword);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.routeMapStatus = '地图搜索失败';
|
this.routeMapStatus = '未找到匹配地址';
|
||||||
this.$message.error('地图搜索失败,请稍后重试');
|
this.$message.warning('地图搜索无匹配地址');
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.routeMapLoading = false;
|
this.routeMapLoading = false;
|
||||||
@@ -1002,9 +1026,9 @@ export default {
|
|||||||
this.onLoad(this.page, this.query);
|
this.onLoad(this.page, this.query);
|
||||||
},
|
},
|
||||||
searchChange(params, done) {
|
searchChange(params, done) {
|
||||||
this.query = params;
|
this.query = this.normalizeListQuery(params);
|
||||||
this.page.currentPage = 1;
|
this.page.currentPage = 1;
|
||||||
this.onLoad(this.page, params);
|
this.onLoad(this.page, this.query);
|
||||||
done();
|
done();
|
||||||
},
|
},
|
||||||
selectionChange(list) {
|
selectionChange(list) {
|
||||||
@@ -1027,8 +1051,10 @@ export default {
|
|||||||
},
|
},
|
||||||
onLoad(page, params = {}) {
|
onLoad(page, params = {}) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
const query = this.normalizeListQuery(params);
|
||||||
|
this.query = query;
|
||||||
api
|
api
|
||||||
.getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: 0 })
|
.getList(page.currentPage, page.pageSize, query)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const result = res.data.data;
|
const result = res.data.data;
|
||||||
this.page.total = result.total;
|
this.page.total = result.total;
|
||||||
@@ -1041,8 +1067,7 @@ export default {
|
|||||||
},
|
},
|
||||||
buildExportParams() {
|
buildExportParams() {
|
||||||
return {
|
return {
|
||||||
...this.query,
|
...this.normalizeListQuery(this.query),
|
||||||
allDept: 0,
|
|
||||||
ids: this.ids,
|
ids: this.ids,
|
||||||
[this.website.tokenHeader]: getToken(),
|
[this.website.tokenHeader]: getToken(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -162,16 +162,16 @@
|
|||||||
v-model="row.billingUnit"
|
v-model="row.billingUnit"
|
||||||
clearable
|
clearable
|
||||||
filterable
|
filterable
|
||||||
:disabled="!row.billingElement"
|
:disabled="!canSelectBillingUnit(row)"
|
||||||
:loading="unitLoading"
|
:loading="unitLoading"
|
||||||
:placeholder="row.billingElement ? '请选择' : '请先选择计费要素'"
|
:placeholder="billingUnitPlaceholder(row)"
|
||||||
><el-option
|
><el-option
|
||||||
v-for="item in unitOptionsFor(row)"
|
v-for="item in unitOptionsFor(row)"
|
||||||
:key="item.id || item.value"
|
:key="item.id || item.value"
|
||||||
:label="item.label"
|
:label="item.label"
|
||||||
:value="item.value" /></el-select></template
|
:value="item.value" /></el-select></template
|
||||||
></el-table-column>
|
></el-table-column>
|
||||||
<el-table-column label="单价(元)" width="180"
|
<el-table-column :label="unitPriceColumnLabel" width="180"
|
||||||
><template #default="{ row }"
|
><template #default="{ row }"
|
||||||
><div v-if="readonly && usesRangeUnitPrice(row)" class="limit-list">
|
><div v-if="readonly && usesRangeUnitPrice(row)" class="limit-list">
|
||||||
<span v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex">{{
|
<span v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex">{{
|
||||||
@@ -379,13 +379,24 @@ import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
|
|||||||
import { InfoFilled } from '@element-plus/icons-vue';
|
import { InfoFilled } from '@element-plus/icons-vue';
|
||||||
import { getDictionary } from '@/api/system/dictbiz';
|
import { getDictionary } from '@/api/system/dictbiz';
|
||||||
import SectionCard from '@/components/section-card/main.vue';
|
import SectionCard from '@/components/section-card/main.vue';
|
||||||
|
import { currencyUnit } from '@/utils/currency';
|
||||||
|
|
||||||
const clone = value => JSON.parse(JSON.stringify(value));
|
const clone = value => JSON.parse(JSON.stringify(value));
|
||||||
/** 计费要素与计量单位维度的对应关系 */
|
/** 计费要素与计量单位维度的对应关系(这些要素可选计费单位) */
|
||||||
const BILLING_ELEMENT_DIMENSION_MAP = {
|
const BILLING_ELEMENT_DIMENSION_MAP = {
|
||||||
按重量: '重量',
|
按重量: '重量',
|
||||||
按体积: '体积',
|
按体积: '体积',
|
||||||
按车辆: '数量',
|
按数量: '数量',
|
||||||
|
按里程: '里程',
|
||||||
|
'按吨·公里': '吨·公里',
|
||||||
|
'固定金额(整单一口价)': '整车',
|
||||||
|
};
|
||||||
|
const BILLING_UNIT_SELECTABLE_ELEMENTS = Object.keys(BILLING_ELEMENT_DIMENSION_MAP);
|
||||||
|
/** 对应维度下无计量单位时的默认计费单位 */
|
||||||
|
const DEFAULT_BILLING_UNIT_MAP = {
|
||||||
|
按里程: '公里',
|
||||||
|
'按吨·公里': '吨·公里',
|
||||||
|
'固定金额(整单一口价)': '单',
|
||||||
};
|
};
|
||||||
const defaultRule = () => ({
|
const defaultRule = () => ({
|
||||||
feeType: '',
|
feeType: '',
|
||||||
@@ -421,6 +432,7 @@ export default {
|
|||||||
value: { type: Object, default: () => ({}) },
|
value: { type: Object, default: () => ({}) },
|
||||||
index: { type: Number, default: -1 },
|
index: { type: Number, default: -1 },
|
||||||
readonly: Boolean,
|
readonly: Boolean,
|
||||||
|
settlementCurrency: { type: String, default: 'RMB' },
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue', 'save'],
|
emits: ['update:modelValue', 'save'],
|
||||||
data() {
|
data() {
|
||||||
@@ -436,7 +448,6 @@ export default {
|
|||||||
billingElements: [
|
billingElements: [
|
||||||
'按重量',
|
'按重量',
|
||||||
'按体积',
|
'按体积',
|
||||||
'按车辆',
|
|
||||||
'按里程',
|
'按里程',
|
||||||
'按吨·公里',
|
'按吨·公里',
|
||||||
'固定金额(整单一口价)',
|
'固定金额(整单一口价)',
|
||||||
@@ -445,7 +456,6 @@ export default {
|
|||||||
typeMap: {
|
typeMap: {
|
||||||
按重量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
|
按重量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
|
||||||
按体积: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
|
按体积: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
|
||||||
按车辆: ['固定单价'],
|
|
||||||
按里程: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
|
按里程: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
|
||||||
'按吨·公里': ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
|
'按吨·公里': ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
|
||||||
'固定金额(整单一口价)': ['固定一口价'],
|
'固定金额(整单一口价)': ['固定一口价'],
|
||||||
@@ -492,6 +502,9 @@ export default {
|
|||||||
emitPath: true,
|
emitPath: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
unitPriceColumnLabel() {
|
||||||
|
return `单价(${currencyUnit(this.settlementCurrency)})`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.loadDictionaries();
|
this.loadDictionaries();
|
||||||
@@ -560,6 +573,7 @@ export default {
|
|||||||
next.limitRanges = next.limitRanges.map(item => ({ ...item, minimumBillingWeight: '' }));
|
next.limitRanges = next.limitRanges.map(item => ({ ...item, minimumBillingWeight: '' }));
|
||||||
this.syncLegacyLimit(next);
|
this.syncLegacyLimit(next);
|
||||||
next.matchCondition = { ...defaultRule().matchCondition, ...(rule.matchCondition || {}) };
|
next.matchCondition = { ...defaultRule().matchCondition, ...(rule.matchCondition || {}) };
|
||||||
|
this.syncBillingUnit(next);
|
||||||
return next;
|
return next;
|
||||||
},
|
},
|
||||||
normalizeRanges(row) {
|
normalizeRanges(row) {
|
||||||
@@ -646,36 +660,61 @@ export default {
|
|||||||
}),
|
}),
|
||||||
]).finally(() => {
|
]).finally(() => {
|
||||||
this.unitLoading = false;
|
this.unitLoading = false;
|
||||||
|
(this.draft.rules || []).forEach(row => this.syncBillingUnit(row));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
measurementDimension(billingElement) {
|
measurementDimension(billingElement) {
|
||||||
return BILLING_ELEMENT_DIMENSION_MAP[billingElement] || '';
|
return BILLING_ELEMENT_DIMENSION_MAP[billingElement] || '';
|
||||||
},
|
},
|
||||||
|
defaultBillingUnit(billingElement) {
|
||||||
|
return DEFAULT_BILLING_UNIT_MAP[String(billingElement || '').trim()] || '';
|
||||||
|
},
|
||||||
|
canSelectBillingUnit(row) {
|
||||||
|
return BILLING_UNIT_SELECTABLE_ELEMENTS.includes(String(row?.billingElement || '').trim());
|
||||||
|
},
|
||||||
|
billingUnitPlaceholder(row) {
|
||||||
|
if (!row?.billingElement) return '请先选择计费要素';
|
||||||
|
if (!this.canSelectBillingUnit(row)) return '当前计费要素无需选择';
|
||||||
|
return '请选择';
|
||||||
|
},
|
||||||
unitOptionsFor(row) {
|
unitOptionsFor(row) {
|
||||||
|
if (!this.canSelectBillingUnit(row)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const dimension = this.measurementDimension(row?.billingElement);
|
const dimension = this.measurementDimension(row?.billingElement);
|
||||||
if (dimension) {
|
const defaultUnit = this.defaultBillingUnit(row?.billingElement);
|
||||||
return this.measurementUnits
|
const options = dimension
|
||||||
|
? this.measurementUnits
|
||||||
.filter(item => String(item.dimension || '').trim() === dimension)
|
.filter(item => String(item.dimension || '').trim() === dimension)
|
||||||
.map(item => ({
|
.map(item => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
label: item.unitName,
|
label: item.unitName,
|
||||||
value: item.unitName,
|
value: item.unitName,
|
||||||
}))
|
}))
|
||||||
.filter(item => item.value);
|
.filter(item => item.value)
|
||||||
|
: [];
|
||||||
|
// 对应维度未配置计量单位时,下拉仅展示默认值
|
||||||
|
if (!options.length && defaultUnit) {
|
||||||
|
return [{ id: `default-${defaultUnit}`, label: defaultUnit, value: defaultUnit }];
|
||||||
}
|
}
|
||||||
return (this.unitOptions || [])
|
return options;
|
||||||
.map(item => ({
|
|
||||||
id: item.id || item.dictKey || item.dictValue,
|
|
||||||
label: item.dictValue,
|
|
||||||
value: item.dictValue,
|
|
||||||
}))
|
|
||||||
.filter(item => item.value);
|
|
||||||
},
|
},
|
||||||
syncBillingUnit(row) {
|
syncBillingUnit(row) {
|
||||||
const options = this.unitOptionsFor(row);
|
if (!this.canSelectBillingUnit(row)) {
|
||||||
if (!options.some(item => String(item.value) === String(row.billingUnit || ''))) {
|
|
||||||
row.billingUnit = '';
|
row.billingUnit = '';
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
const options = this.unitOptionsFor(row);
|
||||||
|
const defaultUnit = this.defaultBillingUnit(row?.billingElement);
|
||||||
|
const current = String(row.billingUnit || '').trim();
|
||||||
|
if (options.some(item => String(item.value) === current)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (defaultUnit && options.some(item => String(item.value) === defaultUnit)) {
|
||||||
|
row.billingUnit = defaultUnit;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
row.billingUnit = options[0]?.value || defaultUnit || '';
|
||||||
},
|
},
|
||||||
feeTypeKey(row) {
|
feeTypeKey(row) {
|
||||||
const option = this.feeCategories.find(
|
const option = this.feeCategories.find(
|
||||||
@@ -892,7 +931,6 @@ export default {
|
|||||||
['taxRate', '税率'],
|
['taxRate', '税率'],
|
||||||
['billingElement', '计费要素'],
|
['billingElement', '计费要素'],
|
||||||
['billingType', '计费类型'],
|
['billingType', '计费类型'],
|
||||||
['billingUnit', '计费单位'],
|
|
||||||
];
|
];
|
||||||
for (const [i, row] of this.draft.rules.entries()) {
|
for (const [i, row] of this.draft.rules.entries()) {
|
||||||
const empty = required.find(
|
const empty = required.find(
|
||||||
@@ -902,6 +940,15 @@ export default {
|
|||||||
this.$message.warning(`第${i + 1}行${empty[1]}不能为空`);
|
this.$message.warning(`第${i + 1}行${empty[1]}不能为空`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
this.canSelectBillingUnit(row) &&
|
||||||
|
(row.billingUnit === undefined ||
|
||||||
|
row.billingUnit === null ||
|
||||||
|
String(row.billingUnit).trim() === '')
|
||||||
|
) {
|
||||||
|
this.$message.warning(`第${i + 1}行计费单位不能为空`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const feeItem = String(row.feeItem).trim();
|
const feeItem = String(row.feeItem).trim();
|
||||||
if (feeItemSet.has(feeItem)) {
|
if (feeItemSet.has(feeItem)) {
|
||||||
this.$message.warning(`费用项“${feeItem}”不能重复`);
|
this.$message.warning(`费用项“${feeItem}”不能重复`);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div class="contract-attachment-section">
|
||||||
<section
|
<section
|
||||||
|
v-if="!dialogOnly"
|
||||||
class="contract-manage-form__section contract-manage-form__section--panel contract-manage-form__section--attachment"
|
class="contract-manage-form__section contract-manage-form__section--panel contract-manage-form__section--attachment"
|
||||||
>
|
>
|
||||||
<div class="contract-manage-form__attachment-head">
|
<div class="contract-manage-form__attachment-head">
|
||||||
<div class="dialog-section-title">{{ title }}</div>
|
<div class="dialog-section-title">{{ title }}</div>
|
||||||
<el-button type="primary" :disabled="!rows.length" @click="batchDownload">批量下载</el-button>
|
<el-button type="primary" :disabled="!rows.length" @click="batchDownload"
|
||||||
|
>批量下载</el-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<el-table
|
<el-table
|
||||||
:data="rows"
|
:data="rows"
|
||||||
@@ -12,22 +16,23 @@
|
|||||||
class="contract-manage-form__attachment-table"
|
class="contract-manage-form__attachment-table"
|
||||||
@selection-change="selected = $event"
|
@selection-change="selected = $event"
|
||||||
>
|
>
|
||||||
<el-table-column v-if="!readonly" type="selection" width="55" align="center" />
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||||
<el-table-column v-if="attachmentType" label="附件类型" min-width="160" align="center">
|
<el-table-column v-if="attachmentType" label="附件类型" min-width="160" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row, $index }">
|
||||||
<span v-if="readonly || isRowLocked(row)">{{ row.fileType || '-' }}</span>
|
<span v-if="readonly || isRowLocked(row)">{{ row.fileType || '-' }}</span>
|
||||||
<el-select
|
<el-select
|
||||||
v-else
|
v-else
|
||||||
:model-value="row.fileType"
|
:model-value="row.fileType"
|
||||||
placeholder="请选择"
|
placeholder="请选择"
|
||||||
:teleported="false"
|
style="width: 100%"
|
||||||
:fit-input-width="true"
|
teleported
|
||||||
@update:model-value="value => updateRowFileType(row, value)"
|
clearable
|
||||||
|
@update:model-value="value => updateRowFileType($index, value)"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in contractAttachmentTypeOptions"
|
v-for="item in contractAttachmentTypeOptions"
|
||||||
:key="item.value"
|
:key="`${item.value}-${item.label}`"
|
||||||
:label="item.label"
|
:label="item.label"
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
/>
|
/>
|
||||||
@@ -40,19 +45,27 @@
|
|||||||
align="left"
|
align="left"
|
||||||
header-align="left"
|
header-align="left"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
|
class-name="is-ellipsis-link"
|
||||||
>
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-link type="primary" @click="preview?.(row, rows)">{{ attachmentName(row) }}</el-link>
|
<span
|
||||||
|
class="attachment-file-name"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="preview?.(row, rows)"
|
||||||
|
@keydown.enter="preview?.(row, rows)"
|
||||||
|
@keydown.space.prevent="preview?.(row, rows)"
|
||||||
|
>{{ attachmentName(row) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="description" label="附件描述" min-width="220">
|
<el-table-column v-if="description" label="附件描述" min-width="220">
|
||||||
<template #default="{ row }">
|
<template #default="{ row, $index }">
|
||||||
<span v-if="readonly || isRowLocked(row)">{{ row.description || '-' }}</span>
|
<span v-if="readonly || isRowLocked(row)">{{ row.description || '-' }}</span>
|
||||||
<el-input
|
<el-input
|
||||||
v-else
|
v-else
|
||||||
:model-value="row.description"
|
:model-value="row.description"
|
||||||
maxlength="200"
|
maxlength="200"
|
||||||
@update:model-value="value => updateRowDescription(row, value)"
|
@update:model-value="value => updateRowDescription($index, value)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -85,10 +98,11 @@
|
|||||||
tip="支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过50M"
|
tip="支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过50M"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
button-text="上传附件"
|
button-text="上传附件"
|
||||||
@update:model-value="handleChange"
|
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
|
@success="handleUploadSuccess"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="uploadDialogVisible"
|
v-model="uploadDialogVisible"
|
||||||
@@ -103,8 +117,18 @@
|
|||||||
<div class="contract-attachment-upload-dialog__body">
|
<div class="contract-attachment-upload-dialog__body">
|
||||||
<div class="contract-attachment-upload-dialog__field">
|
<div class="contract-attachment-upload-dialog__field">
|
||||||
<span class="contract-attachment-upload-dialog__label">附件位置</span>
|
<span class="contract-attachment-upload-dialog__label">附件位置</span>
|
||||||
<el-select :model-value="attachmentLocation" disabled style="width: 100%">
|
<el-select
|
||||||
<el-option :label="attachmentLocation" :value="attachmentLocation" />
|
v-model="uploadDialogLocation"
|
||||||
|
placeholder="请选择附件位置"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="syncUploadDialogTypeByLocation"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in resolvedLocationOptions"
|
||||||
|
:key="item"
|
||||||
|
:label="item"
|
||||||
|
:value="item"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="contract-attachment-upload-dialog__field">
|
<div class="contract-attachment-upload-dialog__field">
|
||||||
@@ -144,7 +168,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</section>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -154,14 +178,42 @@ import { getUploadHeaders } from '@/utils/upload';
|
|||||||
import { downloadFileByUrl } from '@/utils/util';
|
import { downloadFileByUrl } from '@/utils/util';
|
||||||
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||||||
|
|
||||||
const CONTRACT_ATTACHMENT_TYPE_OTHER = '其它文件';
|
const CONTRACT_ATTACHMENT_TYPE_OTHER = '其他文件';
|
||||||
|
/** 用印材料历史取值兼容别名 */
|
||||||
|
export const CONTRACT_SEAL_MATERIAL_LOCATION = '用印材料';
|
||||||
|
export const CONTRACT_SEAL_MATERIAL_TYPE_ALIASES = {
|
||||||
|
合同文件: CONTRACT_SEAL_MATERIAL_LOCATION,
|
||||||
|
};
|
||||||
|
/** 佐证材料附件位置(历史:其它附件) */
|
||||||
|
export const CONTRACT_SUPPORTING_ATTACHMENT_LOCATION = '附件(佐证材料等)';
|
||||||
|
export const CONTRACT_ATTACHMENT_TYPE_DICT = 'contract_attachment_type';
|
||||||
|
export const CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT = 'contract_supporting_attachment_type';
|
||||||
|
export const isContractSupportingAttachmentLocation = location => {
|
||||||
|
const text = String(location || '');
|
||||||
|
return (
|
||||||
|
text === CONTRACT_SUPPORTING_ATTACHMENT_LOCATION ||
|
||||||
|
text === '其它附件' ||
|
||||||
|
text === '其他附件'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const resolveAttachmentTypeDictCode = (location, fallback = CONTRACT_ATTACHMENT_TYPE_DICT) =>
|
||||||
|
isContractSupportingAttachmentLocation(location)
|
||||||
|
? CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT
|
||||||
|
: fallback || CONTRACT_ATTACHMENT_TYPE_DICT;
|
||||||
export const defaultContractAttachmentTypeOptions = [
|
export const defaultContractAttachmentTypeOptions = [
|
||||||
{ label: '合同文件', value: '合同文件' },
|
{ label: '用印材料', value: '用印材料' },
|
||||||
{ label: '双章归档文件', value: '双章归档文件' },
|
{ label: '双章归档文件', value: '双章归档文件' },
|
||||||
{ label: '其它文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
|
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
|
||||||
];
|
];
|
||||||
/** @deprecated 兼容旧引用,实际选项由业务字典 contract_attachment_type 动态加载 */
|
export const defaultContractSupportingAttachmentTypeOptions = [
|
||||||
|
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
|
||||||
|
];
|
||||||
|
/** @deprecated 兼容旧引用,实际选项由业务字典动态加载 */
|
||||||
export const contractAttachmentTypeOptions = defaultContractAttachmentTypeOptions;
|
export const contractAttachmentTypeOptions = defaultContractAttachmentTypeOptions;
|
||||||
|
export const isContractSealMaterialLocation = location => {
|
||||||
|
const text = String(location || '');
|
||||||
|
return text === CONTRACT_SEAL_MATERIAL_LOCATION || text === '合同文件';
|
||||||
|
};
|
||||||
export const attachmentFileTypes = [
|
export const attachmentFileTypes = [
|
||||||
'pdf',
|
'pdf',
|
||||||
'bmp',
|
'bmp',
|
||||||
@@ -201,21 +253,35 @@ const normalizeAttachmentTypeText = value => {
|
|||||||
.replace(/[\s_\-—–·•()()\[\]【】{}《》<>“”"'、,,。.]/g, '');
|
.replace(/[\s_\-—–·•()()\[\]【】{}《》<>“”"'、,,。.]/g, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isOtherAttachmentTypeOption = item => {
|
||||||
|
const text = `${item?.label || ''}${item?.value || ''}`;
|
||||||
|
return (
|
||||||
|
String(item?.value) === CONTRACT_ATTACHMENT_TYPE_OTHER ||
|
||||||
|
String(item?.value) === '其它文件' ||
|
||||||
|
text.includes('其他文件') ||
|
||||||
|
text.includes('其它文件') ||
|
||||||
|
text.includes('其他') ||
|
||||||
|
text.includes('其它')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const attachmentTypeKeywords = fileType => {
|
const attachmentTypeKeywords = fileType => {
|
||||||
if (fileType === '双章归档文件') return ['双章归档文件', '双章归档', '双章'];
|
const text = String(fileType || '');
|
||||||
if (fileType === '合同文件') return ['合同文件', '合同'];
|
if (text.includes('双章')) return ['双章归档文件', '双章归档', '双章'];
|
||||||
return [fileType];
|
if (
|
||||||
|
text.includes('用印') ||
|
||||||
|
(text.includes('合同') && !text.includes('其他') && !text.includes('其它'))
|
||||||
|
) {
|
||||||
|
return ['用印材料', '合同文件', '用印', '合同'];
|
||||||
|
}
|
||||||
|
if (isOtherAttachmentTypeOption({ label: text, value: text })) return [];
|
||||||
|
return text ? [text] : [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveOtherAttachmentType = (options = []) => {
|
const resolveOtherAttachmentType = (options = []) => {
|
||||||
const list = options.length ? options : defaultContractAttachmentTypeOptions;
|
const list = options.length ? options : defaultContractAttachmentTypeOptions;
|
||||||
const matched = list.find(
|
const matched = list.find(isOtherAttachmentTypeOption);
|
||||||
item =>
|
return matched?.value || CONTRACT_ATTACHMENT_TYPE_OTHER;
|
||||||
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 = (
|
export const resolveContractAttachmentType = (
|
||||||
@@ -223,15 +289,24 @@ export const resolveContractAttachmentType = (
|
|||||||
options = defaultContractAttachmentTypeOptions
|
options = defaultContractAttachmentTypeOptions
|
||||||
) => {
|
) => {
|
||||||
const list = options.length ? 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);
|
const otherType = resolveOtherAttachmentType(list);
|
||||||
|
const values = list.map(item => item.value);
|
||||||
|
const rawFileType = String(row.fileType || '');
|
||||||
|
const aliasedFileType = CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawFileType] || rawFileType;
|
||||||
|
if (values.includes(aliasedFileType)) return aliasedFileType;
|
||||||
|
if (values.includes(rawFileType)) return rawFileType;
|
||||||
|
const matchedByLabel = list.find(
|
||||||
|
item => String(item.label) === aliasedFileType || String(item.label) === rawFileType
|
||||||
|
);
|
||||||
|
if (matchedByLabel) return matchedByLabel.value;
|
||||||
|
const fileName = normalizeAttachmentTypeText(attachmentName(row));
|
||||||
if (!fileName) return otherType;
|
if (!fileName) return otherType;
|
||||||
const matched = list
|
const matched = list
|
||||||
.filter(item => item.value !== otherType)
|
.filter(item => !isOtherAttachmentTypeOption(item))
|
||||||
.flatMap(item =>
|
.flatMap(item =>
|
||||||
attachmentTypeKeywords(item.value).map(keyword => ({
|
attachmentTypeKeywords(item.value)
|
||||||
|
.concat(attachmentTypeKeywords(item.label))
|
||||||
|
.map(keyword => ({
|
||||||
value: item.value,
|
value: item.value,
|
||||||
keyword: normalizeAttachmentTypeText(keyword),
|
keyword: normalizeAttachmentTypeText(keyword),
|
||||||
}))
|
}))
|
||||||
@@ -239,6 +314,7 @@ export const resolveContractAttachmentType = (
|
|||||||
.filter(item => item.keyword)
|
.filter(item => item.keyword)
|
||||||
.sort((a, b) => b.keyword.length - a.keyword.length)
|
.sort((a, b) => b.keyword.length - a.keyword.length)
|
||||||
.find(item => fileName.includes(item.keyword));
|
.find(item => fileName.includes(item.keyword));
|
||||||
|
// 文件名未命中任何附件类型时,默认「其他文件」
|
||||||
return matched?.value || otherType;
|
return matched?.value || otherType;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -249,17 +325,32 @@ export const normalizeContractFileRows = (rows, options = defaultContractAttachm
|
|||||||
description: row.description || '',
|
description: row.description || '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mapDictOptions = response => {
|
const mapDictOptions = (response, dictCode = CONTRACT_ATTACHMENT_TYPE_DICT) => {
|
||||||
const data = response?.data?.data || response?.data || [];
|
const data = response?.data?.data || response?.data || [];
|
||||||
const records = Array.isArray(data) ? data : data.records || [];
|
const records = Array.isArray(data) ? data : data.records || [];
|
||||||
|
const applySealAlias = dictCode === CONTRACT_ATTACHMENT_TYPE_DICT;
|
||||||
return records
|
return records
|
||||||
.map(item => ({
|
.map(item => {
|
||||||
label: item.dictValue,
|
const rawLabel = String(item.dictValue || '');
|
||||||
value: item.dictKey,
|
const rawValue = String(item.dictKey || '');
|
||||||
}))
|
const label = applySealAlias
|
||||||
|
? CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawLabel] ||
|
||||||
|
(rawLabel === '合同文件' ? CONTRACT_SEAL_MATERIAL_LOCATION : rawLabel)
|
||||||
|
: rawLabel;
|
||||||
|
const value = applySealAlias
|
||||||
|
? CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawValue] ||
|
||||||
|
(rawValue === '合同文件' ? CONTRACT_SEAL_MATERIAL_LOCATION : rawValue)
|
||||||
|
: rawValue;
|
||||||
|
return { label, value };
|
||||||
|
})
|
||||||
.filter(item => item.label && item.value);
|
.filter(item => item.label && item.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultOptionsByDictCode = dictCode =>
|
||||||
|
dictCode === CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT
|
||||||
|
? [...defaultContractSupportingAttachmentTypeOptions]
|
||||||
|
: [...defaultContractAttachmentTypeOptions];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ContractAttachmentSection',
|
name: 'ContractAttachmentSection',
|
||||||
components: { UploadFilled },
|
components: { UploadFilled },
|
||||||
@@ -272,16 +363,25 @@ export default {
|
|||||||
lockApproved: Boolean,
|
lockApproved: Boolean,
|
||||||
markApprovedOnUpload: Boolean,
|
markApprovedOnUpload: Boolean,
|
||||||
useUploadDialog: Boolean,
|
useUploadDialog: Boolean,
|
||||||
attachmentLocation: { type: String, default: '合同文件' },
|
dialogOnly: Boolean,
|
||||||
|
attachmentLocation: { type: String, default: CONTRACT_SEAL_MATERIAL_LOCATION },
|
||||||
|
attachmentLocationOptions: { type: Array, default: null },
|
||||||
|
/** 附件类型业务字典 code,默认用印材料字典;佐证材料传 contract_supporting_attachment_type */
|
||||||
|
attachmentTypeDictCode: {
|
||||||
|
type: String,
|
||||||
|
default: CONTRACT_ATTACHMENT_TYPE_DICT,
|
||||||
|
},
|
||||||
preview: { type: Function, default: null },
|
preview: { type: Function, default: null },
|
||||||
},
|
},
|
||||||
emits: ['update:rows'],
|
emits: ['update:rows', 'upload-to-location', 'upload-confirmed'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selected: [],
|
selected: [],
|
||||||
attachmentFileTypes,
|
attachmentFileTypes,
|
||||||
contractAttachmentTypeOptions: [...defaultContractAttachmentTypeOptions],
|
contractAttachmentTypeOptions: [...defaultContractAttachmentTypeOptions],
|
||||||
|
activeAttachmentTypeDictCode: CONTRACT_ATTACHMENT_TYPE_DICT,
|
||||||
uploadDialogVisible: false,
|
uploadDialogVisible: false,
|
||||||
|
uploadDialogLocation: CONTRACT_SEAL_MATERIAL_LOCATION,
|
||||||
uploadDialogType: resolveOtherAttachmentType(defaultContractAttachmentTypeOptions),
|
uploadDialogType: resolveOtherAttachmentType(defaultContractAttachmentTypeOptions),
|
||||||
uploadDialogFiles: [],
|
uploadDialogFiles: [],
|
||||||
uploadDialogFileList: [],
|
uploadDialogFileList: [],
|
||||||
@@ -300,47 +400,186 @@ export default {
|
|||||||
defaultAttachmentType() {
|
defaultAttachmentType() {
|
||||||
return resolveOtherAttachmentType(this.contractAttachmentTypeOptions);
|
return resolveOtherAttachmentType(this.contractAttachmentTypeOptions);
|
||||||
},
|
},
|
||||||
|
resolvedAttachmentTypeDictCode() {
|
||||||
|
return this.attachmentTypeDictCode || CONTRACT_ATTACHMENT_TYPE_DICT;
|
||||||
|
},
|
||||||
|
resolvedLocationOptions() {
|
||||||
|
const options = (this.attachmentLocationOptions || []).filter(Boolean);
|
||||||
|
if (options.length) {
|
||||||
|
return options.map(item => {
|
||||||
|
if (item === '合同文件') return CONTRACT_SEAL_MATERIAL_LOCATION;
|
||||||
|
if (item === '其它附件' || item === '其他附件') {
|
||||||
|
return CONTRACT_SUPPORTING_ATTACHMENT_LOCATION;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [CONTRACT_SEAL_MATERIAL_LOCATION, CONTRACT_SUPPORTING_ATTACHMENT_LOCATION];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
attachmentTypeDictCode: {
|
||||||
|
immediate: false,
|
||||||
|
handler() {
|
||||||
|
this.loadAttachmentTypeOptions(this.resolvedAttachmentTypeDictCode, true);
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.loadAttachmentTypeOptions();
|
this.loadAttachmentTypeOptions(this.resolvedAttachmentTypeDictCode, true);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
attachmentName,
|
attachmentName,
|
||||||
loadAttachmentTypeOptions() {
|
currentUploadUserName() {
|
||||||
return getBizDictionary({ code: 'contract_attachment_type' })
|
const raw = this.$store.getters.userInfo;
|
||||||
|
const userInfo = raw && !Array.isArray(raw) ? raw : {};
|
||||||
|
// 与顶部栏一致优先 real_name;用印材料 / 佐证材料共用
|
||||||
|
return (
|
||||||
|
userInfo.real_name ||
|
||||||
|
userInfo.realName ||
|
||||||
|
userInfo.nick_name ||
|
||||||
|
userInfo.nickName ||
|
||||||
|
userInfo.user_name ||
|
||||||
|
userInfo.userName ||
|
||||||
|
userInfo.account ||
|
||||||
|
''
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loadAttachmentTypeOptions(dictCode = this.resolvedAttachmentTypeDictCode, alignRows = false) {
|
||||||
|
const code = dictCode || CONTRACT_ATTACHMENT_TYPE_DICT;
|
||||||
|
this.activeAttachmentTypeDictCode = code;
|
||||||
|
const fallbackOptions = defaultOptionsByDictCode(code);
|
||||||
|
return getBizDictionary({ code })
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const options = mapDictOptions(res);
|
// 切换字典期间若已发起更新请求,忽略过期响应
|
||||||
|
if (this.activeAttachmentTypeDictCode !== code) return;
|
||||||
|
const options = mapDictOptions(res, code);
|
||||||
if (options.length) {
|
if (options.length) {
|
||||||
this.contractAttachmentTypeOptions = options;
|
// 字典缺少「其他文件」时补上,保证未匹配文件名可默认选中
|
||||||
|
this.contractAttachmentTypeOptions = options.some(isOtherAttachmentTypeOption)
|
||||||
|
? options
|
||||||
|
: [...options, { label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER }];
|
||||||
|
} else {
|
||||||
|
this.contractAttachmentTypeOptions = fallbackOptions;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
!options.some(item => String(item.value) === String(this.uploadDialogType))
|
!this.contractAttachmentTypeOptions.some(
|
||||||
|
item => String(item.value) === String(this.uploadDialogType)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
this.uploadDialogType = this.defaultAttachmentType;
|
this.uploadDialogType = this.defaultAttachmentType;
|
||||||
}
|
}
|
||||||
|
// 字典加载后,把已有行的中文类型名对齐到 dictKey,保证下拉可选中
|
||||||
|
if (alignRows && this.attachmentType && (this.rows || []).length) {
|
||||||
|
const aligned = (this.rows || []).map(row => ({
|
||||||
|
...row,
|
||||||
|
fileType: resolveContractAttachmentType(row, this.contractAttachmentTypeOptions),
|
||||||
|
}));
|
||||||
|
const changed = aligned.some(
|
||||||
|
(row, index) => row.fileType !== this.rows[index]?.fileType
|
||||||
|
);
|
||||||
|
if (changed) this.update(aligned);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// 字典加载失败时保留默认选项
|
if (this.activeAttachmentTypeDictCode !== code) return;
|
||||||
|
this.contractAttachmentTypeOptions = fallbackOptions;
|
||||||
|
this.uploadDialogType = this.defaultAttachmentType;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
isRowLocked(row) {
|
isRowLocked(row) {
|
||||||
return this.lockApproved && isAttachmentApproved(row);
|
return this.lockApproved && isAttachmentApproved(row);
|
||||||
},
|
},
|
||||||
update(rows) {
|
update(rows) {
|
||||||
this.$emit('update:rows', rows);
|
this.$emit('update:rows', Array.isArray(rows) ? rows.map(item => ({ ...item })) : []);
|
||||||
},
|
},
|
||||||
updateRowFileType(row, value) {
|
enrichUploadedRow(row, existing) {
|
||||||
if (this.isRowLocked(row)) return;
|
const uploadUserName = this.currentUploadUserName();
|
||||||
row.fileType = value;
|
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||||
this.update([...this.rows]);
|
const next = {
|
||||||
|
...existing,
|
||||||
|
...row,
|
||||||
|
description: row.description || existing?.description || '',
|
||||||
|
// 新上传行始终写入当前用户;已有行保留原上传人
|
||||||
|
uploadUserName: existing?.uploadUserName || uploadUserName || row.uploadUserName || '',
|
||||||
|
uploadTime: existing?.uploadTime || row.uploadTime || uploadTime,
|
||||||
|
approved: existing?.approved || row.approved || false,
|
||||||
|
};
|
||||||
|
if (this.attachmentType) {
|
||||||
|
next.fileType = existing?.fileType
|
||||||
|
? existing.fileType
|
||||||
|
: resolveContractAttachmentType(
|
||||||
|
{ ...next, fileType: '' },
|
||||||
|
this.contractAttachmentTypeOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.markApprovedOnUpload && !existing) next.approved = true;
|
||||||
|
return next;
|
||||||
},
|
},
|
||||||
updateRowDescription(row, value) {
|
findExistingRow(row, rows = this.rows) {
|
||||||
if (this.isRowLocked(row)) return;
|
const fileUrl = attachmentUrl(row);
|
||||||
row.description = value;
|
return (rows || []).find(item => {
|
||||||
this.update([...this.rows]);
|
const sameUid = row.uid && item.uid && String(row.uid) === String(item.uid);
|
||||||
|
const sameUrl = fileUrl && attachmentUrl(item) === fileUrl;
|
||||||
|
return sameUid || sameUrl;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateRowFileType(index, value) {
|
||||||
|
const rows = [...(this.rows || [])];
|
||||||
|
const row = rows[index];
|
||||||
|
if (!row || this.isRowLocked(row)) return;
|
||||||
|
rows[index] = { ...row, fileType: value };
|
||||||
|
this.update(rows);
|
||||||
|
},
|
||||||
|
updateRowDescription(index, value) {
|
||||||
|
const rows = [...(this.rows || [])];
|
||||||
|
const row = rows[index];
|
||||||
|
if (!row || this.isRowLocked(row)) return;
|
||||||
|
rows[index] = { ...row, description: value };
|
||||||
|
this.update(rows);
|
||||||
|
},
|
||||||
|
resolveTypeByLocation(location) {
|
||||||
|
const options = this.contractAttachmentTypeOptions || [];
|
||||||
|
const locationText = String(location || '');
|
||||||
|
if (
|
||||||
|
isContractSupportingAttachmentLocation(locationText) ||
|
||||||
|
locationText.includes('佐证') ||
|
||||||
|
locationText.includes('其它') ||
|
||||||
|
locationText.includes('其他')
|
||||||
|
) {
|
||||||
|
return this.defaultAttachmentType;
|
||||||
|
}
|
||||||
|
const matched = options.find(
|
||||||
|
item =>
|
||||||
|
String(item.value) === locationText ||
|
||||||
|
String(item.label) === locationText ||
|
||||||
|
String(item.value).includes('用印') ||
|
||||||
|
String(item.label).includes('用印') ||
|
||||||
|
String(item.value).includes('合同') ||
|
||||||
|
String(item.label).includes('合同')
|
||||||
|
);
|
||||||
|
return matched?.value || options[0]?.value || this.defaultAttachmentType;
|
||||||
|
},
|
||||||
|
syncUploadDialogTypeByLocation() {
|
||||||
|
const dictCode = resolveAttachmentTypeDictCode(
|
||||||
|
this.uploadDialogLocation,
|
||||||
|
this.resolvedAttachmentTypeDictCode
|
||||||
|
);
|
||||||
|
const applyType = () => {
|
||||||
|
this.uploadDialogType = this.resolveTypeByLocation(this.uploadDialogLocation);
|
||||||
|
};
|
||||||
|
if (dictCode !== this.activeAttachmentTypeDictCode) {
|
||||||
|
this.loadAttachmentTypeOptions(dictCode, false).finally(applyType);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyType();
|
||||||
},
|
},
|
||||||
openUploadDialog() {
|
openUploadDialog() {
|
||||||
this.uploadDialogType = this.defaultAttachmentType;
|
const options = this.resolvedLocationOptions;
|
||||||
|
this.uploadDialogLocation = options.includes(this.attachmentLocation)
|
||||||
|
? this.attachmentLocation
|
||||||
|
: options[0] || this.attachmentLocation;
|
||||||
|
this.syncUploadDialogTypeByLocation();
|
||||||
this.uploadDialogFiles = [];
|
this.uploadDialogFiles = [];
|
||||||
this.uploadDialogFileList = [];
|
this.uploadDialogFileList = [];
|
||||||
this.uploadDialogVisible = true;
|
this.uploadDialogVisible = true;
|
||||||
@@ -348,7 +587,8 @@ export default {
|
|||||||
resetUploadDialog() {
|
resetUploadDialog() {
|
||||||
this.uploadDialogFiles = [];
|
this.uploadDialogFiles = [];
|
||||||
this.uploadDialogFileList = [];
|
this.uploadDialogFileList = [];
|
||||||
this.uploadDialogType = this.defaultAttachmentType;
|
this.uploadDialogLocation = this.attachmentLocation;
|
||||||
|
this.syncUploadDialogTypeByLocation();
|
||||||
},
|
},
|
||||||
beforeUpload(file) {
|
beforeUpload(file) {
|
||||||
const extension = String(file.name || '')
|
const extension = String(file.name || '')
|
||||||
@@ -391,6 +631,8 @@ export default {
|
|||||||
...this.uploadDialogFiles.filter(item => String(item.uid) !== String(file.uid)),
|
...this.uploadDialogFiles.filter(item => String(item.uid) !== String(file.uid)),
|
||||||
normalized,
|
normalized,
|
||||||
];
|
];
|
||||||
|
const locationText = String(this.uploadDialogLocation || '');
|
||||||
|
if (!(locationText.includes('其它') || locationText.includes('其他'))) {
|
||||||
this.uploadDialogType = resolveContractAttachmentType(
|
this.uploadDialogType = resolveContractAttachmentType(
|
||||||
{
|
{
|
||||||
...normalized,
|
...normalized,
|
||||||
@@ -398,6 +640,9 @@ export default {
|
|||||||
},
|
},
|
||||||
this.contractAttachmentTypeOptions
|
this.contractAttachmentTypeOptions
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
this.uploadDialogType = this.defaultAttachmentType;
|
||||||
|
}
|
||||||
this.$message.success('上传成功');
|
this.$message.success('上传成功');
|
||||||
},
|
},
|
||||||
handleDialogError() {
|
handleDialogError() {
|
||||||
@@ -416,57 +661,57 @@ export default {
|
|||||||
this.$message.warning('请先上传附件');
|
this.$message.warning('请先上传附件');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!this.uploadDialogLocation) {
|
||||||
|
this.$message.warning('请选择附件位置');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.uploadDialogType) {
|
if (!this.uploadDialogType) {
|
||||||
this.$message.warning('请选择附件类型');
|
this.$message.warning('请选择附件类型');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const uploadUserName = this.$store.getters.userInfo?.realName || '';
|
const uploadUserName = this.currentUploadUserName();
|
||||||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||||
const appended = this.uploadDialogFiles.map(row => ({
|
const appended = this.uploadDialogFiles.map(row => ({
|
||||||
...row,
|
...row,
|
||||||
fileType: this.uploadDialogType,
|
fileType: this.uploadDialogType,
|
||||||
description: row.description || '',
|
description: row.description || '',
|
||||||
uploadUserName: row.uploadUserName || uploadUserName,
|
uploadUserName: uploadUserName || row.uploadUserName || '',
|
||||||
uploadTime: row.uploadTime || uploadTime,
|
uploadTime: row.uploadTime || uploadTime,
|
||||||
...(this.markApprovedOnUpload ? { approved: true } : {}),
|
...(this.markApprovedOnUpload ? { approved: true } : {}),
|
||||||
}));
|
}));
|
||||||
this.update([...(this.rows || []), ...appended]);
|
// 统一交给父组件按附件位置写入对应列表,避免同位置双写或跨位置不显示
|
||||||
this.uploadDialogVisible = false;
|
this.$emit('upload-to-location', {
|
||||||
},
|
location: this.uploadDialogLocation,
|
||||||
handleChange(rows) {
|
rows: appended,
|
||||||
const uploadUserName = this.$store.getters.userInfo?.realName || '';
|
});
|
||||||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
this.uploadDialogVisible = false;
|
||||||
const existingRows = this.rows || [];
|
this.$emit('upload-confirmed', {
|
||||||
this.update(
|
location: this.uploadDialogLocation,
|
||||||
(rows || []).map(row => {
|
rows: appended,
|
||||||
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
|
handleChange(list) {
|
||||||
);
|
if (!Array.isArray(list)) return;
|
||||||
}
|
// 上传组件偶发回传空列表时,禁止清空已有表格数据
|
||||||
if (this.markApprovedOnUpload && !existing) next.approved = true;
|
if (!list.length) return;
|
||||||
return next;
|
const existingRows = this.rows || [];
|
||||||
})
|
const nextRows = list.map(row => {
|
||||||
);
|
const existing = this.findExistingRow(row, existingRows);
|
||||||
|
if (existing && this.isRowLocked(existing)) return { ...existing };
|
||||||
|
return this.enrichUploadedRow(row, existing);
|
||||||
|
});
|
||||||
|
existingRows.forEach(item => {
|
||||||
|
if (!this.isRowLocked(item)) return;
|
||||||
|
if (this.findExistingRow(item, nextRows)) return;
|
||||||
|
nextRows.unshift({ ...item });
|
||||||
|
});
|
||||||
|
this.update(nextRows);
|
||||||
|
},
|
||||||
|
handleUploadSuccess(file) {
|
||||||
|
const url = attachmentUrl(file);
|
||||||
|
if (!file || !url) return;
|
||||||
|
if (this.findExistingRow(file)) return;
|
||||||
|
this.update([...(this.rows || []), this.enrichUploadedRow(file)]);
|
||||||
},
|
},
|
||||||
remove(index) {
|
remove(index) {
|
||||||
const row = this.rows[index];
|
const row = this.rows[index];
|
||||||
@@ -474,7 +719,7 @@ export default {
|
|||||||
this.$message.warning('已审核通过的附件不允许删除');
|
this.$message.warning('已审核通过的附件不允许删除');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rows = [...this.rows];
|
const rows = [...(this.rows || [])];
|
||||||
rows.splice(index, 1);
|
rows.splice(index, 1);
|
||||||
this.update(rows);
|
this.update(rows);
|
||||||
},
|
},
|
||||||
@@ -487,7 +732,15 @@ export default {
|
|||||||
downloadFileByUrl(url, attachmentName(row));
|
downloadFileByUrl(url, attachmentName(row));
|
||||||
},
|
},
|
||||||
batchDownload() {
|
batchDownload() {
|
||||||
(this.selected.length ? this.selected : this.rows).forEach(this.download);
|
const sourceRows = this.selected.length ? this.selected : this.rows || [];
|
||||||
|
const downloadableRows = sourceRows.filter(row => attachmentUrl(row));
|
||||||
|
if (!downloadableRows.length) {
|
||||||
|
this.$message.warning(this.selected.length ? '所选附件暂无可下载地址' : '暂无可下载附件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
downloadableRows.forEach((row, index) => {
|
||||||
|
window.setTimeout(() => this.download(row), index * 400);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
formatSize(size) {
|
formatSize(size) {
|
||||||
const value = Number(size);
|
const value = Number(size);
|
||||||
@@ -504,7 +757,7 @@ export default {
|
|||||||
.contract-manage-form__section {
|
.contract-manage-form__section {
|
||||||
margin: 12px 0 0;
|
margin: 12px 0 0;
|
||||||
padding: 14px 16px 16px;
|
padding: 14px 16px 16px;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||||
@@ -527,6 +780,15 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contract-manage-form__attachment-table {
|
||||||
|
:deep(.el-table__body-wrapper),
|
||||||
|
:deep(.el-table__header-wrapper),
|
||||||
|
:deep(.el-table__cell),
|
||||||
|
:deep(.cell) {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.contract-manage-form__attachment-head {
|
.contract-manage-form__attachment-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-loading="loading" class="master-detail">
|
<div v-loading="loading" class="master-detail">
|
||||||
<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></div>
|
<div class="detail-heading">
|
||||||
|
<div>
|
||||||
|
<h2>多联总单详情 <span>|</span> {{ master.masterNo }}</h2>
|
||||||
|
<el-tag :type="statusType(master.businessStatus)">{{
|
||||||
|
statusName(master.businessStatus)
|
||||||
|
}}</el-tag>
|
||||||
|
<el-tag v-if="transportFlowLabel" type="primary" class="transport-flow-tag">{{
|
||||||
|
transportFlowLabel
|
||||||
|
}}</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<dl class="detail-meta">
|
<dl class="detail-meta">
|
||||||
<div>
|
<div>
|
||||||
<dt>客户</dt>
|
<dt>客户</dt>
|
||||||
@@ -34,13 +44,16 @@
|
|||||||
<dt>附件</dt>
|
<dt>附件</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<template v-if="attachments.length">
|
<template v-if="attachments.length">
|
||||||
<el-link
|
<span
|
||||||
v-for="file in attachments"
|
v-for="file in attachments"
|
||||||
:key="file.url || file.link || file.name || file.originalName"
|
:key="file.url || file.link || file.name || file.originalName"
|
||||||
type="primary"
|
class="attachment-file-name"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
@click="previewAttachment(file)"
|
@click="previewAttachment(file)"
|
||||||
>{{ attachmentName(file) }}</el-link
|
@keydown.enter="previewAttachment(file)"
|
||||||
>
|
@keydown.space.prevent="previewAttachment(file)"
|
||||||
|
>{{ attachmentName(file) }}</span>
|
||||||
</template>
|
</template>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</dd>
|
</dd>
|
||||||
@@ -69,14 +82,21 @@
|
|||||||
<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="60" align="center" />
|
||||||
<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" min-width="130">
|
||||||
|
<template #header
|
||||||
|
><span>货物类型<span class="required-asterisk">*</span></span></template
|
||||||
|
>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="packageType" label="包装" min-width="120" />
|
<el-table-column prop="packageType" label="包装" min-width="120" />
|
||||||
<el-table-column label="数量" min-width="100"><template #default="{ row }">{{ row.quantity ?? '-' }}{{ row.quantityUnit ? ` ${row.quantityUnit}` : '' }}</template></el-table-column>
|
<el-table-column prop="quantity" label="数量" min-width="100" />
|
||||||
|
<el-table-column prop="quantityUnit" label="数量单位" width="100" />
|
||||||
<el-table-column prop="brand" label="品牌" min-width="120" />
|
<el-table-column prop="brand" label="品牌" min-width="120" />
|
||||||
<el-table-column prop="specification" label="规格" min-width="150" />
|
<el-table-column prop="specification" label="规格" min-width="120" />
|
||||||
<el-table-column prop="model" label="型号" min-width="150" />
|
<el-table-column prop="model" label="型号" min-width="120" />
|
||||||
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
|
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" width="100" align="center" fixed="right" />
|
||||||
</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>
|
||||||
@@ -132,6 +152,7 @@ export default {
|
|||||||
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)}` : '-'; },
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
if (!this.id) return;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await api.getDetail(this.id);
|
const res = await api.getDetail(this.id);
|
||||||
@@ -282,9 +303,37 @@ export default {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.master-detail { padding-bottom: 20px; color: #303133; }
|
.master-detail { padding-bottom: 20px; color: #303133; }
|
||||||
.detail-overview, .execution-detail-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
.detail-overview, .execution-detail-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
||||||
.detail-heading { display: flex; align-items: center; justify-content: space-between; padding: 18px 24px; h2 { display: inline-block; margin: 0 16px 0 0; font-size: 20px; } h2 span { margin: 0 8px; color: #909399; font-weight: 400; } }
|
.detail-heading {
|
||||||
.transport-flow-tag { margin-left: 8px; }
|
display: flex;
|
||||||
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #303133; font-size: 14px; word-break: break-all; } :deep(.el-link) { font-size: 14px; } }
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 18px 24px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 16px 0 0;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 span {
|
||||||
|
margin: 0 8px;
|
||||||
|
color: #909399;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tag) {
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.transport-flow-tag {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #303133; font-size: 14px; word-break: break-all; } .attachment-file-name { font-size: 14px; max-width: 100%; } }
|
||||||
.route-map { display: flex; align-items: flex-start; padding: 16px 24px 20px; border-top: 1px solid #eff1f7; overflow-x: auto; }
|
.route-map { 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: #303133 !important; }
|
.route-map__progress { color: #303133 !important; }
|
||||||
@@ -305,5 +354,7 @@ export default {
|
|||||||
.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; }
|
||||||
.master-goods-card__heading { padding: 20px 24px 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
|
.master-goods-card__heading { padding: 20px 24px 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
|
||||||
|
.master-goods-card :deep(.el-table) { margin: 0 24px 20px; width: auto; }
|
||||||
|
.required-asterisk { margin-left: 4px; color: #f56c6c; }
|
||||||
@media screen and (max-width: 1200px) { .detail-meta { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
@media screen and (max-width: 1200px) { .detail-meta { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -51,9 +51,29 @@
|
|||||||
<section v-for="(route, index) in routes" :key="route.segmentNo" class="dispatch-section" :data-segment="route.segmentNo">
|
<section v-for="(route, index) in routes" :key="route.segmentNo" class="dispatch-section" :data-segment="route.segmentNo">
|
||||||
<header class="segment-header">
|
<header class="segment-header">
|
||||||
<div>
|
<div>
|
||||||
<el-checkbox v-model="route.selected">
|
<el-tooltip
|
||||||
|
v-if="!canDispatchSegment(route)"
|
||||||
|
content="需等上一段运单完成到达后才能调度"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<span class="segment-checkbox-wrap">
|
||||||
|
<el-checkbox :model-value="false" disabled>
|
||||||
<span class="segment-checkbox-label">
|
<span class="segment-checkbox-label">
|
||||||
<span>{{ route.segmentNo }}:</span>
|
<span class="segment-checkbox-no">{{ route.segmentNo }}:</span>
|
||||||
|
<span class="segment-checkbox-path"
|
||||||
|
>{{ dispatchLocationName(route, 'departure') }} → {{ dispatchLocationName(route, 'arrival') }}</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</el-checkbox>
|
||||||
|
</span>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-checkbox
|
||||||
|
v-else
|
||||||
|
v-model="route.selected"
|
||||||
|
@change="value => handleSegmentSelect(route, value)"
|
||||||
|
>
|
||||||
|
<span class="segment-checkbox-label">
|
||||||
|
<span class="segment-checkbox-no">{{ route.segmentNo }}:</span>
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
:content="`${dispatchLocationName(route, 'departure')} → ${dispatchLocationName(route, 'arrival')}`"
|
:content="`${dispatchLocationName(route, 'departure')} → ${dispatchLocationName(route, 'arrival')}`"
|
||||||
placement="top"
|
placement="top"
|
||||||
@@ -65,7 +85,15 @@
|
|||||||
</span>
|
</span>
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div>总量 {{ formatQuantity(totalQuantity) }} {{ quantityUnitLabel }},已调度 <em>{{ formatQuantity(dispatchedQuantity(route)) }} {{ quantityUnitLabel }}</em>,剩余 <em>{{ formatQuantity(remaining(route)) }} {{ quantityUnitLabel }}</em></div>
|
<div>
|
||||||
|
总量 {{ formatQuantity(totalQuantity) }} {{ quantityUnitLabel }},已调度
|
||||||
|
<em>{{ formatQuantity(dispatchedQuantity(route)) }} {{ quantityUnitLabel }}</em>,剩余
|
||||||
|
<em>{{ formatQuantity(remaining(route)) }} {{ quantityUnitLabel }}</em>
|
||||||
|
<template v-if="index > 0">
|
||||||
|
,上一段已到达
|
||||||
|
<em>{{ formatQuantity(previousArrivedQuantity(route)) }} {{ quantityUnitLabel }}</em>
|
||||||
|
</template>
|
||||||
|
</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">
|
||||||
@@ -78,16 +106,28 @@
|
|||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="发货地址" required><el-input :model-value="addressText(route.departureName, route.departureAddress)" disabled /></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-col :span="4"><el-form-item label="联系方式"><el-input v-model="route.departurePhone" placeholder="请输入" /></el-form-item></el-col>
|
><el-form-item label="联系人" label-width="68px" class="dispatch-form__compact-label"
|
||||||
|
><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item
|
||||||
|
></el-col>
|
||||||
|
<el-col :span="4"
|
||||||
|
><el-form-item label="联系方式" label-width="80px" class="dispatch-form__compact-label"
|
||||||
|
><el-input v-model="route.departurePhone" placeholder="请输入" /></el-form-item
|
||||||
|
></el-col>
|
||||||
<el-col :span="4"><el-form-item :label="dateStartLabel(route)"><el-date-picker v-model="route.estimatedStartTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择" /></el-form-item></el-col>
|
<el-col :span="4"><el-form-item :label="dateStartLabel(route)"><el-date-picker v-model="route.estimatedStartTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择" /></el-form-item></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.arrivalName, route.arrivalAddress)" disabled /></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-col :span="4"><el-form-item label="联系方式"><el-input v-model="route.arrivalPhone" placeholder="请输入" /></el-form-item></el-col>
|
><el-form-item label="联系人" label-width="68px" class="dispatch-form__compact-label"
|
||||||
|
><el-input v-model="route.arrivalContact" placeholder="请输入" /></el-form-item
|
||||||
|
></el-col>
|
||||||
|
<el-col :span="4"
|
||||||
|
><el-form-item label="联系方式" label-width="80px" class="dispatch-form__compact-label"
|
||||||
|
><el-input v-model="route.arrivalPhone" placeholder="请输入" /></el-form-item
|
||||||
|
></el-col>
|
||||||
<el-col :span="4"><el-form-item :label="dateEndLabel(route)"><el-date-picker v-model="route.estimatedEndTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择" /></el-form-item></el-col>
|
<el-col :span="4"><el-form-item :label="dateEndLabel(route)"><el-date-picker v-model="route.estimatedEndTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择" /></el-form-item></el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -156,16 +196,59 @@
|
|||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<template v-if="isRoad(route)">
|
<template v-if="isRoad(route)">
|
||||||
<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-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-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-form-item label="司机" :required="route.carrierType !== '承运商'"
|
||||||
<el-col :span="6"><el-form-item label="车牌号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
|
><el-autocomplete
|
||||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="挂车车牌号"><el-input v-model="route.trailerVehicleNo" placeholder="请输入" /></el-form-item></el-col>
|
:ref="element => setDriverInput(route.segmentNo, element)"
|
||||||
<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>
|
v-model="route.driverName"
|
||||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人手机号"><el-input v-model="route.escortPhone" placeholder="请输入" /></el-form-item></el-col>
|
clearable
|
||||||
|
trigger-on-focus
|
||||||
|
:debounce="300"
|
||||||
|
:fetch-suggestions="fetchDriverSuggestions"
|
||||||
|
:loading="driverLoading"
|
||||||
|
placeholder="请输入司机"
|
||||||
|
@mousedown="() => handleDriverFocus(route)"
|
||||||
|
@focus="() => handleDriverFocus(route)"
|
||||||
|
@select="handleDriverSuggestionSelect"
|
||||||
|
/></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 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="押运人"
|
||||||
|
><el-autocomplete
|
||||||
|
v-model="route.escortName"
|
||||||
|
clearable
|
||||||
|
trigger-on-focus
|
||||||
|
:debounce="300"
|
||||||
|
:fetch-suggestions="fetchEscortSuggestions"
|
||||||
|
:loading="escortLoading"
|
||||||
|
placeholder="请输入"
|
||||||
|
@mousedown="() => handleDriverFocus(route)"
|
||||||
|
@focus="() => handleDriverFocus(route)"
|
||||||
|
@select="handleEscortSuggestionSelect"
|
||||||
|
/></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 :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-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="船长"><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="联系电话"><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="箱号"><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>
|
||||||
@@ -183,7 +266,7 @@
|
|||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="单价">
|
<el-form-item label="单价">
|
||||||
<el-input :model-value="freightGroupUnitPrice(group)" inputmode="decimal" placeholder="请输入" @input="value => handleFreightGroupUnitPriceInput(group, value)" @clear="() => handleFreightGroupUnitPriceInput(group, '')">
|
<el-input :model-value="freightGroupUnitPrice(group)" inputmode="decimal" placeholder="请输入" @input="value => handleFreightGroupUnitPriceInput(group, value)" @clear="() => handleFreightGroupUnitPriceInput(group, '')">
|
||||||
<template #append><el-select :model-value="freightGroupPriceUnit(group)" class="freight-unit-select" @change="value => handleFreightGroupPriceUnitChange(group, value)"><el-option label="元/吨" value="元/吨" /><el-option label="元/件" value="元/件" /><el-option label="元/方" value="元/方" /></el-select></template>
|
<template #append><el-select :model-value="freightGroupPriceUnit(group)" class="freight-unit-select" @change="value => handleFreightGroupPriceUnitChange(group, value)"><el-option v-for="item in priceUnitOptions(route)" :key="item.value" :label="item.label" :value="item.value" /></el-select></template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -233,7 +316,14 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as api from '@/api/business/master-order';
|
import * as api from '@/api/business/master-order';
|
||||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
|
||||||
|
import { formatContractOptionLabel } from '@/utils/contract';
|
||||||
|
import {
|
||||||
|
currencyUnit,
|
||||||
|
remapPriceUnitCurrency,
|
||||||
|
resolvePriceUnitByQuantityUnit,
|
||||||
|
} from '@/utils/currency';
|
||||||
|
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||||
import { isMobile } from '@/utils/validate';
|
import { isMobile } from '@/utils/validate';
|
||||||
|
|
||||||
const unwrapRecords = res => {
|
const unwrapRecords = res => {
|
||||||
@@ -249,7 +339,24 @@ 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, escortLoading: false, quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'] };
|
return {
|
||||||
|
loading: false,
|
||||||
|
submitting: false,
|
||||||
|
master: null,
|
||||||
|
routes: [],
|
||||||
|
pending: [],
|
||||||
|
pendingExpanded: false,
|
||||||
|
editingId: null,
|
||||||
|
driverInputs: {},
|
||||||
|
activeSuggestRoute: null,
|
||||||
|
carrierOptions: [],
|
||||||
|
driverOptions: [],
|
||||||
|
carrierLoading: false,
|
||||||
|
driverLoading: false,
|
||||||
|
escortLoading: false,
|
||||||
|
quantityUnitOptions: [],
|
||||||
|
quantityUnitLoading: false,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
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); },
|
||||||
@@ -290,11 +397,26 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await Promise.all([this.load(), this.loadCarrierOptions(), this.loadDriverOptions()]);
|
await this.loadQuantityUnitOptions();
|
||||||
|
await Promise.all([this.load(), this.loadCarrierOptions()]);
|
||||||
this.applySelfOperatedCarrierDefaults();
|
this.applySelfOperatedCarrierDefaults();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
loadQuantityUnitOptions() {
|
||||||
|
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
|
||||||
|
this.quantityUnitLoading = true;
|
||||||
|
this.quantityUnitLoadPromise = loadMeasurementUnitOptions()
|
||||||
|
.then(options => {
|
||||||
|
this.quantityUnitOptions = options.map(item => item.value);
|
||||||
|
return this.quantityUnitOptions;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.quantityUnitLoading = false;
|
||||||
|
});
|
||||||
|
return this.quantityUnitLoadPromise;
|
||||||
|
},
|
||||||
async load() {
|
async load() {
|
||||||
|
if (!this.id) return;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await api.getDetail(this.id);
|
const res = await api.getDetail(this.id);
|
||||||
@@ -312,8 +434,10 @@ export default {
|
|||||||
},
|
},
|
||||||
async loadContractCurrency() {
|
async loadContractCurrency() {
|
||||||
const directCurrency = this.master?.settlementCurrency || this.master?.settlementCurrencyCode || this.master?.currency;
|
const directCurrency = this.master?.settlementCurrency || this.master?.settlementCurrencyCode || this.master?.currency;
|
||||||
|
const previousCurrency = this.master?.settlementCurrency;
|
||||||
if (!this.master?.projectId || !this.master?.contractId) {
|
if (!this.master?.projectId || !this.master?.contractId) {
|
||||||
this.master.settlementCurrency = this.normalizeCurrencyValue(directCurrency || 'RMB');
|
this.master.settlementCurrency = this.normalizeCurrencyValue(directCurrency || 'RMB');
|
||||||
|
this.syncRouteCurrency(previousCurrency, this.master.settlementCurrency);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -327,6 +451,20 @@ export default {
|
|||||||
this.master.customerContractPartyB = '';
|
this.master.customerContractPartyB = '';
|
||||||
this.master.settlementCurrency = 'RMB';
|
this.master.settlementCurrency = 'RMB';
|
||||||
}
|
}
|
||||||
|
this.syncRouteCurrency(previousCurrency, this.master.settlementCurrency);
|
||||||
|
},
|
||||||
|
syncRouteCurrency(_previousCurrency, nextCurrency) {
|
||||||
|
const currency = this.normalizeCurrencyValue(nextCurrency || 'RMB');
|
||||||
|
(this.routes || []).forEach(route => {
|
||||||
|
route.currency = currency;
|
||||||
|
(route.freightItems || []).forEach(item => {
|
||||||
|
if (item.priceUnit) {
|
||||||
|
item.priceUnit = remapPriceUnitCurrency(item.priceUnit, currency);
|
||||||
|
} else if (item.quantityUnit) {
|
||||||
|
item.priceUnit = resolvePriceUnitByQuantityUnit(item.quantityUnit, currency);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
dispatchRouteNodes() {
|
dispatchRouteNodes() {
|
||||||
const routes = Array.isArray(this.master?.routes) ? this.master.routes : [];
|
const routes = Array.isArray(this.master?.routes) ? this.master.routes : [];
|
||||||
@@ -452,12 +590,29 @@ export default {
|
|||||||
dispatchedQuantity(route) {
|
dispatchedQuantity(route) {
|
||||||
return this.quantityNumber(route.dispatchedQuantity) + this.pendingSegmentQuantity(route);
|
return this.quantityNumber(route.dispatchedQuantity) + this.pendingSegmentQuantity(route);
|
||||||
},
|
},
|
||||||
|
previousArrivedQuantity(route) {
|
||||||
|
const routeIndex = this.routes.indexOf(route);
|
||||||
|
if (routeIndex <= 0) return null;
|
||||||
|
return this.quantityNumber(this.routes[routeIndex - 1]?.arrivedQuantity);
|
||||||
|
},
|
||||||
|
canDispatchSegment(route) {
|
||||||
|
const previousArrived = this.previousArrivedQuantity(route);
|
||||||
|
if (previousArrived === null) return true;
|
||||||
|
return previousArrived > 0;
|
||||||
|
},
|
||||||
remaining(route) {
|
remaining(route) {
|
||||||
return Math.max(
|
const masterRemaining = Math.max(0, this.totalQuantity - this.dispatchedQuantity(route));
|
||||||
0,
|
const previousArrived = this.previousArrivedQuantity(route);
|
||||||
this.totalQuantity -
|
if (previousArrived === null) return masterRemaining;
|
||||||
this.dispatchedQuantity(route)
|
return Math.max(0, Math.min(masterRemaining, previousArrived - this.dispatchedQuantity(route)));
|
||||||
|
},
|
||||||
|
handleSegmentSelect(route, value) {
|
||||||
|
if (value && !this.canDispatchSegment(route)) {
|
||||||
|
route.selected = false;
|
||||||
|
this.$message.warning(
|
||||||
|
`${route.segmentNo}需等上一段运单完成到达后才能调度(上一段已到达 ${this.formatQuantity(this.previousArrivedQuantity(route))} ${this.quantityUnitLabel})`
|
||||||
);
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
formatQuantity(value) { const number = Number(value || 0); return Number.isInteger(number) ? number : number.toFixed(3).replace(/\.?0+$/, ''); },
|
formatQuantity(value) { const number = Number(value || 0); return Number.isInteger(number) ? number : number.toFixed(3).replace(/\.?0+$/, ''); },
|
||||||
formatAmount(value) { return Number(value || 0).toFixed(2); },
|
formatAmount(value) { return Number(value || 0).toFixed(2); },
|
||||||
@@ -484,8 +639,23 @@ export default {
|
|||||||
const prices = (group.rows || []).map(item => String(item.unitPrice ?? '').trim()).filter(Boolean);
|
const prices = (group.rows || []).map(item => String(item.unitPrice ?? '').trim()).filter(Boolean);
|
||||||
return !prices.length || prices.some(price => price !== prices[0]) ? '' : prices[0];
|
return !prices.length || prices.some(price => price !== prices[0]) ? '' : prices[0];
|
||||||
},
|
},
|
||||||
freightGroupPriceUnit(group = {}) { return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : ''); },
|
freightGroupPriceUnit(group = {}) {
|
||||||
|
if (group.rows?.[0]?.priceUnit) return group.rows[0].priceUnit;
|
||||||
|
if (!group.quantityUnit) return '';
|
||||||
|
return (
|
||||||
|
resolvePriceUnitByQuantityUnit(group.quantityUnit, this.master?.settlementCurrency || 'RMB') ||
|
||||||
|
`${currencyUnit(this.master?.settlementCurrency || 'RMB')}/${group.quantityUnit}`
|
||||||
|
);
|
||||||
|
},
|
||||||
freightGroupAmount(group = {}) { return (group.rows || []).reduce((sum, item) => sum + this.freightAmount(item), 0); },
|
freightGroupAmount(group = {}) { return (group.rows || []).reduce((sum, item) => sum + this.freightAmount(item), 0); },
|
||||||
|
priceUnitOptions(route = {}) {
|
||||||
|
const unit = currencyUnit(route.currency || this.master?.settlementCurrency || 'RMB');
|
||||||
|
return [
|
||||||
|
{ label: `${unit}/吨`, value: `${unit}/吨` },
|
||||||
|
{ label: `${unit}/件`, value: `${unit}/件` },
|
||||||
|
{ label: `${unit}/方`, value: `${unit}/方` },
|
||||||
|
];
|
||||||
|
},
|
||||||
handleFreightGroupUnitPriceInput(group, value) {
|
handleFreightGroupUnitPriceInput(group, value) {
|
||||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||||
const normalized = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
const normalized = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||||
@@ -495,6 +665,10 @@ export default {
|
|||||||
handleFreightGroupQuantityUnitChange(route, group, value) {
|
handleFreightGroupQuantityUnitChange(route, group, value) {
|
||||||
(group.rows || []).forEach(item => {
|
(group.rows || []).forEach(item => {
|
||||||
item.quantityUnit = value || '';
|
item.quantityUnit = value || '';
|
||||||
|
item.priceUnit = resolvePriceUnitByQuantityUnit(
|
||||||
|
value,
|
||||||
|
route.currency || this.master?.settlementCurrency || 'RMB'
|
||||||
|
) || item.priceUnit;
|
||||||
const goods = (route.goods || []).find(row => String(row.sourceIndex) === String(item.sourceIndex));
|
const goods = (route.goods || []).find(row => String(row.sourceIndex) === String(item.sourceIndex));
|
||||||
if (goods) goods.quantityUnit = value || '';
|
if (goods) goods.quantityUnit = value || '';
|
||||||
});
|
});
|
||||||
@@ -517,7 +691,7 @@ export default {
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
currencyLabel(value) { return ({ CNY: '人民币', RMB: '人民币', USD: '美元', EUR: '欧元' })[value] || value || '人民币'; },
|
currencyLabel(value) { return currencyUnit(value); },
|
||||||
quantityNumber(value) { return Number(value || 0); },
|
quantityNumber(value) { return Number(value || 0); },
|
||||||
goodsKey(goods = {}, includeUnit = true) {
|
goodsKey(goods = {}, includeUnit = true) {
|
||||||
return [
|
return [
|
||||||
@@ -562,13 +736,14 @@ export default {
|
|||||||
}, 0);
|
}, 0);
|
||||||
},
|
},
|
||||||
validatePreviousSegmentCompletedQuantity(route, goods) {
|
validatePreviousSegmentCompletedQuantity(route, goods) {
|
||||||
const routeIndex = this.routes.indexOf(route);
|
const previousCompletedQuantity = this.previousArrivedQuantity(route);
|
||||||
if (routeIndex <= 0) return true;
|
if (previousCompletedQuantity === null) return true;
|
||||||
const previousCompletedQuantity = this.quantityNumber(
|
if (previousCompletedQuantity <= 0) {
|
||||||
this.routes[routeIndex - 1]?.arrivedQuantity
|
this.$message.warning(`${route.segmentNo}需等上一段运单完成到达后才能调度`);
|
||||||
);
|
return false;
|
||||||
|
}
|
||||||
const currentDispatchQuantity = (goods || []).reduce(
|
const currentDispatchQuantity = (goods || []).reduce(
|
||||||
(sum, item) => sum + this.quantityNumber(item.dispatchQuantity),
|
(sum, item) => sum + this.quantityNumber(item.dispatchQuantity ?? item.quantity),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
const dispatchTotal =
|
const dispatchTotal =
|
||||||
@@ -577,7 +752,7 @@ export default {
|
|||||||
currentDispatchQuantity;
|
currentDispatchQuantity;
|
||||||
if (dispatchTotal <= previousCompletedQuantity) return true;
|
if (dispatchTotal <= previousCompletedQuantity) return true;
|
||||||
this.$message.warning(
|
this.$message.warning(
|
||||||
`${route.segmentNo}调度总量不能大于上一段已完成数量(${this.formatQuantity(previousCompletedQuantity)} ${this.quantityUnitLabel})`
|
`${route.segmentNo}调度总量不能大于上一段已到达数量(${this.formatQuantity(previousCompletedQuantity)} ${this.quantityUnitLabel})`
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
@@ -706,6 +881,7 @@ export default {
|
|||||||
removeGoods(route, index) { route.goods.splice(index, 1); this.syncFreightItems(route); },
|
removeGoods(route, index) { route.goods.splice(index, 1); this.syncFreightItems(route); },
|
||||||
syncFreightItems(route) {
|
syncFreightItems(route) {
|
||||||
const oldItems = route.freightItems || [];
|
const oldItems = route.freightItems || [];
|
||||||
|
const currency = route.currency || this.master?.settlementCurrency || 'RMB';
|
||||||
route.freightItems = (route.goods || [])
|
route.freightItems = (route.goods || [])
|
||||||
.filter(item => item.sourceIndex !== undefined && item.sourceIndex !== '')
|
.filter(item => item.sourceIndex !== undefined && item.sourceIndex !== '')
|
||||||
.map(item => {
|
.map(item => {
|
||||||
@@ -717,7 +893,10 @@ export default {
|
|||||||
quantity: item.dispatchQuantity || '',
|
quantity: item.dispatchQuantity || '',
|
||||||
quantityUnit: item.quantityUnit || '',
|
quantityUnit: item.quantityUnit || '',
|
||||||
unitPrice: old.unitPrice || '',
|
unitPrice: old.unitPrice || '',
|
||||||
priceUnit: old.priceUnit || `元/${item.quantityUnit || '吨'}`,
|
priceUnit:
|
||||||
|
old.priceUnit ||
|
||||||
|
resolvePriceUnitByQuantityUnit(item.quantityUnit || '吨', currency) ||
|
||||||
|
`${currencyUnit(currency)}/${item.quantityUnit || '吨'}`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -726,6 +905,7 @@ export default {
|
|||||||
route.carrierName = '';
|
route.carrierName = '';
|
||||||
route.carrierId = '';
|
route.carrierId = '';
|
||||||
route.carrierContractId = '';
|
route.carrierContractId = '';
|
||||||
|
this.clearRouteDriverVehicleFields(route);
|
||||||
if (value === '承运商') {
|
if (value === '承运商') {
|
||||||
route.trailerVehicleNo = '';
|
route.trailerVehicleNo = '';
|
||||||
route.escortName = '';
|
route.escortName = '';
|
||||||
@@ -745,15 +925,35 @@ export default {
|
|||||||
route.carrierContractId = route.carrierType !== '自运' ? contract?.contractId || '' : '';
|
route.carrierContractId = route.carrierType !== '自运' ? contract?.contractId || '' : '';
|
||||||
route.carrierId = route.carrierType !== '自运' ? contract?.carrierId || '' : '';
|
route.carrierId = route.carrierType !== '自运' ? contract?.carrierId || '' : '';
|
||||||
route.carrierName = contract?.carrierName || '';
|
route.carrierName = contract?.carrierName || '';
|
||||||
|
this.clearRouteDriverVehicleFields(route);
|
||||||
|
},
|
||||||
|
clearRouteDriverVehicleFields(route) {
|
||||||
|
if (!route) return;
|
||||||
|
route.driverId = '';
|
||||||
|
route.driverName = '';
|
||||||
|
route.driverPhone = '';
|
||||||
|
route.vehicleNo = '';
|
||||||
|
route.trailerVehicleNo = '';
|
||||||
|
route.escortName = '';
|
||||||
|
route.escortPhone = '';
|
||||||
|
this.driverOptions = [];
|
||||||
|
},
|
||||||
|
getRouteCarrierFilter(route = {}) {
|
||||||
|
return {
|
||||||
|
carrierId: route.carrierId || '',
|
||||||
|
carrierName: route.carrierName || '',
|
||||||
|
};
|
||||||
},
|
},
|
||||||
carrierOptionValue(item = {}) {
|
carrierOptionValue(item = {}) {
|
||||||
return item.contractId || item.id || item.value || item.carrierName || '';
|
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
|
if (duplicateCount <= 1) {
|
||||||
? `${item.carrierName}(${item.contractName || item.contractId})`
|
return item.carrierName;
|
||||||
: item.carrierName;
|
}
|
||||||
|
const contractLabel = formatContractOptionLabel(item);
|
||||||
|
return contractLabel ? `${item.carrierName}(${contractLabel})` : item.carrierName;
|
||||||
},
|
},
|
||||||
driverOptionLabel(item) { return item.driverName || item.name || ''; },
|
driverOptionLabel(item) { return item.driverName || item.name || ''; },
|
||||||
driverOptionKey(item) { return item.id || item.driverId || this.driverOptionLabel(item) || item.mobile; },
|
driverOptionKey(item) { return item.id || item.driverId || this.driverOptionLabel(item) || item.mobile; },
|
||||||
@@ -816,29 +1016,57 @@ export default {
|
|||||||
route.carrierContractId = '';
|
route.carrierContractId = '';
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async loadDriverOptions(keyword = '') {
|
handleDriverFocus(route) {
|
||||||
|
this.activeSuggestRoute = route;
|
||||||
|
},
|
||||||
|
async loadDriverOptions(route = {}, keyword = '') {
|
||||||
|
const carrier = this.getRouteCarrierFilter(route);
|
||||||
|
if (!carrier.carrierId && !carrier.carrierName) {
|
||||||
|
this.driverOptions = [];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
this.driverLoading = true;
|
this.driverLoading = true;
|
||||||
try {
|
try {
|
||||||
this.driverOptions = unwrapRecords(
|
this.driverOptions = await fetchDriversByCarrierOrganizations(
|
||||||
await getDriverList(1, 20, { driverName: keyword, posts: '司机' })
|
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||||
|
carrier
|
||||||
);
|
);
|
||||||
|
return this.driverOptions;
|
||||||
} finally {
|
} finally {
|
||||||
this.driverLoading = false;
|
this.driverLoading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetchDriverSuggestions(queryString, callback) {
|
fetchDriverSuggestions(queryString, callback) {
|
||||||
this.loadDriverOptions(String(queryString || '').trim())
|
const route = this.activeSuggestRoute || {};
|
||||||
.then(() =>
|
const carrier = this.getRouteCarrierFilter(route);
|
||||||
|
const keyword = String(queryString || '').trim();
|
||||||
|
if (!carrier.carrierId && !carrier.carrierName) {
|
||||||
|
callback([]);
|
||||||
|
if (!keyword) this.$message.warning('请先选择承运商');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.driverLoading = true;
|
||||||
|
fetchDriversByCarrierOrganizations(
|
||||||
|
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||||
|
carrier
|
||||||
|
)
|
||||||
|
.then(records => {
|
||||||
|
this.driverOptions = records;
|
||||||
callback(
|
callback(
|
||||||
this.driverOptions.map(item => ({
|
records.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
value: this.driverOptionLabel(item),
|
value: this.driverOptionLabel(item),
|
||||||
}))
|
}))
|
||||||
)
|
);
|
||||||
)
|
})
|
||||||
.catch(() => callback([]));
|
.catch(() => callback([]))
|
||||||
|
.finally(() => {
|
||||||
|
this.driverLoading = false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
handleDriverSuggestionSelect(route, item = {}) {
|
handleDriverSuggestionSelect(item = {}) {
|
||||||
|
const route = this.activeSuggestRoute;
|
||||||
|
if (!route) return;
|
||||||
route.driverName = this.driverOptionLabel(item);
|
route.driverName = this.driverOptionLabel(item);
|
||||||
route.driverId = item.id || item.driverId || '';
|
route.driverId = item.id || item.driverId || '';
|
||||||
route.driverPhone = item.mobile || item.driverPhone || item.phone || route.driverPhone || '';
|
route.driverPhone = item.mobile || item.driverPhone || item.phone || route.driverPhone || '';
|
||||||
@@ -846,12 +1074,22 @@ export default {
|
|||||||
if (drivingVehicle) route.vehicleNo = drivingVehicle;
|
if (drivingVehicle) route.vehicleNo = drivingVehicle;
|
||||||
},
|
},
|
||||||
fetchEscortSuggestions(queryString, callback) {
|
fetchEscortSuggestions(queryString, callback) {
|
||||||
|
const route = this.activeSuggestRoute || {};
|
||||||
const keyword = String(queryString || '').trim();
|
const keyword = String(queryString || '').trim();
|
||||||
|
const carrier = this.getRouteCarrierFilter(route);
|
||||||
|
if (!carrier.carrierId && !carrier.carrierName) {
|
||||||
|
callback([]);
|
||||||
|
if (!keyword) this.$message.warning('请先选择承运商');
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.escortLoading = true;
|
this.escortLoading = true;
|
||||||
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '押运员' })
|
fetchDriversByCarrierOrganizations(
|
||||||
.then(res => {
|
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
|
||||||
|
carrier
|
||||||
|
)
|
||||||
|
.then(records => {
|
||||||
callback(
|
callback(
|
||||||
unwrapRecords(res).map(item => ({
|
records.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
value: this.driverOptionLabel(item),
|
value: this.driverOptionLabel(item),
|
||||||
}))
|
}))
|
||||||
@@ -862,7 +1100,9 @@ export default {
|
|||||||
this.escortLoading = false;
|
this.escortLoading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleEscortSuggestionSelect(route, item = {}) {
|
handleEscortSuggestionSelect(item = {}) {
|
||||||
|
const route = this.activeSuggestRoute;
|
||||||
|
if (!route) return;
|
||||||
route.escortName = this.driverOptionLabel(item);
|
route.escortName = this.driverOptionLabel(item);
|
||||||
const phone = item.mobile || item.driverPhone || item.phone || '';
|
const phone = item.mobile || item.driverPhone || item.phone || '';
|
||||||
if (phone) route.escortPhone = phone;
|
if (phone) route.escortPhone = phone;
|
||||||
@@ -887,6 +1127,10 @@ 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.canDispatchSegment(route)) {
|
||||||
|
return this.$message.warning(`${route.segmentNo}需等上一段运单完成到达后才能调度`);
|
||||||
|
}
|
||||||
|
if (!this.validatePreviousSegmentCompletedQuantity(route, goods)) return;
|
||||||
if (!this.validateDispatchGoods(route, goods)) return;
|
if (!this.validateDispatchGoods(route, goods)) return;
|
||||||
this.syncFreightItems(route);
|
this.syncFreightItems(route);
|
||||||
if (!this.validateFreightItems(route)) return;
|
if (!this.validateFreightItems(route)) return;
|
||||||
@@ -1001,9 +1245,11 @@ export default {
|
|||||||
.pending-list-spacer { height: min(436px, calc(100vh - 130px)); }
|
.pending-list-spacer { height: min(436px, calc(100vh - 130px)); }
|
||||||
.master-overview, .dispatch-section { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
.master-overview, .dispatch-section { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
||||||
.overview-heading, .segment-header, .pending-bar { display: flex; align-items: center; justify-content: space-between; }
|
.overview-heading, .segment-header, .pending-bar { display: flex; align-items: center; justify-content: space-between; }
|
||||||
.segment-header :deep(.el-checkbox.is-checked .el-checkbox__label) { font-size: 16px; font-weight: 700; }
|
.segment-checkbox-wrap { display: inline-flex; max-width: 100%; }
|
||||||
.segment-checkbox-label { display: inline-flex; align-items: center; min-width: 0; }
|
.segment-checkbox-label { display: inline-flex; align-items: center; min-width: 0; font-size: 14px; font-weight: 400; color: #303133; }
|
||||||
.segment-checkbox-path { display: inline-block; white-space: nowrap; }
|
/* 仅「段X:」加粗加大,冒号右侧路线保持常规字号 */
|
||||||
|
.segment-checkbox-no { font-size: 16px; font-weight: 700; color: #303133; }
|
||||||
|
.segment-checkbox-path { display: inline-block; white-space: nowrap; font-size: 14px; font-weight: 400; }
|
||||||
.overview-heading { padding: 18px 24px 12px; h2 { display: inline-block; margin: 0 16px 0 0; font-size: 20px; } h2 span { margin: 0 8px; color: #909399; font-weight: 400; } }
|
.overview-heading { padding: 18px 24px 12px; h2 { display: inline-block; margin: 0 16px 0 0; font-size: 20px; } h2 span { margin: 0 8px; color: #909399; font-weight: 400; } }
|
||||||
.overview-content { display: grid; grid-template-columns: minmax(400px, .9fr) minmax(620px, 1.6fr); gap: 28px; padding: 8px 24px 20px; }
|
.overview-content { display: grid; grid-template-columns: minmax(400px, .9fr) minmax(620px, 1.6fr); gap: 28px; padding: 8px 24px 20px; }
|
||||||
.overview-meta { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 24px; margin: 0; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { margin: 0; color: #409eff; font-size: 14px; word-break: break-all; } }
|
.overview-meta { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 24px; margin: 0; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { margin: 0; color: #409eff; font-size: 14px; word-break: break-all; } }
|
||||||
@@ -1014,7 +1260,13 @@ export default {
|
|||||||
.route-overview__line { flex: 0 1 56px; min-width: 24px; height: 32px; margin-top: 0; border-bottom: 6px solid #e4e7ed; }
|
.route-overview__line { flex: 0 1 56px; min-width: 24px; height: 32px; margin-top: 0; border-bottom: 6px solid #e4e7ed; }
|
||||||
.segment-header { min-height: 46px; padding: 0 20px; border-bottom: 1px solid #eff1f7; font-weight: 600; em { color: #409eff; font-style: normal; } }
|
.segment-header { min-height: 46px; padding: 0 20px; border-bottom: 1px solid #eff1f7; font-weight: 600; em { color: #409eff; font-style: normal; } }
|
||||||
.segment-content { padding: 16px 24px; }
|
.segment-content { padding: 16px 24px; }
|
||||||
.dispatch-form { :deep(.el-form-item) { margin-bottom: 14px; } :deep(.el-input), :deep(.el-select), :deep(.el-date-editor), :deep(.el-input-number) { width: 100%; } :deep(.el-input-group__append .el-select) { width: auto; } }
|
.dispatch-form { :deep(.el-form-item) { margin-bottom: 24px; } :deep(.el-input), :deep(.el-select), :deep(.el-date-editor), :deep(.el-input-number) { width: 100%; } :deep(.el-input-group__append .el-select) { width: auto; } }
|
||||||
|
/* 短标签按自身宽度,避免被「预计发货日期」等长标签把左侧撑开 */
|
||||||
|
.dispatch-form__compact-label {
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
.transport-type-field :deep(.el-input) { width: 240px; }
|
.transport-type-field :deep(.el-input) { width: 240px; }
|
||||||
.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; } }
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
><el-select
|
><el-select
|
||||||
v-model="form.projectId"
|
v-model="form.projectId"
|
||||||
filterable
|
filterable
|
||||||
|
:disabled="isBasicRouteReadonly"
|
||||||
placeholder="请输入并选择"
|
placeholder="请输入并选择"
|
||||||
@change="projectChange"
|
@change="projectChange"
|
||||||
><el-option
|
><el-option
|
||||||
@@ -20,13 +21,13 @@
|
|||||||
><el-form-item label="客户合同" prop="contractName"
|
><el-form-item label="客户合同" prop="contractName"
|
||||||
><el-select
|
><el-select
|
||||||
v-model="form.contractId"
|
v-model="form.contractId"
|
||||||
:disabled="!form.projectId"
|
:disabled="isBasicRouteReadonly || !form.projectId"
|
||||||
placeholder="选择项目后带出"
|
placeholder="选择项目后带出"
|
||||||
@change="contractChange"
|
@change="contractChange"
|
||||||
><el-option
|
><el-option
|
||||||
v-for="contract in contracts"
|
v-for="contract in contracts"
|
||||||
:key="contract.id"
|
:key="contract.id"
|
||||||
:label="contract.contractName"
|
:label="formatContractOptionLabel(contract)"
|
||||||
: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="总单号" required
|
><el-form-item label="总单号" required
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
><el-form-item label="备注"
|
><el-form-item label="备注"
|
||||||
><el-input
|
><el-input
|
||||||
v-model="form.remark"
|
v-model="form.remark"
|
||||||
|
:disabled="isBasicRouteReadonly"
|
||||||
maxlength="200"
|
maxlength="200"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
placeholder="请输入" /></el-form-item></el-col></el-row
|
placeholder="请输入" /></el-form-item></el-col></el-row
|
||||||
@@ -49,7 +51,7 @@
|
|||||||
<section>
|
<section>
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<h3>收发货信息</h3>
|
<h3>收发货信息</h3>
|
||||||
<div class="section-head__actions">
|
<div v-if="!isBasicRouteReadonly" class="section-head__actions">
|
||||||
<el-link type="primary" @click="addRoute">新增途经点</el-link>
|
<el-link type="primary" @click="addRoute">新增途经点</el-link>
|
||||||
<el-link type="primary" @click="openRouteDialog">选择线路</el-link>
|
<el-link type="primary" @click="openRouteDialog">选择线路</el-link>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,13 +64,13 @@
|
|||||||
<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-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-input v-if="isNonRoadAddress('departure')" v-model="form.departureName" class="route-address-name" readonly :disabled="isBasicRouteReadonly" placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths.departure" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('departure', value)" /><el-input v-model="form.departureAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly" placeholder="请输入详细地址" @click="handleAddressDetailClick('departure')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly" @click="openCommonAddress('departure')" /></el-tooltip></div
|
||||||
></el-form-item>
|
></el-form-item>
|
||||||
<el-form-item label="联系人" class="route-contact-form-item"
|
<el-form-item label="联系人" class="route-contact-form-item"
|
||||||
><el-input v-model="form.departureContact" placeholder="请输入"
|
><el-input v-model="form.departureContact" :disabled="isBasicRouteReadonly" placeholder="请输入"
|
||||||
/></el-form-item>
|
/></el-form-item>
|
||||||
<el-form-item label="联系方式" class="route-phone-form-item"
|
<el-form-item label="联系方式" class="route-phone-form-item"
|
||||||
><el-input v-model="form.departurePhone" placeholder="请输入"
|
><el-input v-model="form.departurePhone" :disabled="isBasicRouteReadonly" placeholder="请输入"
|
||||||
/></el-form-item>
|
/></el-form-item>
|
||||||
<el-form-item label="计划开始" class="route-date-form-item"
|
<el-form-item label="计划开始" class="route-date-form-item"
|
||||||
><el-date-picker
|
><el-date-picker
|
||||||
@@ -76,6 +78,7 @@
|
|||||||
type="date"
|
type="date"
|
||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
|
:disabled="isBasicRouteReadonly"
|
||||||
placeholder="请选择"
|
placeholder="请选择"
|
||||||
/></el-form-item>
|
/></el-form-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,7 +86,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<template v-for="(route, index) in form.routes" :key="index">
|
<template v-for="(route, index) in form.routes" :key="index">
|
||||||
<div class="route-flow-segment">
|
<div class="route-flow-segment">
|
||||||
<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>
|
<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" :disabled="isBasicRouteReadonly" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="route-flow-row">
|
<div class="route-flow-row">
|
||||||
<span class="route-flow-marker route-flow-marker--middle">经</span>
|
<span class="route-flow-marker route-flow-marker--middle">经</span>
|
||||||
@@ -91,16 +94,16 @@
|
|||||||
<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-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-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly || !route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||||
></el-form-item>
|
></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="联系人" class="route-contact-form-item"><el-input v-model="route.departureContact" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
|
||||||
<el-form-item label="联系方式" class="route-phone-form-item"><div class="route-contact-actions"><el-input v-model="route.departurePhone" 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" :disabled="isBasicRouteReadonly" placeholder="请输入" /><template v-if="!isBasicRouteReadonly"><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></template></div></el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="route-flow-segment">
|
<div class="route-flow-segment">
|
||||||
<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>
|
<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" :disabled="isBasicRouteReadonly" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="route-flow-row">
|
<div class="route-flow-row">
|
||||||
<span class="route-flow-marker route-flow-marker--end">终</span>
|
<span class="route-flow-marker route-flow-marker--end">终</span>
|
||||||
@@ -108,18 +111,19 @@
|
|||||||
<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-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-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly || !form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||||
></el-form-item>
|
></el-form-item>
|
||||||
<el-form-item label="联系人" class="route-contact-form-item"
|
<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" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
|
||||||
<el-form-item label="联系方式" class="route-phone-form-item"
|
<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" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
|
||||||
<el-form-item label="计划结束" class="route-date-form-item"
|
<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"
|
||||||
|
:disabled="isBasicRouteReadonly"
|
||||||
placeholder="请选择"
|
placeholder="请选择"
|
||||||
/></el-form-item>
|
/></el-form-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,6 +148,7 @@
|
|||||||
><el-autocomplete
|
><el-autocomplete
|
||||||
v-model="row.cargoName"
|
v-model="row.cargoName"
|
||||||
clearable
|
clearable
|
||||||
|
:disabled="isGoodsCategoryLocked(row)"
|
||||||
placeholder="请输入货物名称"
|
placeholder="请输入货物名称"
|
||||||
:debounce="300"
|
:debounce="300"
|
||||||
:fetch-suggestions="
|
:fetch-suggestions="
|
||||||
@@ -168,22 +173,29 @@
|
|||||||
:placeholder="row.cargoType || '请选择'"
|
:placeholder="row.cargoType || '请选择'"
|
||||||
clearable
|
clearable
|
||||||
filterable
|
filterable
|
||||||
|
:disabled="isGoodsCategoryLocked(row)"
|
||||||
:loading="cargoTypeLoading"
|
:loading="cargoTypeLoading"
|
||||||
@visible-change="visible => visible && loadCargoTypeOptions()"
|
@visible-change="visible => visible && loadCargoTypeOptions()"
|
||||||
@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="200" class-name="master-editor__quantity-cell"
|
||||||
><template #header><span>数量<span class="required-asterisk">*</span></span></template>
|
><template #header><span>数量<span class="required-asterisk">*</span></span></template>
|
||||||
<template #default="{ row }"
|
<template #default="{ row }"
|
||||||
|
><div class="master-editor__quantity-field"
|
||||||
><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)"
|
||||||
|
@blur="() => enforceMinDispatchedQuantity(row)"
|
||||||
|
/><small v-if="dispatchedQuantityOf(row) > 0" class="master-editor__quantity-hint"
|
||||||
|
>已调度 {{ formatDispatchedQuantity(dispatchedQuantityOf(row)) }},不可低于此值</small
|
||||||
|
></div
|
||||||
|
></template></el-table-column
|
||||||
><el-table-column label="数量单位" width="120"
|
><el-table-column label="数量单位" width="120"
|
||||||
><template #header><span>数量单位<span class="required-asterisk">*</span></span></template>
|
><template #header><span>数量单位<span class="required-asterisk">*</span></span></template>
|
||||||
<template #default="{ row }"
|
<template #default="{ row }"
|
||||||
><el-select v-model="row.quantityUnit"
|
><el-select v-model="row.quantityUnit" :disabled="isGoodsCategoryLocked(row)"
|
||||||
><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"
|
||||||
><template #default="{ row }"
|
><template #default="{ row }"
|
||||||
@@ -218,7 +230,7 @@
|
|||||||
>新增</el-link
|
>新增</el-link
|
||||||
><el-link
|
><el-link
|
||||||
type="danger"
|
type="danger"
|
||||||
:disabled="form.goods.length === 1"
|
:disabled="form.goods.length === 1 || isGoodsCategoryLocked(form.goods[$index])"
|
||||||
@click="removeGoods($index)"
|
@click="removeGoods($index)"
|
||||||
>删除</el-link
|
>删除</el-link
|
||||||
></div
|
></div
|
||||||
@@ -249,11 +261,22 @@
|
|||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" align="center" />
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||||
<el-table-column label="文件名" min-width="240" align="center" show-overflow-tooltip>
|
<el-table-column
|
||||||
|
label="文件名"
|
||||||
|
min-width="240"
|
||||||
|
align="center"
|
||||||
|
show-overflow-tooltip
|
||||||
|
class-name="is-ellipsis-link"
|
||||||
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-link type="primary" @click="previewAttachment(row)">
|
<span
|
||||||
{{ attachmentName(row) }}
|
class="attachment-file-name"
|
||||||
</el-link>
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="previewAttachment(row)"
|
||||||
|
@keydown.enter="previewAttachment(row)"
|
||||||
|
@keydown.space.prevent="previewAttachment(row)"
|
||||||
|
>{{ attachmentName(row) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="附件描述" min-width="220" align="center">
|
<el-table-column label="附件描述" min-width="220" align="center">
|
||||||
@@ -272,10 +295,9 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
||||||
<el-table-column prop="uploadTime" label="上传时间" width="170" align="center" sortable />
|
<el-table-column prop="uploadTime" label="上传时间" width="170" align="center" sortable />
|
||||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
<el-table-column label="操作" width="80" align="center" fixed="right">
|
||||||
<template #default="{ row, $index }">
|
<template #default="{ $index }">
|
||||||
<el-link type="danger" @click="removeAttachment($index)">删除</el-link>
|
<el-link type="danger" @click="removeAttachment($index)">删除</el-link>
|
||||||
<el-link type="primary" @click="downloadAttachment(row)">下载</el-link>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -402,8 +424,8 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="map-picker-content">
|
<div class="map-picker-content">
|
||||||
<map-search-results :results="mapSearchResults" @select="selectAddressMapSearchResult" />
|
|
||||||
<div ref="addressMap" class="address-map" />
|
<div ref="addressMap" class="address-map" />
|
||||||
|
<map-search-results :results="mapSearchResults" @select="selectAddressMapSearchResult" />
|
||||||
</div>
|
</div>
|
||||||
<div class="address-map-status">
|
<div class="address-map-status">
|
||||||
{{ mapStatus
|
{{ mapStatus
|
||||||
@@ -578,6 +600,7 @@ import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
|||||||
import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
|
import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
|
||||||
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
||||||
import { exportBlob, importBlob } from '@/api/common';
|
import { exportBlob, importBlob } from '@/api/common';
|
||||||
|
import { formatContractOptionLabel } from '@/utils/contract';
|
||||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||||
import { OfficeBuilding, Search } from '@element-plus/icons-vue';
|
import { OfficeBuilding, Search } from '@element-plus/icons-vue';
|
||||||
import { ElImageViewer } from 'element-plus';
|
import { ElImageViewer } from 'element-plus';
|
||||||
@@ -586,10 +609,10 @@ import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from
|
|||||||
import '@open-file-viewer/core/style.css';
|
import '@open-file-viewer/core/style.css';
|
||||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||||
|
|
||||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||||
const quantityUnitOptions = ['吨', '件', '方'];
|
|
||||||
const attachmentViewerPlugins = [
|
const attachmentViewerPlugins = [
|
||||||
imagePlugin(),
|
imagePlugin(),
|
||||||
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
|
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
|
||||||
@@ -601,6 +624,7 @@ let amapLoader;
|
|||||||
import { getList as getCommonRouteList } from '@/api/business/common-route';
|
import { getList as getCommonRouteList } from '@/api/business/common-route';
|
||||||
import * as api from '@/api/business/master-order';
|
import * as api from '@/api/business/master-order';
|
||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
|
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||||
import { isMobile } from '@/utils/validate';
|
import { isMobile } from '@/utils/validate';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -691,10 +715,11 @@ export default {
|
|||||||
'zip',
|
'zip',
|
||||||
],
|
],
|
||||||
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
|
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
|
||||||
quantityUnitOptions,
|
quantityUnitOptions: [],
|
||||||
|
quantityUnitLoading: false,
|
||||||
form: {
|
form: {
|
||||||
routes: [{ segmentNo: '段1', transportType: '' }],
|
routes: [{ segmentNo: '段1', transportType: '' }],
|
||||||
goods: [{ quantityUnit: quantityUnitOptions[0] }],
|
goods: [{ quantityUnit: '吨' }],
|
||||||
attachmentsJson: '[]',
|
attachmentsJson: '[]',
|
||||||
transportOrganizationType: '多式联运',
|
transportOrganizationType: '多式联运',
|
||||||
},
|
},
|
||||||
@@ -706,6 +731,31 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['userInfo']),
|
...mapGetters(['userInfo']),
|
||||||
|
// 非草稿编辑:基本信息与收发货路线只读
|
||||||
|
isBasicRouteReadonly() {
|
||||||
|
return Boolean(this.id) && Boolean(this.form.businessStatus) && this.form.businessStatus !== 'draft';
|
||||||
|
},
|
||||||
|
// 品类 → 已调度量(各分段取最大值;优先用详情返回的 dispatchedGoods)
|
||||||
|
dispatchedGoodsMap() {
|
||||||
|
const map = {};
|
||||||
|
const source = this.form.dispatchedGoods;
|
||||||
|
if (source && typeof source === 'object' && !Array.isArray(source)) {
|
||||||
|
Object.entries(source).forEach(([key, qty]) => {
|
||||||
|
const number = Number(qty);
|
||||||
|
if (Number.isFinite(number)) map[key] = number;
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
(this.form.routeProgress || []).forEach(route => {
|
||||||
|
const goods = route?.dispatchedGoods || {};
|
||||||
|
Object.entries(goods).forEach(([key, qty]) => {
|
||||||
|
const number = Number(qty);
|
||||||
|
if (!Number.isFinite(number)) return;
|
||||||
|
map[key] = Math.max(map[key] || 0, number);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
},
|
||||||
lastRouteName() {
|
lastRouteName() {
|
||||||
return this.form.routes[this.form.routes.length - 1]?.departureName || '途经地';
|
return this.form.routes[this.form.routes.length - 1]?.departureName || '途经地';
|
||||||
},
|
},
|
||||||
@@ -736,6 +786,7 @@ export default {
|
|||||||
async mounted() {
|
async mounted() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
|
await this.loadQuantityUnitOptions();
|
||||||
await Promise.all([this.loadProjects(), this.loadCargoTypeOptions(), this.loadRegionOptions()]);
|
await Promise.all([this.loadProjects(), this.loadCargoTypeOptions(), this.loadRegionOptions()]);
|
||||||
if (this.id) {
|
if (this.id) {
|
||||||
await this.loadDetail(this.id);
|
await this.loadDetail(this.id);
|
||||||
@@ -745,6 +796,23 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formatContractOptionLabel,
|
||||||
|
defaultQuantityUnit() {
|
||||||
|
return this.quantityUnitOptions[0] || '吨';
|
||||||
|
},
|
||||||
|
loadQuantityUnitOptions() {
|
||||||
|
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
|
||||||
|
this.quantityUnitLoading = true;
|
||||||
|
this.quantityUnitLoadPromise = loadMeasurementUnitOptions()
|
||||||
|
.then(options => {
|
||||||
|
this.quantityUnitOptions = options.map(item => item.value);
|
||||||
|
return this.quantityUnitOptions;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.quantityUnitLoading = false;
|
||||||
|
});
|
||||||
|
return this.quantityUnitLoadPromise;
|
||||||
|
},
|
||||||
responseData(response) {
|
responseData(response) {
|
||||||
return response?.data?.data || response?.data || response || {};
|
return response?.data?.data || response?.data || response || {};
|
||||||
},
|
},
|
||||||
@@ -921,7 +989,7 @@ export default {
|
|||||||
finalTransportType: hasFinalSegment
|
finalTransportType: hasFinalSegment
|
||||||
? finalSegment.transportType
|
? finalSegment.transportType
|
||||||
: detail.finalTransportType || '',
|
: detail.finalTransportType || '',
|
||||||
goods: detail.goods || [{ quantityUnit: quantityUnitOptions[0] }],
|
goods: detail.goods || [{ quantityUnit: this.defaultQuantityUnit() }],
|
||||||
attachmentsJson: detail.attachmentsJson || '[]',
|
attachmentsJson: detail.attachmentsJson || '[]',
|
||||||
transportOrganizationType: '多式联运',
|
transportOrganizationType: '多式联运',
|
||||||
};
|
};
|
||||||
@@ -1104,6 +1172,7 @@ export default {
|
|||||||
return row.regionCode || row.districtCode || row.areaCode || row.adcode || '';
|
return row.regionCode || row.districtCode || row.areaCode || row.adcode || '';
|
||||||
},
|
},
|
||||||
openCommonAddress(target) {
|
openCommonAddress(target) {
|
||||||
|
if (this.isBasicRouteReadonly) return;
|
||||||
this.addressTarget = target;
|
this.addressTarget = target;
|
||||||
if (this.addressTransportMode(target) !== 'road') {
|
if (this.addressTransportMode(target) !== 'road') {
|
||||||
this.stationQuery = {};
|
this.stationQuery = {};
|
||||||
@@ -1209,6 +1278,7 @@ export default {
|
|||||||
this.loadRegionOptions().then(() => this.syncAddressRegionPath(this.addressTarget));
|
this.loadRegionOptions().then(() => this.syncAddressRegionPath(this.addressTarget));
|
||||||
},
|
},
|
||||||
handleAddressDetailClick(target) {
|
handleAddressDetailClick(target) {
|
||||||
|
if (this.isBasicRouteReadonly) return;
|
||||||
if (this.addressTransportMode(target) !== 'road') {
|
if (this.addressTransportMode(target) !== 'road') {
|
||||||
this.openCommonAddress(target);
|
this.openCommonAddress(target);
|
||||||
return;
|
return;
|
||||||
@@ -1216,6 +1286,7 @@ export default {
|
|||||||
this.openAddressMap(target);
|
this.openAddressMap(target);
|
||||||
},
|
},
|
||||||
openAddressMap(target) {
|
openAddressMap(target) {
|
||||||
|
if (this.isBasicRouteReadonly) return;
|
||||||
if (this.addressTransportMode(target) !== 'road') {
|
if (this.addressTransportMode(target) !== 'road') {
|
||||||
this.$message.info('非公路运输地址不能使用地图选择');
|
this.$message.info('非公路运输地址不能使用地图选择');
|
||||||
return;
|
return;
|
||||||
@@ -1273,30 +1344,31 @@ export default {
|
|||||||
.catch(() => this.$message.error('高德地图组件加载失败'));
|
.catch(() => this.$message.error('高德地图组件加载失败'));
|
||||||
},
|
},
|
||||||
searchAddressMap() {
|
searchAddressMap() {
|
||||||
if (!this.mapKeyword.trim()) return this.$message.warning('请输入地址关键词');
|
const keyword = String(this.mapKeyword || '').trim();
|
||||||
|
if (!keyword) return this.$message.warning('请输入地址关键词');
|
||||||
|
if (this.mapLoading) return;
|
||||||
|
this.mapSearchResults = [];
|
||||||
|
this.mapLoading = true;
|
||||||
this.loadAmap()
|
this.loadAmap()
|
||||||
.then(() => this.ensureAddressGeocoder())
|
.then(() => searchAmapPlaceText(keyword))
|
||||||
.then(
|
.then(pois => {
|
||||||
() =>
|
this.mapSearchResults = normalizeMapSearchResults(pois, keyword);
|
||||||
new Promise((resolve, reject) =>
|
const first = this.mapSearchResults[0];
|
||||||
this.addressMapGeocoder.getLocation(this.mapKeyword, (status, result) =>
|
if (!first?.location) {
|
||||||
status === 'complete' ? resolve(result) : reject(result)
|
this.mapStatus = '未找到匹配地址';
|
||||||
)
|
return this.$message.warning('地图搜索无匹配地址');
|
||||||
)
|
}
|
||||||
)
|
return this.$nextTick().then(() => {
|
||||||
.then(result => {
|
this.pickAddressMap(first.location, first.address || first.name || keyword);
|
||||||
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('地图搜索无匹配地址');
|
|
||||||
this.pickAddressMap(point, this.mapKeyword);
|
|
||||||
})
|
})
|
||||||
.catch(() => this.$message.error('地图搜索失败'));
|
.catch(() => {
|
||||||
|
this.mapStatus = '未找到匹配地址';
|
||||||
|
this.$message.warning('地图搜索无匹配地址');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.mapLoading = false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
selectAddressMapSearchResult(item) {
|
selectAddressMapSearchResult(item) {
|
||||||
if (item && item.location) {
|
if (item && item.location) {
|
||||||
@@ -1618,7 +1690,7 @@ export default {
|
|||||||
'',
|
'',
|
||||||
cargoTypePath,
|
cargoTypePath,
|
||||||
quantity: row.quantity || '',
|
quantity: row.quantity || '',
|
||||||
quantityUnit: row.quantityUnit || quantityUnitOptions[0],
|
quantityUnit: row.quantityUnit || this.defaultQuantityUnit(),
|
||||||
packageType: row.packageType || row.package || '',
|
packageType: row.packageType || row.package || '',
|
||||||
brand: row.brand || '',
|
brand: row.brand || '',
|
||||||
specification: row.specification || row.spec || '',
|
specification: row.specification || row.spec || '',
|
||||||
@@ -1662,6 +1734,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
openRouteDialog() {
|
openRouteDialog() {
|
||||||
|
if (this.isBasicRouteReadonly) return;
|
||||||
this.routeQuery = {};
|
this.routeQuery = {};
|
||||||
this.routePage.current = 1;
|
this.routePage.current = 1;
|
||||||
this.routeDialogVisible = true;
|
this.routeDialogVisible = true;
|
||||||
@@ -1719,21 +1792,44 @@ export default {
|
|||||||
return { segmentNo: `段${index + 1}`, transportType: '' };
|
return { segmentNo: `段${index + 1}`, transportType: '' };
|
||||||
},
|
},
|
||||||
addRoute() {
|
addRoute() {
|
||||||
|
if (this.isBasicRouteReadonly) return;
|
||||||
this.form.routes.push(this.createRoute());
|
this.form.routes.push(this.createRoute());
|
||||||
},
|
},
|
||||||
removeRoute(index) {
|
removeRoute(index) {
|
||||||
|
if (this.isBasicRouteReadonly) return;
|
||||||
this.form.routes.splice(index, 1);
|
this.form.routes.splice(index, 1);
|
||||||
this.syncAddressRegionPaths();
|
this.syncAddressRegionPaths();
|
||||||
},
|
},
|
||||||
|
goodsKey(row = {}) {
|
||||||
|
return `${row.cargoName || ''}\u0000${row.cargoType || ''}`;
|
||||||
|
},
|
||||||
|
dispatchedQuantityOf(row = {}) {
|
||||||
|
return Number(this.dispatchedGoodsMap[this.goodsKey(row)] || 0);
|
||||||
|
},
|
||||||
|
formatDispatchedQuantity(value) {
|
||||||
|
const number = Number(value);
|
||||||
|
if (!Number.isFinite(number)) return '0';
|
||||||
|
return String(number);
|
||||||
|
},
|
||||||
|
isGoodsCategoryLocked(row = {}) {
|
||||||
|
return this.dispatchedQuantityOf(row) > 0;
|
||||||
|
},
|
||||||
addGoods(index) {
|
addGoods(index) {
|
||||||
this.form.goods.splice(index + 1, 0, {
|
this.form.goods.splice(index + 1, 0, {
|
||||||
quantityUnit: quantityUnitOptions[0],
|
quantityUnit: this.defaultQuantityUnit(),
|
||||||
materialCode: '',
|
materialCode: '',
|
||||||
deviceCode: '',
|
deviceCode: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
removeGoods(index) {
|
removeGoods(index) {
|
||||||
|
const row = this.form.goods[index];
|
||||||
|
if (this.isGoodsCategoryLocked(row)) {
|
||||||
|
this.$message.warning(
|
||||||
|
`货物【${row.cargoName || ''}】已有调度量,不能删除`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.form.goods.length > 1) this.form.goods.splice(index, 1);
|
if (this.form.goods.length > 1) this.form.goods.splice(index, 1);
|
||||||
},
|
},
|
||||||
handleQuantityInput(row, value) {
|
handleQuantityInput(row, value) {
|
||||||
@@ -1741,6 +1837,42 @@ export default {
|
|||||||
const [integer = '', ...decimals] = text.split('.');
|
const [integer = '', ...decimals] = text.split('.');
|
||||||
row.quantity = decimals.length ? `${integer}.${decimals.join('').slice(0, 3)}` : integer;
|
row.quantity = decimals.length ? `${integer}.${decimals.join('').slice(0, 3)}` : integer;
|
||||||
},
|
},
|
||||||
|
enforceMinDispatchedQuantity(row = {}) {
|
||||||
|
const min = this.dispatchedQuantityOf(row);
|
||||||
|
if (min <= 0) return true;
|
||||||
|
const current = Number(row.quantity);
|
||||||
|
if (!Number.isFinite(current) || current < min) {
|
||||||
|
row.quantity = String(min);
|
||||||
|
this.$message.warning(
|
||||||
|
`货物【${row.cargoName || ''}】数量不能低于已调度量(${this.formatDispatchedQuantity(min)})`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
validateGoodsDispatchedQuantity() {
|
||||||
|
const totals = {};
|
||||||
|
(this.form.goods || []).forEach(row => {
|
||||||
|
const key = this.goodsKey(row);
|
||||||
|
totals[key] = (totals[key] || 0) + (Number(row.quantity) || 0);
|
||||||
|
});
|
||||||
|
for (const [key, qty] of Object.entries(this.dispatchedGoodsMap)) {
|
||||||
|
const dispatched = Number(qty) || 0;
|
||||||
|
if (dispatched <= 0) continue;
|
||||||
|
const cargoName = String(key).split('\u0000')[0] || '未知货物';
|
||||||
|
if (!(key in totals)) {
|
||||||
|
this.$message.warning(`货物【${cargoName}】已有调度量,不能删除或变更品类`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((totals[key] || 0) < dispatched) {
|
||||||
|
this.$message.warning(
|
||||||
|
`货物【${cargoName}】数量不能低于已调度量(${this.formatDispatchedQuantity(dispatched)})`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
syncAttachmentsJson() {
|
syncAttachmentsJson() {
|
||||||
this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
|
this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
|
||||||
},
|
},
|
||||||
@@ -1863,6 +1995,7 @@ export default {
|
|||||||
return this.$message.warning(`请完整填写第${invalidRouteIndex + 1}段途经地及运输方式`);
|
return this.$message.warning(`请完整填写第${invalidRouteIndex + 1}段途经地及运输方式`);
|
||||||
if (!this.form.finalTransportType) return this.$message.warning('请填写最后一段运输方式');
|
if (!this.form.finalTransportType) return this.$message.warning('请填写最后一段运输方式');
|
||||||
}
|
}
|
||||||
|
if (!this.validateGoodsDispatchedQuantity()) return false;
|
||||||
const payload = {
|
const payload = {
|
||||||
...this.form,
|
...this.form,
|
||||||
planStartTime: this.normalizeDateTimeValue(this.form.planStartTime),
|
planStartTime: this.normalizeDateTimeValue(this.form.planStartTime),
|
||||||
@@ -1945,9 +2078,19 @@ export default {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.el-form-item {
|
.el-form-item {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.master-editor__quantity-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.master-editor__quantity-hint {
|
||||||
|
color: #e6a23c;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
.goods-heading {
|
.goods-heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
@@ -2199,8 +2342,15 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.el-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.address-map {
|
.address-map {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
height: 440px;
|
height: 440px;
|
||||||
}
|
}
|
||||||
.address-map-status {
|
.address-map-status {
|
||||||
|
|||||||
@@ -0,0 +1,305 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="groups.length" class="punch-record-photos">
|
||||||
|
<section v-for="group in groups" :key="group.type" class="punch-record-photos__group">
|
||||||
|
<div class="punch-record-photos__head">
|
||||||
|
<span class="punch-record-photos__title">{{ group.type }}</span>
|
||||||
|
<span v-if="editable" class="punch-record-photos__count">{{ countText(group) }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="punch-record-photos__grid"
|
||||||
|
:class="{ 'punch-record-photos__grid--edit': editable }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(item, groupIndex) in group.items"
|
||||||
|
:key="`${item.photo.url}-${item.index}`"
|
||||||
|
class="punch-record-photos__item"
|
||||||
|
@click="handlePreview(item.index)"
|
||||||
|
>
|
||||||
|
<el-image
|
||||||
|
:src="item.photo.url"
|
||||||
|
:preview-src-list="nativePreview ? previewUrls : []"
|
||||||
|
:initial-index="nativePreview ? item.index : 0"
|
||||||
|
:preview-teleported="nativePreview"
|
||||||
|
:lazy="lazy"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<!-- 主图 = 该类型的第一张,按上传顺序取;没有独立字段,也不可切换 -->
|
||||||
|
<div v-if="groupIndex === 0" class="punch-record-photos__badge">主图</div>
|
||||||
|
<el-link
|
||||||
|
v-if="editable"
|
||||||
|
class="punch-record-photos__remove"
|
||||||
|
type="danger"
|
||||||
|
:underline="false"
|
||||||
|
@click.stop="$emit('remove', item.index)"
|
||||||
|
>删除</el-link
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<el-upload
|
||||||
|
v-if="editable && !group.full"
|
||||||
|
class="punch-record-photos__upload"
|
||||||
|
:action="uploadAction"
|
||||||
|
name="file"
|
||||||
|
:headers="uploadHeaders"
|
||||||
|
:show-file-list="false"
|
||||||
|
:before-upload="file => handleBeforeUpload(file, group.type)"
|
||||||
|
:on-success="res => $emit('upload-success', res, group.type)"
|
||||||
|
:on-error="error => $emit('upload-error', error, group.type)"
|
||||||
|
>
|
||||||
|
<div class="punch-record-photos__add">+ 添加</div>
|
||||||
|
</el-upload>
|
||||||
|
<div v-else-if="editable" class="punch-record-photos__add punch-record-photos__add--full">
|
||||||
|
已满 {{ maxPerType }} 张
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
// 打卡照片是每个打卡节点必有的一个照片类型,不由过程配置的凭证类型决定;
|
||||||
|
// 它与各凭证类型各自最多 9 张,同一类型下第一张即界面上的「主图」。
|
||||||
|
const PUNCH_PHOTO_TYPE = '打卡照片';
|
||||||
|
// 照片没带类型时的兜底名称,与后端落库时的兜底口径一致
|
||||||
|
const PHOTO_TYPE_FALLBACK = '凭证';
|
||||||
|
const MAX_PHOTOS_PER_TYPE = 9;
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// 扁平照片数组:管理端回显用 voucherType,司机端回显与旧数据用 type
|
||||||
|
photos: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
// 恒定展示的照片类型:「打卡照片」+ 该节点过程配置勾选的凭证类型。
|
||||||
|
// 留空则只按照片自身带的类型分组(只读回显用)。
|
||||||
|
groupTypes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
// 编辑态:每个类型自带「添加」格与删除链接,并显示已选/上限
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// 每个类型的张数上限;0 表示不限制(在途打卡只有一个照片字段,张数由「只保留第一张」约束)
|
||||||
|
maxPerType: {
|
||||||
|
type: Number,
|
||||||
|
default: MAX_PHOTOS_PER_TYPE,
|
||||||
|
},
|
||||||
|
// 编辑态用 el-image 自带预览;只读态不发预览事件,把点击交给调用方开自己的看图器
|
||||||
|
nativePreview: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
lazy: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
uploadAction: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
uploadHeaders: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
// 返回 false 即中止本次上传;类型、体积、张数校验都在调用方
|
||||||
|
beforeUpload: {
|
||||||
|
type: Function,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['preview', 'remove', 'upload-success', 'upload-error']);
|
||||||
|
|
||||||
|
const photoType = photo =>
|
||||||
|
String(photo?.voucherType || photo?.type || '').trim() || PHOTO_TYPE_FALLBACK;
|
||||||
|
|
||||||
|
const groups = computed(() => {
|
||||||
|
const photos = Array.isArray(props.photos) ? props.photos : [];
|
||||||
|
const names = [];
|
||||||
|
const buckets = new Map();
|
||||||
|
const ensure = name => {
|
||||||
|
if (!buckets.has(name)) {
|
||||||
|
buckets.set(name, []);
|
||||||
|
names.push(name);
|
||||||
|
}
|
||||||
|
return buckets.get(name);
|
||||||
|
};
|
||||||
|
// 恒定展示的类型先占位:一张照片都没有的类型也要出现,否则编辑态下没有添加入口
|
||||||
|
props.groupTypes.forEach(type => {
|
||||||
|
const name = String(type || '').trim();
|
||||||
|
if (name) ensure(name);
|
||||||
|
});
|
||||||
|
// 按上传顺序入组,组间顺序即类型首次出现的顺序,不重排照片
|
||||||
|
photos.forEach((photo, index) => {
|
||||||
|
if (!photo?.url) return;
|
||||||
|
ensure(photoType(photo)).push({ photo, index });
|
||||||
|
});
|
||||||
|
const list = names.map(type => {
|
||||||
|
const items = buckets.get(type);
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
items,
|
||||||
|
full: props.editable && props.maxPerType > 0 && items.length >= props.maxPerType,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// 编辑态至少要有一个分组,否则记录里还没有照片时就没有添加入口
|
||||||
|
if (props.editable && !list.length) {
|
||||||
|
return [{ type: props.groupTypes[0] || PUNCH_PHOTO_TYPE, items: [], full: false }];
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 预览列表按原始下标对齐,删除/分组都不改变点击某张时打开的是哪一张
|
||||||
|
const previewUrls = computed(() =>
|
||||||
|
(Array.isArray(props.photos) ? props.photos : []).map(photo => photo?.url || '')
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePreview = index => {
|
||||||
|
if (props.nativePreview) return;
|
||||||
|
emit('preview', index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBeforeUpload = (file, type) => {
|
||||||
|
if (typeof props.beforeUpload !== 'function') return true;
|
||||||
|
return props.beforeUpload(file, type);
|
||||||
|
};
|
||||||
|
|
||||||
|
const countText = group =>
|
||||||
|
props.maxPerType > 0 ? `${group.items.length}/${props.maxPerType}` : String(group.items.length);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.punch-record-photos {
|
||||||
|
// 弹窗里本组件挂在 el-form-item__content 下,那是个 flex 容器:
|
||||||
|
// 不给宽度就会收缩成内容宽,里面的网格跟着塌成一小块
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
|
&__group {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只读回显:撑满容器宽度的五列网格
|
||||||
|
&__grid {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑态沿用弹窗原本的固定 96px 方图 + 自动换行:尺寸不随容器宽窄变化
|
||||||
|
&__grid--edit {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.el-image {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border: 1px solid #eff1f7;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__grid--edit &__item {
|
||||||
|
width: 96px;
|
||||||
|
|
||||||
|
.el-image {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
aspect-ratio: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 4px 0 4px 0;
|
||||||
|
background: rgba(64, 158, 255, 0.9);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__remove {
|
||||||
|
align-self: center;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__upload {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__grid--edit &__upload {
|
||||||
|
width: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__add {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border: 1px dashed #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__grid--edit &__add {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
aspect-ratio: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__add--full {
|
||||||
|
color: #c0c4cc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
<el-option
|
<el-option
|
||||||
v-for="item in contractOptions"
|
v-for="item in contractOptions"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:label="item.contractName"
|
:label="formatContractOptionLabel(item)"
|
||||||
:value="item.id"
|
:value="item.id"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -454,11 +454,20 @@
|
|||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column type="index" label="序号" width="70" />
|
<el-table-column type="index" label="序号" width="70" />
|
||||||
<el-table-column label="文件名" min-width="220"
|
<el-table-column
|
||||||
|
label="文件名"
|
||||||
|
min-width="220"
|
||||||
|
show-overflow-tooltip
|
||||||
|
class-name="is-ellipsis-link"
|
||||||
><template #default="{ row }"
|
><template #default="{ row }"
|
||||||
><el-link type="primary" @click="previewAttachment(row)">{{
|
><span
|
||||||
attachmentName(row)
|
class="attachment-file-name"
|
||||||
}}</el-link></template
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="previewAttachment(row)"
|
||||||
|
@keydown.enter="previewAttachment(row)"
|
||||||
|
@keydown.space.prevent="previewAttachment(row)"
|
||||||
|
>{{ attachmentName(row) }}</span></template
|
||||||
></el-table-column
|
></el-table-column
|
||||||
>
|
>
|
||||||
<el-table-column label="附件描述" min-width="220"
|
<el-table-column label="附件描述" min-width="220"
|
||||||
@@ -721,11 +730,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<el-table :data="detailAttachmentRows" border empty-text="暂无附件">
|
<el-table :data="detailAttachmentRows" border empty-text="暂无附件">
|
||||||
<el-table-column type="index" label="序号" width="70" />
|
<el-table-column type="index" label="序号" width="70" />
|
||||||
<el-table-column label="文件名" min-width="220">
|
<el-table-column
|
||||||
|
label="文件名"
|
||||||
|
min-width="220"
|
||||||
|
show-overflow-tooltip
|
||||||
|
class-name="is-ellipsis-link"
|
||||||
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-link type="primary" @click="previewAttachment(row)">{{
|
<span
|
||||||
attachmentName(row)
|
class="attachment-file-name"
|
||||||
}}</el-link>
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="previewAttachment(row)"
|
||||||
|
@keydown.enter="previewAttachment(row)"
|
||||||
|
@keydown.space.prevent="previewAttachment(row)"
|
||||||
|
>{{ attachmentName(row) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="附件描述" min-width="220">
|
<el-table-column label="附件描述" min-width="220">
|
||||||
@@ -931,11 +950,11 @@
|
|||||||
/><el-button type="primary" @click="searchTransportMapKeyword">搜索</el-button>
|
/><el-button type="primary" @click="searchTransportMapKeyword">搜索</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="map-picker-content">
|
<div class="map-picker-content">
|
||||||
|
<div ref="transportMap" class="shipping-template-page__map" />
|
||||||
<map-search-results
|
<map-search-results
|
||||||
:results="transportMapSearchResults"
|
:results="transportMapSearchResults"
|
||||||
@select="selectTransportMapSearchResult"
|
@select="selectTransportMapSearchResult"
|
||||||
/>
|
/>
|
||||||
<div ref="transportMap" class="shipping-template-page__map" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="shipping-template-page__map-info">{{ transportMapStatus }}</div>
|
<div class="shipping-template-page__map-info">{{ transportMapStatus }}</div>
|
||||||
<template #footer
|
<template #footer
|
||||||
@@ -1098,6 +1117,9 @@ import {
|
|||||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||||
import { getDictionary } from '@/api/system/dictbiz';
|
import { getDictionary } from '@/api/system/dictbiz';
|
||||||
import { packageOptions } from '@/option/business/common';
|
import { packageOptions } from '@/option/business/common';
|
||||||
|
import { formatContractOptionLabel } from '@/utils/contract';
|
||||||
|
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||||
|
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||||
import { isMobile } from '@/utils/validate';
|
import { isMobile } from '@/utils/validate';
|
||||||
import { Location } from '@element-plus/icons-vue';
|
import { Location } from '@element-plus/icons-vue';
|
||||||
@@ -1114,6 +1136,7 @@ import '@open-file-viewer/core/style.css';
|
|||||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||||
import { h, resolveComponent } from 'vue';
|
import { h, resolveComponent } from 'vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { isAdministrator } from '@/utils/auth';
|
||||||
|
|
||||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||||
@@ -1276,7 +1299,8 @@ export default {
|
|||||||
cargoTypeRequest: null,
|
cargoTypeRequest: null,
|
||||||
transportCargoRows: [],
|
transportCargoRows: [],
|
||||||
packageOptions,
|
packageOptions,
|
||||||
quantityUnitOptions: ['吨', '千克', '立方米', '立方', '公里', '单', '件', '车', '箱', '托盘'],
|
quantityUnitOptions: [],
|
||||||
|
quantityUnitLoading: false,
|
||||||
feeUnitOptions: [],
|
feeUnitOptions: [],
|
||||||
currencyOptions: [],
|
currencyOptions: [],
|
||||||
currencyLoading: false,
|
currencyLoading: false,
|
||||||
@@ -1360,7 +1384,7 @@ export default {
|
|||||||
},
|
},
|
||||||
isAdmin() {
|
isAdmin() {
|
||||||
return (
|
return (
|
||||||
String(this.userInfo?.authority || '').includes('admin') ||
|
isAdministrator(this.userInfo) ||
|
||||||
this.userInfo?.authority?.includes?.('admin')
|
this.userInfo?.authority?.includes?.('admin')
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1523,11 +1547,26 @@ export default {
|
|||||||
created() {
|
created() {
|
||||||
this.loadProjectOptions();
|
this.loadProjectOptions();
|
||||||
this.ensureCargoTypeOptions();
|
this.ensureCargoTypeOptions();
|
||||||
|
this.loadQuantityUnitOptions();
|
||||||
this.loadFeeUnitOptions();
|
this.loadFeeUnitOptions();
|
||||||
this.loadCurrencyOptions();
|
this.loadCurrencyOptions();
|
||||||
if (this.isStandaloneFormPage) this.$nextTick(() => this.initStandaloneFormPage());
|
if (this.isStandaloneFormPage) this.$nextTick(() => this.initStandaloneFormPage());
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formatContractOptionLabel,
|
||||||
|
loadQuantityUnitOptions() {
|
||||||
|
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
|
||||||
|
this.quantityUnitLoading = true;
|
||||||
|
this.quantityUnitLoadPromise = loadMeasurementUnitOptions()
|
||||||
|
.then(options => {
|
||||||
|
this.quantityUnitOptions = options.map(item => item.value);
|
||||||
|
return this.quantityUnitOptions;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.quantityUnitLoading = false;
|
||||||
|
});
|
||||||
|
return this.quantityUnitLoadPromise;
|
||||||
|
},
|
||||||
buildOption(option) {
|
buildOption(option) {
|
||||||
const next = { ...option, column: (option.column || []).map(column => ({ ...column })) };
|
const next = { ...option, column: (option.column || []).map(column => ({ ...column })) };
|
||||||
const tableOrder = this.config.tableColumnOrder || [];
|
const tableOrder = this.config.tableColumnOrder || [];
|
||||||
@@ -2610,26 +2649,22 @@ export default {
|
|||||||
searchTransportMapKeyword() {
|
searchTransportMapKeyword() {
|
||||||
const keyword = String(this.transportMapKeyword || '').trim();
|
const keyword = String(this.transportMapKeyword || '').trim();
|
||||||
if (!keyword) return this.$message.warning('请输入地址关键词');
|
if (!keyword) return this.$message.warning('请输入地址关键词');
|
||||||
|
if (this.transportMapLoading) return;
|
||||||
|
this.transportMapSearchResults = [];
|
||||||
this.transportMapLoading = true;
|
this.transportMapLoading = true;
|
||||||
this.loadAmap()
|
this.loadAmap()
|
||||||
.then(() => this.ensureTransportMapGeocoder())
|
.then(() => searchAmapPlaceText(keyword))
|
||||||
.then(() => this.runTransportMapGeocode('location', keyword))
|
.then(pois => {
|
||||||
.then(result => {
|
this.transportMapSearchResults = normalizeMapSearchResults(pois, keyword);
|
||||||
this.transportMapSearchResults = (result.geocodes || []).map((item, index) => ({
|
const first = this.transportMapSearchResults[0];
|
||||||
id: item.id || index,
|
if (!first?.location) throw new Error('未找到匹配地址');
|
||||||
name: item.formattedAddress || keyword,
|
|
||||||
address: item.formattedAddress || keyword,
|
|
||||||
location: item.location,
|
|
||||||
}));
|
|
||||||
const point = this.resolveTransportMapPoint(result);
|
|
||||||
if (!point) throw new Error('未找到匹配地址');
|
|
||||||
return this.$nextTick().then(() => {
|
return this.$nextTick().then(() => {
|
||||||
this.transportMapInstance?.resize();
|
this.transportMapInstance?.resize();
|
||||||
return this.pickMapPoint(point, keyword);
|
return this.pickMapPoint(first.location, first.address || first.name || keyword);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.transportMapStatus = '地图搜索失败';
|
this.transportMapStatus = '未找到匹配地址';
|
||||||
this.$message.warning('地图搜索无匹配地址');
|
this.$message.warning('地图搜索无匹配地址');
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -3508,7 +3543,7 @@ export default {
|
|||||||
}
|
}
|
||||||
.shipping-template-page__detail-form :deep(.el-form-item) {
|
.shipping-template-page__detail-form :deep(.el-form-item) {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
.shipping-template-page__detail-form :deep(.el-form-item__label) {
|
.shipping-template-page__detail-form :deep(.el-form-item__label) {
|
||||||
color: #909399;
|
color: #909399;
|
||||||
@@ -3697,7 +3732,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:global(.shipping-template-form-page-dialog .avue-group .el-form-item) {
|
:global(.shipping-template-form-page-dialog .avue-group .el-form-item) {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.shipping-template-form-page-dialog .avue-form__menu) {
|
:global(.shipping-template-form-page-dialog .avue-form__menu) {
|
||||||
@@ -3754,6 +3789,12 @@ export default {
|
|||||||
.shipping-template-page__map-toolbar {
|
.shipping-template-page__map-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.el-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.shipping-template-page__dialog-search {
|
.shipping-template-page__dialog-search {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
@@ -3766,11 +3807,9 @@ export default {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
.shipping-template-page__map {
|
.shipping-template-page__map {
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 480px;
|
height: 480px;
|
||||||
margin-top: 12px;
|
|
||||||
}
|
}
|
||||||
:deep(.shipping-template-page__map > .amap-container) {
|
:deep(.shipping-template-page__map > .amap-container) {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user