Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bc1f69a7f | |||
| 6de87ed4e2 | |||
| 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'
|
||||
|
||||
#接口地址
|
||||
# 开发环境建议填 [/api]:由下方 vite.config.mjs 的 proxy 同源转发到后端(172.16.203.228:8000),避免跨域(CORS)被浏览器拦截
|
||||
# 如需直连线上绝对地址(如 http://172.16.203.228:8000/api),必须让后端 CORS 把 Allow-Origin 改为具体前端域名,不能用 `*`(配合 credentials 会被浏览器拒绝)
|
||||
# 开发环境建议填 [/api]:由 vite.config.mjs 的 proxy 同源转发到本地网关 http://localhost(80)
|
||||
# 本地需先启动:Nacos + blade-gateway(80) + blade-auth + blade-system 等;8080 端口一般是 Nacos 控制台不是网关
|
||||
# 如需临时联调远程,把 vite proxy target 改为 http://172.16.203.228:8000 并去掉 rewrite
|
||||
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,8 +12,46 @@
|
||||
|
||||
**坑二:宿主 view 直绑全局 `$route.query.*` 给子组件 prop。** 缓存实例被灌入当前页 id → 拿错 id 查后端(批量弹「运单管理不存在」),live computed 还会误 push 标签。修法:`routePathLocked` 锁路径 + watcher / push 前判 `isOwnedRouteActive` + 宿主绑定加路径守卫。⚠️ `routePathLocked` **必须放 `data()`**(Options API 顺序 data → computed → watch(immediate) → created;放 created 会读到空值、挡掉首次加载)。已修 waybill-manage-page / transport-plan-page + 三个宿主 view。
|
||||
|
||||
**坑四:avue-crud 弹窗 `append-to-body` 默认 true(`dialog-form3.js`:`validData(tableOption.dialogAppendToBody, true)`),DOM Teleport 到 body。** 于是页面里以页面容器为祖先的 scoped 选择器(`.xxx-page :deep(.yyy)`)**命中不到弹窗内容**,尤其 `el-cascader`(inline-block、宽度按内容收缩,不像 `el-input`/`el-autocomplete` 有 `--el-input-width:100%` 兜底)会明显比同行 input 窄。修法:option 里配 `dialogCustomClass: 'xxx-dialog'`,规则写到 `src/styles/element-ui.scss` 的 `.xxx-dialog ...`(项目既有约定,10+ 处同款);或把基准类放在 **Teleport 内部元素**上(如 ship.vue 的 `.ship-form` 挂在 el-form 上,自身带 scope 属性,照样生效)。
|
||||
|
||||
**坑五:avue 表单项控件不是 `.el-form-item__content` 的直接子级。** `form/index3.js` 渲染为 `content > div > [div|elTooltip] > 控件`,所以 `.el-form-item__content > .el-cascader` 这类**直接子级选择器对 avue 表单全部失效**(手写 `<el-form-item>` 才生效),一律用后代选择器。
|
||||
|
||||
**坑六:`labelWidth: 'auto'` 在 Element Plus 里逐项计算,不是全表单共享。** 实测同一弹窗:3 字 54/64px、2 字 40px、4 字 68px、4 字带必填星号 78px;label 越宽 → 控件可用宽度越小,同级控件宽度就不一致(车牌号 input 486 vs 使用部门 cascader 472)。要让同一行控件等宽必须统一 label 宽度:`.xxx-dialog .el-form-item__label { flex: 0 0 calc(Nem + 24px) !important; width: calc(Nem + 24px) !important }`(N=本列最长汉字数,与 ship.vue、`.dialog-form--carded` 同款)。
|
||||
|
||||
**验证 avue 弹窗 DOM/宽度的可复用办法:** 项目根建临时 html(Vite 直接服务)→ 引真实 option 与 `src/styles/element-ui.scss`,挂 avue-crud 并 `mounted(){ this.$refs.crud.rowAdd() }` 弹出真弹窗;用 `agent-browser`(`/Users/gxwebsoft/.workbuddy/binaries/node/workspace/node_modules/.bin/agent-browser`)`open` + `eval` 读 DOM 链路与 `getBoundingClientRect`,用完删文件。
|
||||
|
||||
**坑三:路由表重复定义 → 内容区整页空白(2026-09-18)。** `/business/contract-manage/detail` 在 `router/views/index.js` 定义两次、子路由同名 `合同详情`。重名 `addRoute` 会 `removeRoute(name)` 移除旧子记录,但**不从父记录 children 里摘除**,第一条 Layout 壳记录留在 matcher 且靠前 → `resolve()` 命中它 → `$route.matched.length === 1` → 二级 router-view 无匹配 → 侧栏/标签正常但内容空白。修法:删重复定义。排查:扫「顶层 path + 首个子路由 name」重复项。
|
||||
|
||||
## 列表页右上角按钮组(table-header-tools 标准件)
|
||||
⚠️ **2026-10-14 修正:最终方向是手写列表页改用 `avue-crud`**。用户要求与其它模块"完全一致",而 avue 的列显隐抽屉(`el-drawer`+表格,隐藏/冻结/过滤/排序,行拖拽)是 avue-crud 内部子组件(`inject: ['crud']`),脱离 avue-crud 拿不出来;第 4 键也是 avue 的"表格↔卡片视图"切换。所以 `table-header-tools` 只是过渡仿制件,**新页面不要再接**,直接写 avue-crud。
|
||||
|
||||
手写 `el-table` 列表页若暂时保留,右上角统一用 `src/components/table-header-tools/main.vue`,复刻 avue-crud 默认 4 键:刷新 `el-icon-refresh` / 列设置 `el-icon-operation` / 搜索显隐 `el-icon-search` / 表格尺寸 `el-icon-grid`(default→large→small 循环)。
|
||||
|
||||
- 接入范式:`<table-header-tools :columns="columns" v-model:visible-column-keys="visibleColumnKeys" v-model:table-size="tableSize" v-model:search-visible="searchVisible" @refresh="loadTable" />`;配套:搜索区 `v-if="searchVisible"`、el-table `:size="tableSize"`、列用 computed `visibleColumns = columns.filter(c => visibleColumnKeys.includes(c.prop))`。⚠️ 不能 `v-for` + `v-if` 同元素(Vue3 v-if 优先级更高,取不到 column 变量)。
|
||||
- 表格在子组件时(如 `payment/components/bill-*-table.vue`):子组件加 props `searchVisible` + emits `refresh`/`update:searchVisible`,父页面 `v-model:search-visible` + `@refresh`,搜索子组件加 `v-if="searchVisible"`。
|
||||
- `icon="el-icon-xxx"` 字符串**依赖 Avue 的全局 el-icon 映射**(`main.js` 只注册 EP 的 PascalCase 图标),临时验证页没 `app.use(Avue)` 会渲染成空圆按钮 —— 别误判成组件 bug。
|
||||
- 组件已加 `deactivated()` 关列设置弹窗(防 keep-alive 下 Teleport 弹窗残留)。
|
||||
- 已接入(过渡态,待逐页改 avue):business/{master-order,loading-manage,voucher-manage}、settlement、payment 模块手写页。
|
||||
|
||||
## avue-crud 列表页改造要点(payment 模块样板,2026-10-14)
|
||||
- **列显隐抽屉**:点第 2 键弹出,标题"列显隐"(语言包 `crud.showTitle`),表头 列名/隐藏/冻结/过滤/排序。直接读写 avue crud 的列配置,页面无需实现。
|
||||
- **第 4 键**:`handleGridShow()` = 表格 ↔ 卡片视图(`tableCard` 按列通用渲染),页面无需写卡片模板。
|
||||
- `searchOrder` 是**降序**排序(值越大越靠前):`list.sort((a,b) => (b.searchOrder||0) - (a.searchOrder||0))`。首次改造踩过坑。
|
||||
- `searchIndex: 4` + `searchIcon: true` → 默认只显示前 4 个搜索项 + "展开/收起"(columnLen > searchIndex 时才出现该按钮)。
|
||||
- **搜索项 label 与表格列 label 不同**(列「收款方」/搜索「客户名称」):单独定义 hide 列 + `searchProp` 指向后端字段(avue 在 search-change 里自动改名)。
|
||||
- 搜索专用列加 `hide: true` + `showColumn: false`,否则会混进列显隐抽屉。
|
||||
- ⚠️ **column 数组顺序 = 表格列顺序**:搜索专用列别插在表格列中间(会把表格列顶到前面);deptName 这种"既搜索又展示"的列要放回表格列的正确位置。
|
||||
- 分页:`page: { currentPage, pageSize, pageSizes, total }`;`on-load` 由 avue 内部(table-page)在 created / 翻页 / 改页容量时触发,`current-change`/`size-change` 里**只改 page 不要再调 onLoad**(否则双请求)。
|
||||
- `onLoad(page)` 只用 `this.query`(avue 传的 page 对象含 total/pageSizes 等分页字段,别 spread 进查询参数)。
|
||||
- 搜索参数转换放 `searchChange(params, done)`:日期区间数组拆成 `xxxStartDate/xxxEndDate`;所属组织级联用 `normalizeOrganizationSearch(params, 'deptName')`(见 `mixins/organization-search.js`,option 里该列加 `searchslot: true`)。
|
||||
- 单元格空值统一 `-`:列级 `formatter: (row, value) => ...`;金额 `Number(v||0).toFixed(2)`;状态/链接用 `slot: true` + `#prop` 插槽。
|
||||
- 操作列用 `#menu` 插槽(`menuWidth: 320`),`addBtn/viewBtn/delBtn/editBtn: false` 关掉 avue 自带按钮。
|
||||
|
||||
## 地址/行政区划展示(全站既有规则)
|
||||
「省/市/区县」都要空格分段,但**省与自治区不重复展示**:`上海市浦东新区`→`上海市 浦东新区`,`广西壮族自治区南宁市邕宁区`→`南宁市 邕宁区`(同 `formatRoadLocation` / `formatRouteCityDistrict` / 总单列表 `formatRoadAddress` 一致)。
|
||||
- 统一入口:`src/utils/region-address.js` 的 `formatRegionAddress(value)`(按后缀切分行政区划,`州+市`、`县+区` 连写合并;**只有整串都是行政区划**才重排,站点名「南宁站/广州南站」与含门牌的详细地址原样返回)。v-for 列表里用它做展示时,直接把函数挂进 `methods` 用 ES6 简写(`formatRegionAddress,`)。
|
||||
- ⚠️ 路线节点的名称字段来自后端 `routeJson`:`routes[i].departureName` = 第 i 段**到货地/途经地**(不是本段发货地),末段 = 最终收货地;`master.departureName/arrivalName` 才是总发货/总收货。总单调度/详情都按「上一个节点的 departureName」拼分段起点。
|
||||
|
||||
## 样式硬规则
|
||||
- 底栏 `flex; justify-content:flex-end`,**不写 gap**(靠 EP 默认 `.el-button+.el-button{margin-left:12px}`);浮动底栏必须 `position:fixed`(祖先 overflow:hidden 使 sticky 失效)+ `:global(.avue--collapse .x){left:60px}`、`:global(.avue-layout--horizontal .x){left:0}`,`:global()` 要包整个选择器。
|
||||
- 按钮层级:次要=无 type;中间步骤=`primary plain`;主操作=`primary`(禁绿、禁两个蓝实心)。顺序 `[辅助][取消][保存草稿][提交]`。
|
||||
@@ -25,3 +63,9 @@
|
||||
- `business-crud-page.vue`:option 经 `cloneOption` 克隆,独立表单页走 `PageAvueForm`。
|
||||
- 自检:`curl "localhost:2889/src/xxx.vue"` 与 `"...?vue&type=style&index=0&scoped=true&lang.scss"`(200 / 500)。`vite build` 在 `facelogin.vue` 被沙箱中断,不能作唯一校验。dev `VITE_APP_API=/api` 代理 `172.16.203.228:8000`。
|
||||
- 导出:前端 `exportColumns` 后端未用,以 `XxxExportExcel.java` 为准;BladeX `BeanUtil` 类型不兼容静默跳过(`Date` → `LocalDateTime` 丢值,用 `DateUtil.fromDate`)。
|
||||
|
||||
## Element Plus 图标两种用法的机制差异(易误判)
|
||||
`main.js` 里 `for (...ElementPlusIconsVue) app.component(key, component)` 全局注册了全部图标,因此模板里可以写标签形式 `<Location />`(靠全局注册解析)。
|
||||
但 `:icon="OfficeBuilding"` 是**表达式**,编译成 `_ctx.OfficeBuilding`,只会从 data / props / computed / setup 返回值里取 —— **组件对象必须放进 `data()`**;放进 `components: {}` 注册里模板取不到,会静默渲染不出图标(不报错,只是空白)。
|
||||
项目既有惯例:需要 `:icon` 的图标一律 `import` 后在 `data()` 里 `return { Xxx }`(transport-plan-page、waybill-manage-page、business-crud-page 都是这么写的)。
|
||||
注:标签形式(如 `<Location />`)不依赖 data,放不放 data 都能显示;仅 `:icon` 必须放 data。
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"crypto-js": "^4.1.1",
|
||||
"dayjs": "^1.10.6",
|
||||
"disable-devtool": "^0.3.8",
|
||||
"echarts": "^6.1.0",
|
||||
"element-plus": "^2.14.2",
|
||||
"exceljs": "^4.4.0",
|
||||
"highlight.js": "^11.9.0",
|
||||
|
||||
Generated
+18
-44
@@ -4,11 +4,6 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
patchedDependencies:
|
||||
'@smallwei/avue':
|
||||
hash: 5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db
|
||||
path: patches/@smallwei__avue.patch
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -33,7 +28,7 @@ importers:
|
||||
version: 1.7.0
|
||||
'@smallwei/avue':
|
||||
specifier: ~3.9.2
|
||||
version: 3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34))(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:
|
||||
specifier: ^4.1.1
|
||||
version: 4.1.1
|
||||
@@ -55,6 +50,9 @@ importers:
|
||||
disable-devtool:
|
||||
specifier: ^0.3.8
|
||||
version: 0.3.9
|
||||
echarts:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0
|
||||
element-plus:
|
||||
specifier: ^2.14.2
|
||||
version: 2.14.2(vue@3.5.34)
|
||||
@@ -63,13 +61,13 @@ importers:
|
||||
version: 4.4.0
|
||||
highlight.js:
|
||||
specifier: ^11.9.0
|
||||
version: 11.11.1
|
||||
version: 11.12.0
|
||||
js-base64:
|
||||
specifier: ^3.7.4
|
||||
version: 3.7.8
|
||||
version: 3.9.4
|
||||
js-cookie:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.5
|
||||
version: 3.0.8
|
||||
js-md5:
|
||||
specifier: ^0.7.3
|
||||
version: 0.7.3
|
||||
@@ -406,35 +404,30 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@1.0.2':
|
||||
resolution: {integrity: sha512-qwROoDIC9upfvDoRLuPn2aNg9CGW1x0Ygr4k2Or+8paA9d0qBLwk87U+g8KQpoOviKoPoiwl97kvBYuYD7qZoA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@1.0.2':
|
||||
resolution: {integrity: sha512-fXRjnPihdnbO6qy1QQOgxAonb68A0TCEG7rj1x7v7rxNElsE8EVIKIEUTvyDtU+sthYSbX+8e7g3oZiLGnOmxw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@1.0.2':
|
||||
resolution: {integrity: sha512-nPR97DXhbWIAy7yazF3jc06kEPMqYMLmPzFOVNlwKPfIoSChnI+x7dc0hTLaihz3jxrjL6j4BbA7earxfx4X3g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@1.0.2':
|
||||
resolution: {integrity: sha512-l7zZY5+jL5qnBZtDz7CoBtY6p7EkHu422g/0zWwrOrzIwWyWxZFRfZZORY1UG7YApymPLx+UbOkN206xXn/c1Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@1.0.2':
|
||||
resolution: {integrity: sha512-yE0koHCFF4PIbMc2o2SEALhnipz7WBISh5glLvQiomtIoCcW0np3H4Lw93ceJAfJttTTeIIWFbwH84F7EVzjMQ==}
|
||||
@@ -510,42 +503,36 @@ packages:
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.6':
|
||||
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.6':
|
||||
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.6':
|
||||
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.6':
|
||||
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.6':
|
||||
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.6':
|
||||
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
|
||||
@@ -612,79 +599,66 @@ packages:
|
||||
resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.60.3':
|
||||
resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.60.3':
|
||||
resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.60.3':
|
||||
resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.60.3':
|
||||
resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.60.3':
|
||||
resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.60.3':
|
||||
resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.60.3':
|
||||
resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.60.3':
|
||||
resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.60.3':
|
||||
resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.60.3':
|
||||
resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.60.3':
|
||||
resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.60.3':
|
||||
resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.60.3':
|
||||
resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==}
|
||||
@@ -1208,6 +1182,7 @@ packages:
|
||||
|
||||
crypto-js@4.2.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
@@ -1453,8 +1428,8 @@ packages:
|
||||
heic2any@0.0.4:
|
||||
resolution: {integrity: sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA==}
|
||||
|
||||
highlight.js@11.11.1:
|
||||
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
||||
highlight.js@11.12.0:
|
||||
resolution: {integrity: sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
hls.js@1.6.16:
|
||||
@@ -1532,12 +1507,11 @@ packages:
|
||||
isarray@1.0.0:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
|
||||
js-base64@3.7.8:
|
||||
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
|
||||
js-base64@3.9.4:
|
||||
resolution: {integrity: sha512-PtOMXpEGuP0RRiRXsjzHzl44dMHxSu2CPvAhinupR1tBa88me+1DPqsobl7eupn5UukjLkln1ZnAOAOXOrYG0Q==}
|
||||
|
||||
js-cookie@3.0.5:
|
||||
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
||||
engines: {node: '>=14'}
|
||||
js-cookie@3.0.8:
|
||||
resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==}
|
||||
|
||||
js-md5@0.7.3:
|
||||
resolution: {integrity: sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==}
|
||||
@@ -2653,7 +2627,7 @@ snapshots:
|
||||
countup.js: 2.10.0
|
||||
signature_pad: 5.1.3
|
||||
|
||||
'@smallwei/avue@3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34))(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:
|
||||
'@element-plus/icons-vue': 2.3.2(vue@3.5.34)
|
||||
axios: 0.21.4
|
||||
@@ -3546,7 +3520,7 @@ snapshots:
|
||||
|
||||
heic2any@0.0.4: {}
|
||||
|
||||
highlight.js@11.11.1: {}
|
||||
highlight.js@11.12.0: {}
|
||||
|
||||
hls.js@1.6.16: {}
|
||||
|
||||
@@ -3605,9 +3579,9 @@ snapshots:
|
||||
|
||||
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: {}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
packages:
|
||||
- '.'
|
||||
|
||||
allowBuilds:
|
||||
'@parcel/watcher': true
|
||||
es5-ext: true
|
||||
esbuild: true
|
||||
|
||||
patchedDependencies:
|
||||
'@smallwei/avue': patches/@smallwei__avue.patch
|
||||
|
||||
@@ -90,3 +90,11 @@ export const expireStats = params =>
|
||||
method: 'get',
|
||||
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,
|
||||
},
|
||||
});
|
||||
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 remove = api.remove;
|
||||
export const copy = api.copy;
|
||||
|
||||
@@ -52,6 +52,17 @@ export const createFileTask = data =>
|
||||
request({ url: `${fileTaskBaseUrl}/createFileTask`, method: 'post', data });
|
||||
export const getPartUploadUrl = (id, partNumber) =>
|
||||
request({ url: `${fileTaskBaseUrl}/getUploadUrl`, method: 'get', params: { id, partNumber } });
|
||||
export const uploadPartByProxy = (id, partNumber, partBlob) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', partBlob, `part-${partNumber}.bin`);
|
||||
return request({
|
||||
url: `${fileTaskBaseUrl}/uploadPart`,
|
||||
method: 'post',
|
||||
params: { id, partNumber },
|
||||
data: formData,
|
||||
timeout: 600000,
|
||||
});
|
||||
};
|
||||
export const updateFileTask = data =>
|
||||
request({ url: `${fileTaskBaseUrl}/updateFileTask`, method: 'post', data });
|
||||
export const pauseFileTask = ids =>
|
||||
|
||||
@@ -34,6 +34,14 @@ export const getPunchRecords = waybillId =>
|
||||
params: { waybillId },
|
||||
});
|
||||
|
||||
/** 编辑打卡记录(含打卡状态;punched=false 表示取消打卡) */
|
||||
export const savePunchRecord = data =>
|
||||
request({
|
||||
url: `${baseUrl}/punch-record/save`,
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
|
||||
/** 运单车辆实时定位 */
|
||||
export const locateVehicle = id =>
|
||||
request({
|
||||
@@ -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 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 saveImportDraft = data => request({ url: `${baseUrl}/import-batch/draft`, method: 'post', data });
|
||||
export const validateImport = data => request({ url: `${baseUrl}/import-batch/validate`, method: 'post', data, responseType: 'blob' });
|
||||
export const confirmImport = data => request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data });
|
||||
export const 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, 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 = [
|
||||
{ label: '项目预付', value: 'project_advance' },
|
||||
{ label: '进度预付', value: 'progress_advance' },
|
||||
{ label: '结算付款', value: 'settlement_payment' },
|
||||
{ label: '尾款付款', value: 'settlement_payment' },
|
||||
];
|
||||
export const approvalStatusOptions = [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
|
||||
@@ -6,8 +6,9 @@ import request from '@/axios';
|
||||
* @param {string} data.templateCode 模板编码
|
||||
* @param {string} data.submitIdentity 提交人(手机号)
|
||||
* @param {string} data.loginName 登录账号(手机号)
|
||||
* @param {string} data.formInstanceId 业务表单实例 id
|
||||
* @param {string} data.bizId 业务主键(后端生成/复用 formInstanceId)
|
||||
* @param {string} [data.subject] 流程标题
|
||||
* @param {string} [data.bizType] 业务类型
|
||||
*/
|
||||
export const processSubmit = data => {
|
||||
return request({
|
||||
@@ -20,7 +21,7 @@ export const processSubmit = data => {
|
||||
/**
|
||||
* 调用 MK processDelete 删除审核流(驳回后重新提交前使用)
|
||||
* @param {Object} data
|
||||
* @param {string} data.formInstanceId 业务表单实例 id
|
||||
* @param {string} data.bizId 业务主键
|
||||
*/
|
||||
export const processDelete = data => {
|
||||
return request({
|
||||
@@ -29,3 +30,30 @@ export const processDelete = 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({
|
||||
url: '/blade-system/dept/sync-oa-company',
|
||||
method: 'post',
|
||||
params: {
|
||||
current,
|
||||
size,
|
||||
incremental: !!incremental,
|
||||
modifiedSince: modifiedSince || undefined,
|
||||
},
|
||||
timeout: 60000,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const syncOaDepartment = (current = 1, size = 20, signal) => {
|
||||
export const syncOaDepartment = (
|
||||
current = 1,
|
||||
size = 20,
|
||||
signal,
|
||||
incremental = false,
|
||||
modifiedSince
|
||||
) => {
|
||||
return request({
|
||||
url: '/blade-system/dept/sync-oa-department',
|
||||
method: 'post',
|
||||
params: {
|
||||
current,
|
||||
size,
|
||||
incremental: !!incremental,
|
||||
modifiedSince: modifiedSince || undefined,
|
||||
},
|
||||
timeout: 60000,
|
||||
signal,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import request from '@/axios';
|
||||
|
||||
export const getList = (current, size, params) => {
|
||||
return request({
|
||||
url: '/blade-system/mp-user/list',
|
||||
method: 'get',
|
||||
params: {
|
||||
...params,
|
||||
current,
|
||||
size,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getDetail = id => {
|
||||
return request({
|
||||
url: '/blade-system/mp-user/detail',
|
||||
method: 'get',
|
||||
params: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const submit = row => {
|
||||
return request({
|
||||
url: '/blade-system/mp-user/submit',
|
||||
method: 'post',
|
||||
data: row,
|
||||
});
|
||||
};
|
||||
|
||||
export const remove = ids => {
|
||||
return request({
|
||||
url: '/blade-system/mp-user/remove',
|
||||
method: 'post',
|
||||
params: {
|
||||
ids,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const changeStatus = (id, status) => {
|
||||
return request({
|
||||
url: '/blade-system/mp-user/change-status',
|
||||
method: 'post',
|
||||
params: {
|
||||
id,
|
||||
status,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const resetPassword = userIds => {
|
||||
return request({
|
||||
url: '/blade-system/mp-user/reset-password',
|
||||
method: 'post',
|
||||
params: {
|
||||
userIds,
|
||||
},
|
||||
});
|
||||
};
|
||||
+14
-1
@@ -31,13 +31,15 @@ export const add = row => {
|
||||
});
|
||||
};
|
||||
|
||||
export const syncIamAccounts = (current = 1, size = 50, signal) => {
|
||||
export const syncIamAccounts = (current = 1, size = 50, signal, incremental = false, modifiedSince) => {
|
||||
return request({
|
||||
url: '/blade-system/user/sync-iam-accounts',
|
||||
method: 'post',
|
||||
params: {
|
||||
current,
|
||||
size,
|
||||
incremental: !!incremental,
|
||||
modifiedSince: modifiedSince || undefined,
|
||||
},
|
||||
timeout: 60000,
|
||||
signal,
|
||||
@@ -52,6 +54,17 @@ export const update = row => {
|
||||
});
|
||||
};
|
||||
|
||||
export const changeStatus = (id, status) => {
|
||||
return request({
|
||||
url: '/blade-system/user/change-status',
|
||||
method: 'post',
|
||||
params: {
|
||||
id,
|
||||
status,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const updatePlatform = (userId, userType, userExt) => {
|
||||
return request({
|
||||
url: '/blade-system/user/update-platform',
|
||||
|
||||
@@ -9,6 +9,8 @@ export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get'
|
||||
|
||||
export const follow = data => request({ url: `${baseUrl}/follow`, method: 'post', data });
|
||||
|
||||
export const update = data => request({ url: `${baseUrl}/update`, method: 'post', data });
|
||||
|
||||
export const complete = data => request({ url: `${baseUrl}/complete`, method: 'post', data });
|
||||
|
||||
export const batchComplete = ids =>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import request from '@/axios';
|
||||
|
||||
/**
|
||||
* 认证记录列表:按审核状态分页,按提交时间倒序
|
||||
* 审核对象是**一条认证记录**,不是一辆车或一名司机,故必须带 bizType 区分
|
||||
* status 为审核状态(0审核中 1已通过 2已驳回 3已作废);不传时后端按 0 处理
|
||||
*/
|
||||
export const getPendingPage = (bizType, current, size, params) => {
|
||||
return request({
|
||||
url: '/blade-transport/certification-audit/pending-page',
|
||||
method: 'get',
|
||||
params: {
|
||||
...params,
|
||||
bizType,
|
||||
current,
|
||||
size,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 审核详情:同时返回本次待审快照与该对象当前生效快照,供前端做字段级对比
|
||||
* 从未认证通过时 effectiveSnapshotJson 为空串
|
||||
*/
|
||||
export const getAuditDetail = id => {
|
||||
return request({
|
||||
url: '/blade-transport/certification-audit/detail',
|
||||
method: 'get',
|
||||
params: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 审核一条认证记录
|
||||
* 通过时不传驳回原因;驳回时必须传,且原因不超过 200 字
|
||||
*/
|
||||
export const auditCertification = (id, pass, rejectReason) => {
|
||||
return request({
|
||||
url: '/blade-transport/certification-audit/audit',
|
||||
method: 'post',
|
||||
params: {
|
||||
id,
|
||||
pass,
|
||||
...(rejectReason ? { rejectReason } : {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -51,14 +51,6 @@ export const changeStatus = (id, status) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const auditCertification = (id, certificationStatus, rejectReason) => {
|
||||
return request({
|
||||
url: '/blade-transport/transport-vehicle/audit-certification',
|
||||
method: 'post',
|
||||
params: { id, certificationStatus, rejectReason },
|
||||
});
|
||||
};
|
||||
|
||||
export const getExpiryStat = params => {
|
||||
return request({
|
||||
url: '/blade-transport/transport-vehicle/expiry-stat',
|
||||
|
||||
+8
-1
@@ -189,7 +189,14 @@ axios.interceptors.response.use(
|
||||
// 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。
|
||||
if (isJsonContentType(res) && !isPlainObject(res.data)) {
|
||||
try {
|
||||
const text = res.data instanceof Blob ? await res.data.text() : res.data;
|
||||
let text = '';
|
||||
if (typeof res.data?.text === 'function') {
|
||||
text = await res.data.text();
|
||||
} else if (typeof res.data === 'string') {
|
||||
text = res.data;
|
||||
} else if (typeof res.data?.arrayBuffer === 'function') {
|
||||
text = new TextDecoder().decode(await res.data.arrayBuffer());
|
||||
}
|
||||
const parsed = typeof text === 'string' && text ? JSON.parse(text) : null;
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
res.data = parsed;
|
||||
|
||||
@@ -10,17 +10,17 @@
|
||||
<el-button type="primary" :loading="searching" @click="searchKeyword">搜索</el-button>
|
||||
</div>
|
||||
<div class="address-map-picker__content">
|
||||
<map-search-results
|
||||
v-if="searchResults.length"
|
||||
:results="searchResults"
|
||||
@select="selectSearchResult"
|
||||
/>
|
||||
<div ref="map" class="address-map-picker__map"></div>
|
||||
<map-search-results :results="searchResults" @select="selectSearchResult" />
|
||||
</div>
|
||||
<div class="address-map-picker__info">{{ status }}</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selected.longitude || resolving" @click="confirm">
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!hasCoordinateValue(selected.longitude) || !hasCoordinateValue(selected.latitude) || resolving"
|
||||
@click="confirm"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -28,8 +28,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
|
||||
const AMAP_KEY = '13dcb15bc29bfe9c432d17ecd808b812';
|
||||
const AMAP_SECURITY_CODE = 'f9d036ef29e376e15d309e0de5c0f7ab';
|
||||
let amapLoader;
|
||||
|
||||
export default {
|
||||
@@ -42,6 +44,14 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
longitude: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
latitude: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'confirm'],
|
||||
data() {
|
||||
@@ -66,9 +76,11 @@ export default {
|
||||
this.visible = value;
|
||||
if (value) {
|
||||
this.keyword = this.address || '';
|
||||
this.selected = {};
|
||||
this.status = '可搜索地址或点击地图选点';
|
||||
this.searchResults = [];
|
||||
this.selected = this.buildInitialSelection();
|
||||
this.status = this.selected.longitude
|
||||
? this.selected.address || '已选点,可确认回填'
|
||||
: '可搜索地址或点击地图选点';
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -82,6 +94,32 @@ export default {
|
||||
}
|
||||
},
|
||||
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() {
|
||||
if (window.AMap && window.AMap.Map) {
|
||||
return Promise.resolve();
|
||||
@@ -110,15 +148,29 @@ export default {
|
||||
return new Promise(resolve => {
|
||||
this.$nextTick(() => {
|
||||
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) {
|
||||
this.amap = new window.AMap.Map(this.$refs.map, {
|
||||
center: [116.40769, 39.89945],
|
||||
zoom: 11,
|
||||
center,
|
||||
zoom,
|
||||
});
|
||||
this.amap.on('click', event => this.pickPoint(event.lnglat));
|
||||
} else {
|
||||
this.amap.resize();
|
||||
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);
|
||||
});
|
||||
@@ -176,23 +228,17 @@ export default {
|
||||
this.searchResults = [];
|
||||
this.searching = true;
|
||||
this.initMap()
|
||||
.then(map => (map ? this.runGeocode('location', keyword) : null))
|
||||
.then(result => {
|
||||
if (!result) return;
|
||||
const geocodes = result.geocodes || (result.location ? [result] : []);
|
||||
this.searchResults = geocodes.map((item, index) => ({
|
||||
id: item.id || index,
|
||||
name: item.formattedAddress || item.address || keyword,
|
||||
address: item.formattedAddress || item.address || keyword,
|
||||
location: item.location,
|
||||
}));
|
||||
const point = geocodes[0]?.location || result.location;
|
||||
if (!point) {
|
||||
.then(map => (map ? searchAmapPlaceText(keyword) : null))
|
||||
.then(pois => {
|
||||
if (!pois) return;
|
||||
this.searchResults = normalizeMapSearchResults(pois, keyword);
|
||||
const first = this.searchResults[0];
|
||||
if (!first?.location) {
|
||||
throw new Error('未找到匹配地址');
|
||||
}
|
||||
return this.$nextTick().then(() => {
|
||||
this.amap?.resize();
|
||||
this.pickPoint(point, geocodes[0]?.formattedAddress || keyword);
|
||||
this.pickPoint(first.location, first.address || first.name || keyword);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -209,8 +255,10 @@ export default {
|
||||
}
|
||||
},
|
||||
pickPoint(point, fallbackAddress = '') {
|
||||
const longitude = typeof point.getLng === 'function' ? point.getLng() : point.lng;
|
||||
const latitude = typeof point.getLat === 'function' ? point.getLat() : point.lat;
|
||||
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))) {
|
||||
this.$message.warning('选点坐标无效');
|
||||
return;
|
||||
@@ -227,7 +275,12 @@ export default {
|
||||
this.runGeocode('address', lnglat)
|
||||
.then(result => {
|
||||
const detailAddress = result.regeocode?.formattedAddress || fallbackAddress;
|
||||
this.selected = { ...this.selected, address: detailAddress };
|
||||
const region = this.resolveRegionFromGeocode(result);
|
||||
this.selected = {
|
||||
...this.selected,
|
||||
address: detailAddress,
|
||||
...region,
|
||||
};
|
||||
this.keyword = detailAddress || this.keyword;
|
||||
this.status = detailAddress || '已选点,可确认回填';
|
||||
})
|
||||
@@ -241,6 +294,26 @@ export default {
|
||||
this.resolving = false;
|
||||
});
|
||||
},
|
||||
resolveRegionFromGeocode(result = {}) {
|
||||
const component = result.regeocode?.addressComponent || {};
|
||||
const city = Array.isArray(component.city) ? '' : String(component.city || '').trim();
|
||||
const province = String(component.province || '').trim();
|
||||
const district = String(component.district || '').trim();
|
||||
const country = String(component.country || '').trim();
|
||||
const adcode = String(component.adcode || '').trim();
|
||||
return {
|
||||
country,
|
||||
province,
|
||||
city: city || province,
|
||||
district,
|
||||
adcode,
|
||||
regionCode: adcode,
|
||||
provinceName: province,
|
||||
cityName: city || province,
|
||||
districtName: district,
|
||||
countryName: country,
|
||||
};
|
||||
},
|
||||
renderMarker(point) {
|
||||
this.clearMarker();
|
||||
this.marker = new window.AMap.Marker({ position: point });
|
||||
@@ -253,12 +326,53 @@ export default {
|
||||
this.marker = null;
|
||||
}
|
||||
},
|
||||
syncSelectedFromMarker() {
|
||||
// 确认时以当前标记点坐标为准,避免换地址后仍回填上一次经纬度
|
||||
if (!this.marker || typeof this.marker.getPosition !== 'function') {
|
||||
return;
|
||||
}
|
||||
const point = this.marker.getPosition();
|
||||
if (!point) {
|
||||
return;
|
||||
}
|
||||
const longitude =
|
||||
typeof point.getLng === 'function' ? point.getLng() : point.lng ?? point.longitude;
|
||||
const latitude =
|
||||
typeof point.getLat === 'function' ? point.getLat() : point.lat ?? point.latitude;
|
||||
if (!Number.isFinite(Number(longitude)) || !Number.isFinite(Number(latitude))) {
|
||||
return;
|
||||
}
|
||||
this.selected = {
|
||||
...this.selected,
|
||||
longitude: Number(longitude),
|
||||
latitude: Number(latitude),
|
||||
};
|
||||
},
|
||||
confirm() {
|
||||
this.syncSelectedFromMarker();
|
||||
if (!this.hasCoordinateValue(this.selected.longitude) || !this.hasCoordinateValue(this.selected.latitude)) {
|
||||
this.$message.warning('请先搜索或点击地图完成选点');
|
||||
return;
|
||||
}
|
||||
if (!this.selected.address) {
|
||||
this.$message.warning('请等待地址解析完成后再确认');
|
||||
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;
|
||||
},
|
||||
},
|
||||
@@ -278,7 +392,6 @@ export default {
|
||||
}
|
||||
|
||||
&__map {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 420px;
|
||||
@@ -286,9 +399,19 @@ export default {
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
|
||||
.address-map-picker__map {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:deep(.map-search-results) {
|
||||
z-index: 2000;
|
||||
}
|
||||
}
|
||||
|
||||
&__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) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mode-content :deep(.el-form-item:last-child) {
|
||||
|
||||
@@ -1,20 +1,55 @@
|
||||
<template>
|
||||
<div v-if="results.length" class="map-search-results">
|
||||
<div class="map-search-results__title">搜索结果</div>
|
||||
<el-empty v-if="!results.length" description="暂无搜索结果" :image-size="60" />
|
||||
<div
|
||||
v-for="(item, index) in results"
|
||||
:key="item.id || index"
|
||||
class="map-search-results__item"
|
||||
@click="$emit('select', item)"
|
||||
>
|
||||
<div class="map-search-results__name">{{ item.name || item.address || '未命名地址' }}</div>
|
||||
<div class="map-search-results__address">{{ item.address || item.name || '-' }}</div>
|
||||
<div class="map-search-results__list">
|
||||
<div
|
||||
v-for="(item, index) in pageResults"
|
||||
:key="item.id || `${currentPage}-${index}`"
|
||||
class="map-search-results__item"
|
||||
:class="{ 'is-active': isActive(item, index) }"
|
||||
@click="handleSelect(item, index)"
|
||||
>
|
||||
<img
|
||||
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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const PAGE_SIZE = 5;
|
||||
|
||||
export default {
|
||||
props: {
|
||||
results: {
|
||||
@@ -23,43 +58,167 @@ export default {
|
||||
},
|
||||
},
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.map-search-results {
|
||||
width: 260px;
|
||||
height: 420px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #eff1f7;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 360px;
|
||||
max-height: 360px;
|
||||
overflow: hidden;
|
||||
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 {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #eff1f7;
|
||||
font-weight: 600;
|
||||
&__list {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
&__item:hover {
|
||||
background: #f5f9ff;
|
||||
&__item:hover,
|
||||
&__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 {
|
||||
overflow: hidden;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__address {
|
||||
margin-top: 4px;
|
||||
color: #909399;
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="avue-crud__right table-header-tools">
|
||||
<el-button v-if="showRefresh" icon="el-icon-refresh" circle @click="$emit('refresh')" />
|
||||
<el-button
|
||||
v-if="showColumn"
|
||||
icon="el-icon-operation"
|
||||
circle
|
||||
@click="columnSettingVisible = true"
|
||||
/>
|
||||
<el-button
|
||||
v-if="showSearch"
|
||||
icon="el-icon-search"
|
||||
circle
|
||||
@click="$emit('update:searchVisible', !searchVisible)"
|
||||
/>
|
||||
<el-button v-if="showGrid" icon="el-icon-grid" circle @click="toggleTableSize" />
|
||||
|
||||
<el-dialog
|
||||
v-if="showColumn"
|
||||
v-model="columnSettingVisible"
|
||||
title="列设置"
|
||||
width="360px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<el-checkbox-group
|
||||
class="table-header-tools__column-setting"
|
||||
:model-value="visibleColumnKeys"
|
||||
@update:model-value="$emit('update:visibleColumnKeys', $event)"
|
||||
>
|
||||
<el-checkbox v-for="item in columns" :key="item.prop" :label="item.prop">
|
||||
{{ item.label }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<template #footer>
|
||||
<el-button @click="columnSettingVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const TABLE_SIZE_OPTIONS = ['default', 'large', 'small'];
|
||||
|
||||
export default {
|
||||
name: 'TableHeaderTools',
|
||||
props: {
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
visibleColumnKeys: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
tableSize: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
searchVisible: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showRefresh: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showColumn: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showSearch: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showGrid: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['refresh', 'update:visibleColumnKeys', 'update:tableSize', 'update:searchVisible'],
|
||||
data() {
|
||||
return {
|
||||
columnSettingVisible: false,
|
||||
};
|
||||
},
|
||||
deactivated() {
|
||||
// keep-alive 缓存页面时关闭列设置弹窗,避免 Teleport 到 body 的弹窗切页后残留
|
||||
this.columnSettingVisible = false;
|
||||
},
|
||||
methods: {
|
||||
toggleTableSize() {
|
||||
const index = TABLE_SIZE_OPTIONS.indexOf(this.tableSize);
|
||||
const next = TABLE_SIZE_OPTIONS[(index + 1) % TABLE_SIZE_OPTIONS.length];
|
||||
this.$emit('update:tableSize', next);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.table-header-tools.avue-crud__right {
|
||||
width: auto;
|
||||
margin: 0 0 0 auto;
|
||||
flex: none;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.table-header-tools__column-setting {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 360px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -9,18 +9,21 @@
|
||||
<el-table :data="rows" border @selection-change="selectedRows = $event">
|
||||
<el-table-column v-if="!readonly" type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="240" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="240"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span
|
||||
class="vehicle-attachment-table__file-name"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>
|
||||
{{ attachmentName(row) }}
|
||||
</span>
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220">
|
||||
@@ -308,16 +311,6 @@ export default {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__file-name {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__file-name:hover,
|
||||
&__file-name:focus-visible {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&__upload {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
||||
@@ -212,7 +212,14 @@ export default {
|
||||
.filter(Boolean);
|
||||
},
|
||||
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() {
|
||||
return this.tip || `${UNIFIED_ATTACHMENT_TIP_PREFIX}${this.maxSize}M`;
|
||||
@@ -304,12 +311,22 @@ export default {
|
||||
});
|
||||
},
|
||||
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) || '上传失败');
|
||||
return;
|
||||
}
|
||||
this.emitUploadFiles(this.multiple ? files : [file]);
|
||||
this.$emit('success', this.normalizeUploadFile(file));
|
||||
if (file && !file.response) {
|
||||
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('上传成功');
|
||||
},
|
||||
handleChange(file, files) {
|
||||
@@ -323,35 +340,89 @@ export default {
|
||||
this.emitUploadFiles(files);
|
||||
return true;
|
||||
},
|
||||
emitUploadFiles(files) {
|
||||
const list = files
|
||||
.filter(file => file.status === 'success' || this.getFileUrl(file))
|
||||
.map(this.normalizeUploadFile)
|
||||
emitUploadFiles(files, preferredFile) {
|
||||
const list = (files || [])
|
||||
.filter(file => {
|
||||
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);
|
||||
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) {
|
||||
const data = (file.response && file.response.data) || file.data || file;
|
||||
const url = data.link || data.url || data.domain || file.url || '';
|
||||
normalizeUploadFile(file, responseOverride) {
|
||||
const response = responseOverride || file?.response;
|
||||
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 =
|
||||
data.originalName ||
|
||||
file.originalName ||
|
||||
file.name ||
|
||||
file?.originalName ||
|
||||
file?.name ||
|
||||
data.name ||
|
||||
this.getFileName(url) ||
|
||||
'附件';
|
||||
const extension = this.getExtension({ name: originalName, url });
|
||||
return {
|
||||
...data,
|
||||
uid: file.uid || data.uid,
|
||||
uid: file?.uid || data.uid,
|
||||
originalName,
|
||||
name: originalName,
|
||||
url,
|
||||
link: data.link || url,
|
||||
size: data.size || data.attachSize || file.size || '',
|
||||
size: data.size || data.attachSize || file?.size || '',
|
||||
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) {
|
||||
|
||||
@@ -146,6 +146,13 @@ export const createOption = () => ({
|
||||
showWordLimit: true,
|
||||
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
minWidth: 120,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '更新时间',
|
||||
prop: 'updateTime',
|
||||
@@ -298,6 +305,10 @@ export const createOption = () => ({
|
||||
searchOrder: 3,
|
||||
searchPlaceholder: '请选择',
|
||||
dicData: [],
|
||||
props: {
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
},
|
||||
filterable: true,
|
||||
hide: true,
|
||||
display: false,
|
||||
|
||||
@@ -2,6 +2,9 @@ const dimensionOptions = [
|
||||
{ label: '重量', value: '重量' },
|
||||
{ label: '体积', value: '体积' },
|
||||
{ label: '数量', value: '数量' },
|
||||
{ label: '整车', value: '整车' },
|
||||
{ label: '里程', value: '里程' },
|
||||
{ label: '吨·公里', value: '吨·公里' },
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
@@ -42,6 +45,7 @@ export const createOption = () => ({
|
||||
label: '计量单位编码',
|
||||
prop: 'unitCode',
|
||||
minWidth: 150,
|
||||
span: 24,
|
||||
search: true,
|
||||
searchOrder: 4,
|
||||
searchSpan: 6,
|
||||
|
||||
+143
-139
@@ -35,26 +35,7 @@ export const formatCargoValue = value => {
|
||||
};
|
||||
|
||||
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: '货物名称',
|
||||
prop: 'cargoName',
|
||||
@@ -79,12 +60,24 @@ export const option = createCrudOption([
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '一级货物类型编码',
|
||||
prop: 'firstCargoTypeCode',
|
||||
hide: true,
|
||||
display: false,
|
||||
addDisplay: false,
|
||||
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: '二级货物类型编码',
|
||||
@@ -92,9 +85,132 @@ export const option = createCrudOption([
|
||||
display: false,
|
||||
addDisplay: false,
|
||||
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: '货物名称',
|
||||
prop: 'searchCargoName',
|
||||
@@ -186,118 +302,6 @@ export const option = createCrudOption([
|
||||
editDisplay: 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,
|
||||
},
|
||||
...auditColumns
|
||||
.filter(column => column.prop !== 'createUserName')
|
||||
.map(column =>
|
||||
['createTime', 'updateTime'].includes(column.prop) ? { ...column, minWidth: 190 } : column
|
||||
)
|
||||
.sort(
|
||||
(first, second) =>
|
||||
['updateUserName', 'updateTime', 'createTime'].indexOf(first.prop) -
|
||||
['updateUserName', 'updateTime', 'createTime'].indexOf(second.prop)
|
||||
['createUserName', 'updateUserName', 'createTime', 'updateTime'].indexOf(first.prop) -
|
||||
['createUserName', 'updateUserName', 'createTime', 'updateTime'].indexOf(second.prop)
|
||||
),
|
||||
]),
|
||||
dialogCustomClass: 'avue-dialog--common-route',
|
||||
|
||||
@@ -117,6 +117,7 @@ export const contractCategoryOptions = [
|
||||
export const signTypeOptions = [
|
||||
{ label: '新签', value: '新签' },
|
||||
{ label: '续签', value: '续签' },
|
||||
{ label: '补充协议', value: '补充协议' },
|
||||
];
|
||||
|
||||
export const contractStageOptions = [
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
contractStageOptions,
|
||||
createCrudOption,
|
||||
effectiveTypeOptions,
|
||||
nonNegativeRule,
|
||||
selectRule,
|
||||
signTypeOptions,
|
||||
textRule,
|
||||
@@ -61,13 +62,14 @@ export const config = {
|
||||
['contractNo', '合同编号'],
|
||||
['contractName', '合同名称'],
|
||||
['projectName', '所属项目'],
|
||||
['organizationName', '所属组织'],
|
||||
['organizationName', '合同负责部门'],
|
||||
['contractCategory', '合同类别'],
|
||||
['signType', '签约类型'],
|
||||
['partyA', '甲方'],
|
||||
['partyB', '乙方'],
|
||||
['handlerUserName', '经办人'],
|
||||
['signDate', '签订日期'],
|
||||
['applyDate', '申请日期'],
|
||||
['contractFormat', '合同格式'],
|
||||
['legalSealFlag', '是否需要加盖法人章'],
|
||||
['copyCount', '一式(份)'],
|
||||
@@ -76,7 +78,8 @@ export const config = {
|
||||
['templateFlag', '是否范本'],
|
||||
['originalContractNo', '原件合同编号'],
|
||||
['electronicSealFlag', '是否电子章'],
|
||||
['remark', '备注', 2],
|
||||
['mainContent', '合同主要内容', 2],
|
||||
['remark', '其他事项说明', 2],
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -114,7 +117,7 @@ export const config = {
|
||||
operations: [
|
||||
{
|
||||
action: 'flow',
|
||||
label: '查看流程',
|
||||
label: '流程',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing', 'change_reviewing'],
|
||||
stage: ['temporary', 'formal'],
|
||||
@@ -206,10 +209,10 @@ export const config = {
|
||||
},
|
||||
{
|
||||
action: 'flow',
|
||||
label: '查看流程',
|
||||
label: '流程',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['approved', 'change_approved'],
|
||||
stage: ['formal', 'terminated'],
|
||||
stage: ['temporary', 'formal', 'terminated'],
|
||||
permission: 'contract_manage_view',
|
||||
},
|
||||
{
|
||||
@@ -294,17 +297,17 @@ export const option = createCrudOption([
|
||||
display: false,
|
||||
},
|
||||
{
|
||||
label: '所属组织',
|
||||
label: '合同负责部门',
|
||||
prop: 'organizationName',
|
||||
search: true,
|
||||
searchOrder: 3,
|
||||
searchslot: true,
|
||||
searchPlaceholder: '请选择 所属组织',
|
||||
searchPlaceholder: '请选择 合同负责部门',
|
||||
formslot: true,
|
||||
order: 120,
|
||||
dicData: [],
|
||||
minWidth: 150,
|
||||
rules: selectRule('所属组织'),
|
||||
rules: selectRule('合同负责部门'),
|
||||
},
|
||||
{
|
||||
label: '合同类别',
|
||||
@@ -435,6 +438,17 @@ export const option = createCrudOption([
|
||||
minWidth: 130,
|
||||
rules: [{ required: true, message: '请选择签订日期', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '申请日期',
|
||||
prop: 'applyDate',
|
||||
type: 'date',
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'YYYY-MM-DD',
|
||||
sortable: true,
|
||||
minWidth: 130,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '结算类型',
|
||||
prop: 'settlementMode',
|
||||
@@ -468,11 +482,10 @@ export const option = createCrudOption([
|
||||
{ label: '否', value: 0 },
|
||||
{ label: '是', value: 1 },
|
||||
],
|
||||
value: 0,
|
||||
clearable: true,
|
||||
hide: true,
|
||||
order: 70,
|
||||
minWidth: 100,
|
||||
rules: selectRule('是否需要加盖法人章'),
|
||||
},
|
||||
{
|
||||
label: '一式(份)',
|
||||
@@ -500,6 +513,10 @@ export const option = createCrudOption([
|
||||
hide: true,
|
||||
order: 45,
|
||||
minWidth: 130,
|
||||
rules: [
|
||||
{ required: true, message: '请输入合同金额', trigger: 'blur' },
|
||||
nonNegativeRule('合同金额'),
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '是否范本',
|
||||
@@ -532,6 +549,13 @@ export const option = createCrudOption([
|
||||
order: 30,
|
||||
minWidth: 110,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
minWidth: 120,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
@@ -610,7 +634,19 @@ export const option = createCrudOption([
|
||||
minWidth: 110,
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
label: '合同主要内容',
|
||||
prop: 'mainContent',
|
||||
type: 'textarea',
|
||||
span: 24,
|
||||
minRows: 4,
|
||||
hide: true,
|
||||
order: 41,
|
||||
maxlength: 1000,
|
||||
showWordLimit: true,
|
||||
rules: textRule('合同主要内容', 1000),
|
||||
},
|
||||
{
|
||||
label: '其他事项说明',
|
||||
prop: 'remark',
|
||||
type: 'textarea',
|
||||
span: 24,
|
||||
@@ -619,7 +655,7 @@ export const option = createCrudOption([
|
||||
order: 40,
|
||||
maxlength: 2000,
|
||||
showWordLimit: true,
|
||||
rules: textRule('备注', 2000),
|
||||
rules: textRule('其他事项说明', 2000),
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
@@ -730,7 +766,7 @@ export const option = createCrudOption([
|
||||
labelWidth: 0,
|
||||
},
|
||||
...auditColumns
|
||||
.filter(column => column.prop !== 'createTime')
|
||||
.filter(column => !['createTime', 'createUserName'].includes(column.prop))
|
||||
.map(column => ({ ...column, hide: true })),
|
||||
]);
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export const loadingManageConfig = {
|
||||
exportColumns: [
|
||||
{ prop: 'loadingNo', label: '配载单号' },
|
||||
{ prop: 'loadingSubNos', label: '配载子单号' },
|
||||
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' },
|
||||
{ prop: 'vehicleNo', label: '车牌号/船班号/船次号/班次号' },
|
||||
{ prop: 'driverName', label: '司机' },
|
||||
{ prop: 'driverPhone', label: '联系电话' },
|
||||
{ prop: 'carrierType', label: '承运类型' },
|
||||
@@ -41,6 +41,7 @@ export const loadingManageConfig = {
|
||||
{ prop: 'carrierName', label: '承运商' },
|
||||
{ prop: 'transportType', label: '运输方式' },
|
||||
{ prop: 'dataSource', label: '数据来源' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
{ prop: 'updateTime', label: '更新时间' },
|
||||
{ prop: 'currentProcessNode', label: '过程节点' },
|
||||
|
||||
@@ -112,12 +112,12 @@ export const option = {
|
||||
editDisplay: false,
|
||||
},
|
||||
...auditColumns
|
||||
.filter(column => ['updateTime', 'createTime'].includes(column.prop))
|
||||
.filter(column => ['createUserName', 'createTime', 'updateTime'].includes(column.prop))
|
||||
.map(column => ({ ...column, minWidth: 190 }))
|
||||
.sort(
|
||||
(first, second) =>
|
||||
['updateTime', 'createTime'].indexOf(first.prop) -
|
||||
['updateTime', 'createTime'].indexOf(second.prop)
|
||||
['createUserName', 'createTime', 'updateTime'].indexOf(first.prop) -
|
||||
['createUserName', 'createTime', 'updateTime'].indexOf(second.prop)
|
||||
),
|
||||
{
|
||||
label: '项目ID集合',
|
||||
|
||||
@@ -57,16 +57,24 @@ export const config = {
|
||||
action: 'submitApproval',
|
||||
label: '提交',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['draft'],
|
||||
status: ['draft', 'rejected', 'withdrawn'],
|
||||
permission: 'project_apply_submit',
|
||||
},
|
||||
{
|
||||
action: 'withdraw',
|
||||
label: '撤回',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing'],
|
||||
status: ['draft', 'reviewing'],
|
||||
permission: 'project_apply_withdraw',
|
||||
},
|
||||
{
|
||||
action: 'mkAudit',
|
||||
label: '流程',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing', 'change_reviewing', 'approved', 'change_approved'],
|
||||
permission: 'project_apply_approve',
|
||||
mkOnly: true,
|
||||
},
|
||||
{
|
||||
action: 'approve',
|
||||
label: '通过',
|
||||
@@ -74,6 +82,7 @@ export const config = {
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing', 'change_reviewing'],
|
||||
permission: 'project_apply_approve',
|
||||
localOnly: true,
|
||||
},
|
||||
{
|
||||
action: 'reject',
|
||||
@@ -82,6 +91,7 @@ export const config = {
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing', 'change_reviewing'],
|
||||
permission: 'project_apply_reject',
|
||||
localOnly: true,
|
||||
},
|
||||
{
|
||||
action: 'startChange',
|
||||
@@ -442,5 +452,9 @@ export const option = createCrudOption([
|
||||
span: 24,
|
||||
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,
|
||||
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',
|
||||
menuWidth: 320,
|
||||
index: false,
|
||||
|
||||
@@ -249,9 +249,10 @@ export const config = {
|
||||
title: '其它信息',
|
||||
fields: [
|
||||
['remark', '备注'],
|
||||
['createUserName', '创建人'],
|
||||
['createTime', '创建时间'],
|
||||
['updateUserName', '更新人'],
|
||||
['updateTime', '更新时间'],
|
||||
['createTime', '创建时间'],
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -399,7 +400,6 @@ export const option = {
|
||||
span: 6,
|
||||
order: 350,
|
||||
minWidth: 150,
|
||||
rules: [{ required: true, message: '请选择计划开始日期', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '计划结束日期',
|
||||
@@ -410,7 +410,6 @@ export const option = {
|
||||
span: 6,
|
||||
order: 340,
|
||||
minWidth: 150,
|
||||
rules: [{ required: true, message: '请选择计划结束日期', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '数据来源',
|
||||
@@ -457,6 +456,9 @@ export const option = {
|
||||
showWordLimit: true,
|
||||
rules: textRule('备注', 200),
|
||||
},
|
||||
{
|
||||
...auditColumn('createUserName'),
|
||||
},
|
||||
{
|
||||
...auditColumn('updateUserName'),
|
||||
},
|
||||
|
||||
@@ -98,6 +98,24 @@ const getProcessConfigNodes = row => {
|
||||
|
||||
const isAcceptProcessNode = node => node.key === 'accept' || node.name === '接单';
|
||||
|
||||
/**
|
||||
* 过程节点列显示文案。
|
||||
*
|
||||
* 该列在库里存的是「原始值」,且历史上写入了两种格式:司机打卡推进写的是配置的
|
||||
* key(unload / dispatch / load …),配载单新建默认值写的是中文名(接单)。
|
||||
* 故这里按两种格式都认一次,认不出就原样显示,不把值吞掉。
|
||||
*
|
||||
* 运单创建后、司机首次打卡前该字段为空,属正常——显示「-」,避免留白看不出是没值还是没渲染。
|
||||
*/
|
||||
const formatCurrentProcessNode = row => {
|
||||
const value = String(getFirstValue(row, ['currentProcessNode', 'currentNode']) || '').trim();
|
||||
if (!value) return '-';
|
||||
const node = getProcessConfigNodes(row).find(
|
||||
item => item.key === value || item.name === value
|
||||
);
|
||||
return node?.name || node?.key || value;
|
||||
};
|
||||
|
||||
const isTruthyFlag = value => {
|
||||
if (value === true || value === 1) return true;
|
||||
if (value === false || value === 0 || value === null || value === undefined || value === '') {
|
||||
@@ -543,7 +561,8 @@ export const option = {
|
||||
prop: 'projectName',
|
||||
formslot: true,
|
||||
search: true,
|
||||
searchLabel: '项目',
|
||||
searchLabel: '项目名称',
|
||||
searchPlaceholder: '请选择或输入',
|
||||
searchOrder: 22,
|
||||
span: 6,
|
||||
order: 890,
|
||||
@@ -555,6 +574,7 @@ export const option = {
|
||||
prop: 'customerName',
|
||||
search: true,
|
||||
searchLabel: '客户名称',
|
||||
searchPlaceholder: '请选择或输入',
|
||||
searchOrder: 21,
|
||||
minWidth: 150,
|
||||
addDisplay: false,
|
||||
@@ -563,10 +583,10 @@ export const option = {
|
||||
display: false,
|
||||
},
|
||||
{
|
||||
label: '车牌号/航班号/船号/班列号',
|
||||
label: '车牌号/船班号/船次号/班次号',
|
||||
prop: 'vehicleNo',
|
||||
search: true,
|
||||
searchLabel: '车/船/航班/班列',
|
||||
searchLabel: '车牌号/船班号/船次号/班次号',
|
||||
searchOrder: 13,
|
||||
minWidth: 210,
|
||||
display: false,
|
||||
@@ -576,6 +596,7 @@ export const option = {
|
||||
prop: 'driverName',
|
||||
search: true,
|
||||
searchLabel: '司机名称',
|
||||
searchPlaceholder: '请选择或输入',
|
||||
searchOrder: 14,
|
||||
minWidth: 120,
|
||||
display: false,
|
||||
@@ -605,6 +626,7 @@ export const option = {
|
||||
{
|
||||
label: '过程节点',
|
||||
prop: 'currentProcessNode',
|
||||
formatter: row => formatCurrentProcessNode(row),
|
||||
minWidth: 140,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
@@ -626,6 +648,7 @@ export const option = {
|
||||
prop: 'carrierName',
|
||||
search: true,
|
||||
searchLabel: '承运商名称',
|
||||
searchPlaceholder: '请选择或输入',
|
||||
searchOrder: 15,
|
||||
minWidth: 150,
|
||||
display: false,
|
||||
@@ -729,15 +752,30 @@ export const option = {
|
||||
prop: 'planName',
|
||||
formslot: true,
|
||||
search: true,
|
||||
searchPlaceholder: '请选择或输入',
|
||||
searchOrder: 7,
|
||||
span: 6,
|
||||
order: 870,
|
||||
minWidth: 150,
|
||||
formatter: row => {
|
||||
const value = row?.planName;
|
||||
if (
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
String(value) === '-1' ||
|
||||
Number(value) === -1
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '货物类型',
|
||||
prop: 'cargoType',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择或输入',
|
||||
searchOrder: 18,
|
||||
formatter: row => formatGoodsField(row, ['cargoType', 'goodsType', 'typeName']),
|
||||
minWidth: 130,
|
||||
@@ -864,6 +902,9 @@ export const option = {
|
||||
viewDisplay: false,
|
||||
display: false,
|
||||
},
|
||||
{
|
||||
...auditColumn('createUserName'),
|
||||
},
|
||||
{
|
||||
...auditColumn('createTime'),
|
||||
},
|
||||
|
||||
@@ -1,20 +1,165 @@
|
||||
export const paymentApplicationTableColumns = [
|
||||
{ prop: 'paymentNo', label: '单据号', minWidth: 140, link: true },
|
||||
{ prop: 'paymentTypeName', label: '付款类型', minWidth: 110 },
|
||||
{ prop: 'projectName', label: '所属项目', minWidth: 140 },
|
||||
{ prop: 'deptName', label: '所属组织', minWidth: 120 },
|
||||
{ prop: 'contractNo', label: '合同编号', minWidth: 130 },
|
||||
{ prop: 'contractName', label: '合同名称', minWidth: 140 },
|
||||
{ prop: 'payerName', label: '付款方', minWidth: 130 },
|
||||
{ prop: 'payeeName', label: '收款方', minWidth: 130 },
|
||||
{ prop: 'appliedAmount', label: '申请付款金额', minWidth: 140, money: true },
|
||||
{ prop: 'paidAmount', label: '已付款金额', minWidth: 130, money: true },
|
||||
{ prop: 'matchedInvoiceAmount', label: '匹配发票金额', minWidth: 130, money: true },
|
||||
{ prop: 'applicantName', label: '申请人', minWidth: 100 },
|
||||
{ prop: 'applyDate', label: '申请日期', minWidth: 120 },
|
||||
{ prop: 'approvalStatusName', label: '审核状态', minWidth: 110, status: true },
|
||||
{ prop: 'currentNode', label: '当前节点', minWidth: 120 },
|
||||
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 120 },
|
||||
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 130 },
|
||||
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 130 },
|
||||
];
|
||||
/**
|
||||
* 付款申请列表(avue-crud)配置
|
||||
* ------------------------------------------------------------------
|
||||
* 列表页与其它模块统一使用 avue-crud,头部 4 个按钮(刷新 / 列显隐 / 搜索显隐 /
|
||||
* 表格卡片切换)由 avue-crud 自带,不做二次封装。
|
||||
*
|
||||
* 搜索项与表格列 label 不一致的(如表格列「收款方」/ 搜索项「客户名称」),
|
||||
* 单独定义一个 hide 的搜索列,并用 searchProp 映射到后端字段。
|
||||
*
|
||||
* ⚠️ avue 的 searchOrder 为降序排序,值越大越靠前(见 header-search2.js:
|
||||
* `list.sort((a, b) => (b.searchOrder || 0) - (a.searchOrder || 0))`)。
|
||||
*/
|
||||
|
||||
const textFormatter = (row, value) =>
|
||||
value === null || value === undefined || value === '' ? '-' : value;
|
||||
|
||||
const moneyFormatter = (row, value) => Number(value || 0).toFixed(2);
|
||||
|
||||
// 仅用于搜索、不在表格中出现的列
|
||||
const searchOnlyColumn = ({ label, prop, searchOrder, searchProp, dicData }) => {
|
||||
const column = {
|
||||
label,
|
||||
prop,
|
||||
search: true,
|
||||
searchOrder,
|
||||
searchPlaceholder: '请输入',
|
||||
hide: true,
|
||||
showColumn: false,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
};
|
||||
if (searchProp) column.searchProp = searchProp;
|
||||
if (dicData) {
|
||||
column.type = 'select';
|
||||
column.searchPlaceholder = '请选择';
|
||||
column.dicData = dicData;
|
||||
}
|
||||
return column;
|
||||
};
|
||||
|
||||
export const paymentApplicationOption = ({
|
||||
paymentTypeOptions = [],
|
||||
approvalStatusOptions = [],
|
||||
kingdeeStatusOptions = [],
|
||||
} = {}) => ({
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
tip: false,
|
||||
searchBtnText: '查询',
|
||||
emptyBtnText: '重置',
|
||||
searchShow: true,
|
||||
searchIcon: true,
|
||||
searchLabelWidth: 88,
|
||||
searchMenuSpan: 24,
|
||||
searchIndex: 4,
|
||||
searchMenuPosition: 'right',
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 70,
|
||||
indexFixed: 'left',
|
||||
selection: true,
|
||||
selectionWidth: 52,
|
||||
selectionFixed: 'left',
|
||||
addBtn: false,
|
||||
viewBtn: false,
|
||||
delBtn: false,
|
||||
editBtn: false,
|
||||
copyBtn: false,
|
||||
menuWidth: 320,
|
||||
menuFixed: 'right',
|
||||
dialogClickModal: false,
|
||||
column: [
|
||||
// ---------- 搜索项(前 4 项默认展示,其余折叠;searchOrder 越大越靠前) ----------
|
||||
{
|
||||
label: '单据号',
|
||||
prop: 'paymentNo',
|
||||
slot: true,
|
||||
search: true,
|
||||
searchOrder: 9,
|
||||
searchPlaceholder: '请输入',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
label: '日期',
|
||||
prop: 'applyDateRange',
|
||||
type: 'daterange',
|
||||
searchType: 'daterange',
|
||||
search: true,
|
||||
searchOrder: 8,
|
||||
hide: true,
|
||||
showColumn: false,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
searchOnlyColumn({
|
||||
label: '客户名称',
|
||||
prop: 'payeeNameSearch',
|
||||
searchOrder: 7,
|
||||
searchProp: 'payeeName',
|
||||
}),
|
||||
searchOnlyColumn({
|
||||
label: '项目名称',
|
||||
prop: 'projectNameSearch',
|
||||
searchOrder: 6,
|
||||
searchProp: 'projectName',
|
||||
}),
|
||||
searchOnlyColumn({
|
||||
label: '关联结算单',
|
||||
prop: 'settlementNo',
|
||||
searchOrder: 4,
|
||||
}),
|
||||
searchOnlyColumn({
|
||||
label: '付款类型',
|
||||
prop: 'paymentType',
|
||||
searchOrder: 3,
|
||||
dicData: paymentTypeOptions,
|
||||
}),
|
||||
searchOnlyColumn({
|
||||
label: '审核状态',
|
||||
prop: 'approvalStatus',
|
||||
searchOrder: 2,
|
||||
dicData: approvalStatusOptions,
|
||||
}),
|
||||
searchOnlyColumn({
|
||||
label: '金蝶单据状态',
|
||||
prop: 'kingdeeStatus',
|
||||
searchOrder: 1,
|
||||
dicData: kingdeeStatusOptions,
|
||||
}),
|
||||
// ---------- 表格列 ----------
|
||||
{ label: '付款类型', prop: 'paymentTypeName', minWidth: 110, formatter: textFormatter },
|
||||
{ label: '所属项目', prop: 'projectName', minWidth: 140, formatter: textFormatter },
|
||||
{
|
||||
label: '所属组织',
|
||||
prop: 'deptName',
|
||||
search: true,
|
||||
searchOrder: 5,
|
||||
searchslot: true,
|
||||
minWidth: 120,
|
||||
formatter: textFormatter,
|
||||
},
|
||||
{ label: '合同编号', prop: 'contractNo', minWidth: 130, formatter: textFormatter },
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 140, formatter: textFormatter },
|
||||
{ label: '付款方', prop: 'payerName', minWidth: 130, formatter: textFormatter },
|
||||
{ label: '收款方', prop: 'payeeName', minWidth: 130, formatter: textFormatter },
|
||||
{ label: '申请付款金额', prop: 'appliedAmount', minWidth: 140, formatter: moneyFormatter },
|
||||
{ label: '已付款金额', prop: 'paidAmount', minWidth: 130, formatter: moneyFormatter },
|
||||
{
|
||||
label: '匹配发票金额',
|
||||
prop: 'matchedInvoiceAmount',
|
||||
minWidth: 130,
|
||||
formatter: moneyFormatter,
|
||||
},
|
||||
{ label: '申请人', prop: 'applicantName', minWidth: 100, formatter: textFormatter },
|
||||
{ label: '申请日期', prop: 'applyDate', minWidth: 120, formatter: textFormatter },
|
||||
{ label: '审核状态', prop: 'approvalStatusName', slot: true, minWidth: 110 },
|
||||
{ label: '当前节点', prop: 'currentNode', minWidth: 120, formatter: textFormatter },
|
||||
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 120, formatter: textFormatter },
|
||||
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 130, formatter: textFormatter },
|
||||
{ label: '金蝶单据状态', prop: 'kingdeeStatusName', minWidth: 130, formatter: textFormatter },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ export const generateWaybillColumns = [
|
||||
{ label: '运单号', prop: 'waybillNo', minWidth: 160, link: true },
|
||||
{ label: '项目名称', prop: 'projectName', minWidth: 160 },
|
||||
{ label: '客户/委托方', prop: 'customerName', minWidth: 160 },
|
||||
{ label: '车牌号/航班号/船号/班列号', prop: 'vehicleNo', minWidth: 190 },
|
||||
{ label: '车牌号/船班号/船次号/班次号', prop: 'vehicleNo', minWidth: 190 },
|
||||
{ label: '司机', prop: 'driverName', minWidth: 120 },
|
||||
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
|
||||
{ label: '运输类型', prop: 'transportType', minWidth: 130 },
|
||||
|
||||
@@ -5,6 +5,7 @@ export const GRANT_TYPE_DIC = [
|
||||
{ label: '社交登录', value: 'social' },
|
||||
{ label: '客户端凭证', value: 'client_credentials' },
|
||||
{ label: '刷新令牌', value: 'refresh_token' },
|
||||
{ label: '退出登录', value: 'logout' },
|
||||
];
|
||||
|
||||
export const authLogOption = {
|
||||
@@ -38,7 +39,7 @@ export const authLogOption = {
|
||||
prop: 'realName',
|
||||
},
|
||||
{
|
||||
label: '授权类型',
|
||||
label: '操作类型',
|
||||
prop: 'grantType',
|
||||
type: 'select',
|
||||
search: true,
|
||||
@@ -80,7 +81,7 @@ export const authLogOption = {
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
label: '登录时间',
|
||||
label: '操作时间',
|
||||
prop: 'loginTime',
|
||||
sortable: true,
|
||||
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 { validateLoginPassword } from '@/utils/validate';
|
||||
|
||||
export const userOption = safe => {
|
||||
const validatePass = (rule, value, callback) => {
|
||||
if (value === '') {
|
||||
callback(new Error('请输入密码'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
validateLoginPassword(rule, value, callback);
|
||||
};
|
||||
const validatePass2 = (rule, value, callback) => {
|
||||
if (value === '') {
|
||||
@@ -30,7 +27,8 @@ export const userOption = safe => {
|
||||
index: true,
|
||||
selection: true,
|
||||
addBtn: false,
|
||||
viewBtn: true,
|
||||
viewBtn: false,
|
||||
menuWidth: 160,
|
||||
dialogType: 'dialog',
|
||||
dialogWidth: '90%',
|
||||
labelPosition: 'right',
|
||||
@@ -40,8 +38,11 @@ export const userOption = safe => {
|
||||
{
|
||||
label: '账号名',
|
||||
prop: 'account',
|
||||
slot: true,
|
||||
minWidth: 180,
|
||||
search: true,
|
||||
searchOrder: 2,
|
||||
searchPlaceholder: '支持模糊搜索',
|
||||
display: false,
|
||||
},
|
||||
{
|
||||
@@ -49,6 +50,7 @@ export const userOption = safe => {
|
||||
prop: 'realName',
|
||||
search: true,
|
||||
searchOrder: 4,
|
||||
searchPlaceholder: '支持模糊搜索',
|
||||
display: false,
|
||||
},
|
||||
{
|
||||
@@ -56,6 +58,7 @@ export const userOption = safe => {
|
||||
prop: 'phone',
|
||||
search: true,
|
||||
searchOrder: 3,
|
||||
searchPlaceholder: '支持模糊搜索',
|
||||
display: false,
|
||||
},
|
||||
{ label: '工号', prop: 'code', display: false },
|
||||
@@ -63,7 +66,8 @@ export const userOption = safe => {
|
||||
label: '所属部门',
|
||||
prop: 'deptName',
|
||||
slot: true,
|
||||
overHidden: true,
|
||||
minWidth: 200,
|
||||
overHidden: false,
|
||||
display: false,
|
||||
},
|
||||
{ label: '岗位', prop: 'postName', overHidden: true, display: false },
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// 一级 value 与二级 value 都用中文原文:库里 exception_type 存的是大类中文名、
|
||||
// exception_reason 存子类中文名(小程序上报即如此),此处必须与之一致,
|
||||
// 否则按异常类型做精确匹配的查询会一行都查不到。
|
||||
export const exceptionCategoryOptions = [
|
||||
{
|
||||
label: '交通异常',
|
||||
value: 'traffic',
|
||||
value: '交通异常',
|
||||
children: [
|
||||
{ label: '道路拥堵', value: '道路拥堵' },
|
||||
{ label: '交通事故', value: '交通事故' },
|
||||
@@ -12,7 +15,7 @@ export const exceptionCategoryOptions = [
|
||||
},
|
||||
{
|
||||
label: '车辆异常',
|
||||
value: 'vehicle',
|
||||
value: '车辆异常',
|
||||
children: [
|
||||
{ label: '车辆故障维修', value: '车辆故障维修' },
|
||||
{ label: '车辆事故', value: '车辆事故' },
|
||||
@@ -24,7 +27,7 @@ export const exceptionCategoryOptions = [
|
||||
},
|
||||
{
|
||||
label: '货物异常',
|
||||
value: 'cargo',
|
||||
value: '货物异常',
|
||||
children: [
|
||||
{ label: '货损', value: '货损' },
|
||||
{ label: '货差', value: '货差' },
|
||||
@@ -35,7 +38,7 @@ export const exceptionCategoryOptions = [
|
||||
},
|
||||
{
|
||||
label: '其他异常',
|
||||
value: 'other',
|
||||
value: '其他异常',
|
||||
children: [
|
||||
{ label: '变更路线地址', value: '变更路线地址' },
|
||||
{ label: '其他', value: '其他异常-其他' },
|
||||
@@ -43,6 +46,16 @@ export const exceptionCategoryOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
/** 一级类型的中文名列表,供编辑表单的级联选择使用 */
|
||||
export const exceptionTypeOptions = exceptionCategoryOptions.map(item => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
}));
|
||||
|
||||
/** 按一级类型取二级选项 */
|
||||
export const exceptionReasonOptions = typeValue =>
|
||||
exceptionCategoryOptions.find(item => item.value === typeValue)?.children || [];
|
||||
|
||||
export const disposalStatusOptions = [
|
||||
{ label: '待处理', value: 'pending' },
|
||||
{ label: '处理中', value: 'processing' },
|
||||
@@ -65,20 +78,30 @@ export const tableColumns = [
|
||||
{ label: '项目', prop: 'projectName', minWidth: 160 },
|
||||
{ label: '车辆', prop: 'vehicleNo', minWidth: 150, link: true },
|
||||
{ label: '上报人', prop: 'reporterName', minWidth: 120 },
|
||||
{ label: '上报时间', prop: 'reportTime', minWidth: 170 },
|
||||
{ label: '异常类型', prop: 'exceptionTypeName', minWidth: 160 },
|
||||
{ label: '上报说明', prop: 'reportDescription', minWidth: 180 },
|
||||
{ label: '异常节点', prop: 'nodeName', minWidth: 120 },
|
||||
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
|
||||
{ label: '最新跟进说明', prop: 'latestFollowContent', minWidth: 200 },
|
||||
{ label: '状态', prop: 'disposalStatusName', minWidth: 120 },
|
||||
];
|
||||
|
||||
export const detailFields = [
|
||||
{ label: '配载单号/总单号', prop: 'loadingOrMasterNo', link: true },
|
||||
{ label: '运单号', prop: 'waybillNo', link: true },
|
||||
{ label: '项目', prop: 'projectName', link: true },
|
||||
{ label: '车牌号', prop: 'vehicleNo', link: true },
|
||||
{ label: '上报人', prop: 'reporterName' },
|
||||
// 详情/跟进弹窗的只读信息,按来源分两块:
|
||||
// 「运单信息」是上报时从运单复制的字段,「异常信息」是司机上报的内容。
|
||||
// 与编辑弹窗的分区口径一致,改动时两处要一起改。
|
||||
export const detailWaybillFields = [
|
||||
{ label: '运单号', prop: 'waybillNo' },
|
||||
{ label: '配载单号/总单号', prop: 'loadingOrMasterNo' },
|
||||
{ label: '车牌号', prop: 'vehicleNo' },
|
||||
{ label: '项目', prop: 'projectName' },
|
||||
{ label: '承运商', prop: 'carrierName' },
|
||||
];
|
||||
|
||||
export const detailExceptionFields = [
|
||||
{ label: '上报人', prop: 'reporterName' },
|
||||
{ label: '上报时间', prop: 'reportTime' },
|
||||
{ label: '异常节点', prop: 'nodeName' },
|
||||
{ label: '异常类型', prop: 'exceptionTypeName' },
|
||||
{ label: '异常原因', prop: 'exceptionReason' },
|
||||
{ label: '上报说明', prop: 'reportDescription', span: 2 },
|
||||
@@ -89,3 +112,21 @@ export const followColumns = [
|
||||
{ label: '跟进人', prop: 'followUserName', minWidth: 130 },
|
||||
{ label: '跟进时间', prop: 'followTime', minWidth: 170 },
|
||||
];
|
||||
|
||||
// 编辑表单只开放上报四件套。车牌、项目、承运商、配载单号由上报时从运单回填,
|
||||
// 允许手改会让记录与运单脱钩;处置状态由跟进与完成流转,不在此处。
|
||||
export const editFormFields = [
|
||||
{ label: '异常类型', prop: 'exceptionType', type: 'select', required: true },
|
||||
{ label: '异常原因', prop: 'exceptionReason', type: 'select' },
|
||||
{ label: '上报说明', prop: 'reportDescription', type: 'textarea', required: true, span: 2 },
|
||||
];
|
||||
|
||||
// 编辑弹窗顶部的运单信息,只读展示,帮助运管员确认改的是哪一单。
|
||||
// 上报人与上报时间属上报行为,放在「异常信息」里,不在此处重复。
|
||||
export const editWaybillFields = [
|
||||
{ label: '运单号', prop: 'waybillNo' },
|
||||
{ label: '配载单号/总单号', prop: 'loadingOrMasterNo' },
|
||||
{ label: '车牌号', prop: 'vehicleNo' },
|
||||
{ label: '项目', prop: 'projectName' },
|
||||
{ label: '承运商', prop: 'carrierName' },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
export const option = {
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
tip: false,
|
||||
searchShow: true,
|
||||
searchMenuSpan: 24,
|
||||
searchIcon: true,
|
||||
searchIndex: 4,
|
||||
searchMenuPosition: 'right',
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 70,
|
||||
selection: false,
|
||||
addBtn: false,
|
||||
editBtn: false,
|
||||
delBtn: false,
|
||||
viewBtn: false,
|
||||
columnBtn: false,
|
||||
menuWidth: 110,
|
||||
column: [
|
||||
{
|
||||
label: '司机姓名',
|
||||
prop: 'driverName',
|
||||
search: true,
|
||||
searchOrder: 3,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '司机手机号',
|
||||
prop: 'driverMobile',
|
||||
search: true,
|
||||
searchOrder: 2,
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
label: '审核状态',
|
||||
prop: 'statusText',
|
||||
slot: true,
|
||||
width: 110,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
label: '提交时间',
|
||||
prop: 'submitTime',
|
||||
slot: true,
|
||||
minWidth: 170,
|
||||
align: 'center',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
export const option = {
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
tip: false,
|
||||
searchShow: true,
|
||||
searchMenuSpan: 24,
|
||||
searchIcon: true,
|
||||
searchIndex: 4,
|
||||
searchMenuPosition: 'right',
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 70,
|
||||
selection: false,
|
||||
addBtn: false,
|
||||
editBtn: false,
|
||||
delBtn: false,
|
||||
viewBtn: false,
|
||||
columnBtn: false,
|
||||
menuWidth: 110,
|
||||
column: [
|
||||
{
|
||||
label: '车牌号',
|
||||
prop: 'bizKey',
|
||||
search: true,
|
||||
searchOrder: 3,
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
label: '司机姓名',
|
||||
prop: 'driverName',
|
||||
search: true,
|
||||
searchOrder: 2,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '司机手机号',
|
||||
prop: 'driverMobile',
|
||||
search: true,
|
||||
searchOrder: 1,
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
label: '审核状态',
|
||||
prop: 'statusText',
|
||||
slot: true,
|
||||
width: 110,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
label: '提交时间',
|
||||
prop: 'submitTime',
|
||||
slot: true,
|
||||
minWidth: 170,
|
||||
align: 'center',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -125,6 +125,23 @@ export const option = {
|
||||
slot: true,
|
||||
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: '准驾车型',
|
||||
prop: 'drivingType',
|
||||
|
||||
@@ -91,6 +91,23 @@ export const option = {
|
||||
minWidth: 140,
|
||||
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: '',
|
||||
prop: 'expireStatus',
|
||||
|
||||
@@ -95,6 +95,8 @@ export const option = {
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
// 这里是「最新一次提交的状态」缓存,不等于该车当前是否已认证:
|
||||
// 已认证由生效快照指针决定,曾审核通过即永久保持,重新提交不会让它退回审核中
|
||||
label: '认证状态',
|
||||
prop: 'certificationStatus',
|
||||
slot: true,
|
||||
@@ -104,9 +106,10 @@ export const option = {
|
||||
type: 'select',
|
||||
dicData: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '认证中', value: 0 },
|
||||
{ label: '已认证', value: 1 },
|
||||
{ label: '审核中', value: 0 },
|
||||
{ label: '已通过', value: 1 },
|
||||
{ label: '已驳回', value: 2 },
|
||||
{ label: '已作废', value: 3 },
|
||||
],
|
||||
minWidth: 110,
|
||||
align: 'center',
|
||||
@@ -151,6 +154,23 @@ export const option = {
|
||||
minWidth: 130,
|
||||
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: '',
|
||||
prop: 'expireStatus',
|
||||
|
||||
@@ -12,6 +12,9 @@ export const option = {
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
dialogWidth: 1200,
|
||||
// 弹窗根节点 class:avue-crud 弹窗默认 append-to-body 到 body,
|
||||
// 页面内 scoped 样式命中不到弹窗内容,故用该 class 在全局样式里做兜底(见 styles/element-ui.scss)
|
||||
dialogCustomClass: 'vehicle-dispatch-dialog',
|
||||
labelPosition: 'right',
|
||||
labelWidth: 'auto',
|
||||
tip: false,
|
||||
@@ -101,6 +104,7 @@ export const option = {
|
||||
minWidth: 120,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '当前节点',
|
||||
@@ -108,6 +112,7 @@ export const option = {
|
||||
minWidth: 120,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '当前处理人',
|
||||
@@ -115,6 +120,7 @@ export const option = {
|
||||
minWidth: 130,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
@@ -122,6 +128,7 @@ export const option = {
|
||||
minWidth: 120,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
@@ -132,6 +139,7 @@ export const option = {
|
||||
minWidth: 170,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
// ========== 新增/编辑表单专用(不进表格)==========
|
||||
{
|
||||
@@ -165,6 +173,7 @@ export const option = {
|
||||
type: 'textarea',
|
||||
hide: true,
|
||||
span: 24,
|
||||
minRows: 2,
|
||||
maxlength: 200,
|
||||
showWordLimit: true,
|
||||
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
|
||||
const validateNonNegative = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
callback();
|
||||
} else if (Number(value) < 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 = [
|
||||
{ label: '车辆', value: '车辆' },
|
||||
{ label: '船舶', value: '船舶' },
|
||||
@@ -135,6 +138,7 @@ export const option = {
|
||||
minWidth: 140,
|
||||
span: 12,
|
||||
placeholder: '请输入',
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -146,6 +150,7 @@ export const option = {
|
||||
minWidth: 140,
|
||||
span: 12,
|
||||
placeholder: '请输入',
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -204,6 +209,14 @@ export const option = {
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
display: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
@@ -281,6 +294,8 @@ export const exportColumns = [
|
||||
{ prop: 'insuranceClaimAmount', label: '保险理赔金额' },
|
||||
{ prop: 'accidentReasonDamage', label: '事故原因及损坏情况' },
|
||||
{ prop: 'remark', label: '备注' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
];
|
||||
|
||||
export const excelOption = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
|
||||
const validateNonNegative = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
callback();
|
||||
} else if (Number(value) < 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 = [
|
||||
{ label: '车辆', value: '车辆' },
|
||||
{ label: '船舶', value: '船舶' },
|
||||
@@ -159,6 +162,7 @@ export const option = {
|
||||
minWidth: 100,
|
||||
span: 12,
|
||||
placeholder: '请输入',
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [
|
||||
{ required: true, message: '请输入费用', trigger: 'blur' },
|
||||
{ validator: validateNonNegative, trigger: 'blur' },
|
||||
@@ -206,6 +210,14 @@ export const option = {
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
display: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
@@ -282,6 +294,8 @@ export const exportColumns = [
|
||||
{ prop: 'fee', label: '费用(元)' },
|
||||
{ prop: 'assessmentUnit', label: '评定(复核)单位' },
|
||||
{ prop: 'remark', label: '备注' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
];
|
||||
|
||||
export const excelOption = {
|
||||
|
||||
@@ -147,6 +147,15 @@ export const option = {
|
||||
viewDisplay: false,
|
||||
minWidth: 170,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
display: false,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
@@ -224,6 +233,8 @@ export const exportColumns = [
|
||||
{ prop: 'specificationModel', label: '规格型号' },
|
||||
{ prop: 'remark', label: '备注' },
|
||||
{ prop: 'originalEquipmentNo', label: '原厂设备号' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
];
|
||||
|
||||
export const excelOption = {
|
||||
|
||||
@@ -172,6 +172,14 @@ export const option = {
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
display: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
@@ -218,6 +226,8 @@ export const exportColumns = [
|
||||
{ prop: 'exitStation', label: '出口站' },
|
||||
{ prop: 'dataSource', label: '数据来源' },
|
||||
{ prop: 'remark', label: '备注' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
];
|
||||
|
||||
export const excelOption = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
|
||||
const validateNonNegative = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
callback();
|
||||
} else if (Number(value) < 0) {
|
||||
callback(new Error('不能小于 0'));
|
||||
@@ -141,6 +141,7 @@ export const option = {
|
||||
formslot: true,
|
||||
append: '元',
|
||||
minWidth: 120,
|
||||
formatter: (row, value) => (value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value),
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -150,6 +151,7 @@ export const option = {
|
||||
formslot: true,
|
||||
append: '元',
|
||||
minWidth: 120,
|
||||
formatter: (row, value) => (value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value),
|
||||
rules: [
|
||||
{ required: true, message: '请输入保费', trigger: 'blur' },
|
||||
{ validator: validateNonNegative, trigger: 'blur' },
|
||||
@@ -159,6 +161,7 @@ export const option = {
|
||||
label: '发票号',
|
||||
prop: 'invoiceNo',
|
||||
minWidth: 150,
|
||||
formatter: (row, value) => (value === undefined || value === null || value === '' || String(value) === '-1' ? '' : value),
|
||||
rules: [{ max: 80, message: '最多 80 个字符', trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -168,6 +171,7 @@ export const option = {
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'YYYY-MM-DD',
|
||||
minWidth: 120,
|
||||
formatter: (row, value) => (value === undefined || value === null || value === '' || String(value) === '-1' ? '' : value),
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
@@ -178,6 +182,14 @@ export const option = {
|
||||
hide: true,
|
||||
rules: [{ max: 200, message: '备注最多 200 个字', trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
display: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
@@ -256,6 +268,8 @@ export const exportColumns = [
|
||||
{ prop: 'invoiceNo', label: '发票号' },
|
||||
{ prop: 'invoiceDate', label: '开票日期' },
|
||||
{ prop: 'remark', label: '备注' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
];
|
||||
|
||||
export const excelOption = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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) => {
|
||||
if (isEmpty(value)) {
|
||||
@@ -12,6 +13,8 @@ const validateNonNegative = (rule, value, callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatEmptySentinel = (row, value) => (isEmpty(value) ? '' : value);
|
||||
|
||||
export const option = {
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
@@ -60,6 +63,7 @@ export const option = {
|
||||
append: 'km',
|
||||
minWidth: 210,
|
||||
slot: true,
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -70,6 +74,7 @@ export const option = {
|
||||
append: 'km',
|
||||
minWidth: 210,
|
||||
slot: true,
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -80,6 +85,7 @@ export const option = {
|
||||
append: 'km',
|
||||
minWidth: 170,
|
||||
slot: true,
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -90,6 +96,7 @@ export const option = {
|
||||
append: 'km',
|
||||
minWidth: 170,
|
||||
slot: true,
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -133,6 +140,14 @@ export const option = {
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
display: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
@@ -208,6 +223,8 @@ export const exportColumns = [
|
||||
{ prop: 'monthlyMileage', label: '本月行驶里程数' },
|
||||
{ prop: 'totalMileage', label: '累计行驶里程数' },
|
||||
{ prop: 'remark', label: '备注' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
];
|
||||
|
||||
export const excelOption = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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) => {
|
||||
if (isEmpty(value)) {
|
||||
@@ -12,6 +13,8 @@ const validateNonNegative = (rule, value, callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatEmptySentinel = (row, value) => (isEmpty(value) ? '' : value);
|
||||
|
||||
export const vehicleTypeDic = [
|
||||
{ label: '车辆', value: '车辆' },
|
||||
{ label: '船舶', value: '船舶' },
|
||||
@@ -124,6 +127,7 @@ export const option = {
|
||||
type: 'input',
|
||||
formslot: true,
|
||||
minWidth: 110,
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -133,6 +137,7 @@ export const option = {
|
||||
formslot: true,
|
||||
append: '元/升',
|
||||
minWidth: 120,
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -141,6 +146,7 @@ export const option = {
|
||||
type: 'input',
|
||||
formslot: true,
|
||||
minWidth: 130,
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [
|
||||
{ required: true, message: '请输入交易金额', trigger: 'blur' },
|
||||
{ validator: validateNonNegative, trigger: 'blur' },
|
||||
@@ -152,6 +158,7 @@ export const option = {
|
||||
type: 'input',
|
||||
formslot: true,
|
||||
minWidth: 110,
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -225,6 +232,14 @@ export const option = {
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
display: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
@@ -304,6 +319,8 @@ export const exportColumns = [
|
||||
{ prop: 'balance', label: '余额' },
|
||||
{ prop: 'station', label: '站点' },
|
||||
{ prop: 'remark', label: '备注' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
];
|
||||
|
||||
export const excelOption = {
|
||||
|
||||
@@ -165,6 +165,14 @@ export const option = {
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
display: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
@@ -209,6 +217,8 @@ export const exportColumns = [
|
||||
{ prop: 'amount', label: '金额' },
|
||||
{ prop: 'dataSource', label: '数据来源' },
|
||||
{ prop: 'remark', label: '备注' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
];
|
||||
|
||||
export const excelOption = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
|
||||
const validateNonNegative = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
callback();
|
||||
} else if (Number(value) < 0) {
|
||||
callback(new Error('不能小于 0'));
|
||||
@@ -11,7 +11,7 @@ const validateNonNegative = (rule, value, callback) => {
|
||||
};
|
||||
|
||||
const validatePositiveInteger = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
callback();
|
||||
} else if (!Number.isInteger(Number(value)) || Number(value) <= 0) {
|
||||
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 = {
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
@@ -98,6 +101,7 @@ export const option = {
|
||||
minWidth: 110,
|
||||
span: 12,
|
||||
placeholder: '请输入',
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validatePositiveInteger, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -108,6 +112,7 @@ export const option = {
|
||||
minWidth: 120,
|
||||
span: 12,
|
||||
placeholder: '请输入',
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [
|
||||
{ required: true, message: '请输入换胎费用', trigger: 'blur' },
|
||||
{ validator: validateNonNegative, trigger: 'blur' },
|
||||
@@ -142,6 +147,14 @@ export const option = {
|
||||
slot: true,
|
||||
formslot: true,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
display: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
@@ -217,6 +230,8 @@ export const exportColumns = [
|
||||
{ prop: 'replacementCost', label: '换胎费用' },
|
||||
{ prop: 'replacementDescription', label: '换胎说明' },
|
||||
{ prop: 'remark', label: '备注' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
];
|
||||
|
||||
export const excelOption = {
|
||||
|
||||
@@ -97,6 +97,14 @@ export const option = {
|
||||
slot: true,
|
||||
formslot: true,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
display: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
@@ -169,6 +177,8 @@ export const exportColumns = [
|
||||
{ prop: 'changeItem', label: '变更事项' },
|
||||
{ prop: 'changeContent', label: '变更内容' },
|
||||
{ prop: 'remark', label: '备注' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
];
|
||||
|
||||
export const excelOption = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
|
||||
const validateNonNegative = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
callback();
|
||||
} else if (Number(value) < 0) {
|
||||
callback(new Error('不能小于 0'));
|
||||
@@ -11,7 +11,7 @@ const validateNonNegative = (rule, value, callback) => {
|
||||
};
|
||||
|
||||
const validateDeductPoints = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
callback();
|
||||
} else if (!Number.isInteger(Number(value)) || Number(value) < 0 || Number(value) > 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 = [
|
||||
{ label: '车辆', value: '车辆' },
|
||||
{ label: '船舶', value: '船舶' },
|
||||
@@ -150,6 +153,7 @@ export const option = {
|
||||
minWidth: 100,
|
||||
span: 12,
|
||||
placeholder: '请输入',
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -160,6 +164,7 @@ export const option = {
|
||||
minWidth: 110,
|
||||
span: 12,
|
||||
placeholder: '请输入',
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateDeductPoints, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -216,6 +221,14 @@ export const option = {
|
||||
slot: true,
|
||||
formslot: true,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
display: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
@@ -294,6 +307,8 @@ export const exportColumns = [
|
||||
{ prop: 'processStatus', label: '状态' },
|
||||
{ prop: 'processDescription', label: '过程描述' },
|
||||
{ prop: 'processResult', label: '处理结果' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
];
|
||||
|
||||
export const excelOption = {
|
||||
|
||||
@@ -13,8 +13,8 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
loginForm: {
|
||||
username: 'admin',
|
||||
password: '123456',
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
-->
|
||||
<userLogin v-if="activeName === 'user'"></userLogin>
|
||||
<registerLogin v-else-if="activeName === 'register'"></registerLogin>
|
||||
<!-- 暂时默认账号密码登录,IAM 选择入口先隐藏
|
||||
<div v-else class="iam-login">
|
||||
<el-button type="primary" class="login-submit" @click.prevent="handleIamLogin">
|
||||
IAM统一身份认证
|
||||
@@ -32,6 +33,7 @@
|
||||
账号密码登录
|
||||
</el-button>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,7 +63,7 @@ export default {
|
||||
return {
|
||||
website: website,
|
||||
time: '',
|
||||
activeName: 'iam',
|
||||
activeName: 'user',
|
||||
socialForm: {
|
||||
tenantId: '000000',
|
||||
source: '',
|
||||
|
||||
@@ -93,9 +93,9 @@ export default {
|
||||
//角色ID
|
||||
roleId: '',
|
||||
//用户名
|
||||
username: 'admin',
|
||||
username: '',
|
||||
//密码
|
||||
password: 'admin',
|
||||
password: '',
|
||||
//账号类型
|
||||
type: 'account',
|
||||
//验证码的值
|
||||
|
||||
+38
-16
@@ -87,7 +87,7 @@ export default [
|
||||
{
|
||||
path: '/business/project-apply/public-view',
|
||||
name: '查看项目信息',
|
||||
component: () => import('@/views/mk/public-biz-view.vue'),
|
||||
component: () => import('@/views/business/project-apply-public-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
isTab: false,
|
||||
@@ -98,7 +98,7 @@ export default [
|
||||
{
|
||||
path: '/business/contract-manage/public-view',
|
||||
name: '查看合同信息',
|
||||
component: () => import('@/views/mk/public-biz-view.vue'),
|
||||
component: () => import('@/views/business/contract-manage-public-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
isTab: false,
|
||||
@@ -106,21 +106,10 @@ export default [
|
||||
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',
|
||||
name: '查看预结算信息',
|
||||
component: () => import('@/views/mk/public-biz-view.vue'),
|
||||
component: () => import('@/views/settlement/pre-settlement-public-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
isTab: false,
|
||||
@@ -131,7 +120,7 @@ export default [
|
||||
{
|
||||
path: '/settlement/formal-settlement/public-view',
|
||||
name: '查看正式结算信息',
|
||||
component: () => import('@/views/mk/public-biz-view.vue'),
|
||||
component: () => import('@/views/settlement/formal-settlement-public-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
isTab: false,
|
||||
@@ -142,7 +131,7 @@ export default [
|
||||
{
|
||||
path: '/payment/payment-application/public-view',
|
||||
name: '查看付款申请信息',
|
||||
component: () => import('@/views/mk/public-biz-view.vue'),
|
||||
component: () => import('@/views/payment/payment-application-public-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
isTab: false,
|
||||
@@ -150,6 +139,39 @@ export default [
|
||||
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: '/',
|
||||
name: '主页',
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { isAdministrator as checkAdministrator } from '@/utils/auth';
|
||||
|
||||
const getters = {
|
||||
tag: state => state.tags.tag,
|
||||
language: state => state.common.language,
|
||||
setting: state => state.common.setting,
|
||||
userInfo: state => state.user.userInfo,
|
||||
// 超级管理员:角色别名中包含 administrator
|
||||
isAdministrator: state => checkAdministrator(state.user.userInfo),
|
||||
colorName: state => state.common.colorName,
|
||||
themeName: state => state.common.themeName,
|
||||
isMacOs: (state, getters) => getters.themeName === 'mac-os',
|
||||
|
||||
+10
-3
@@ -150,12 +150,15 @@ a {
|
||||
bottom: 0;
|
||||
}
|
||||
.map-picker-content {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
|
||||
> [class$='__map'],
|
||||
> .address-map {
|
||||
flex: 1 1 auto;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
@@ -165,4 +168,8 @@ a {
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
> .map-search-results {
|
||||
z-index: 2000;
|
||||
}
|
||||
}
|
||||
|
||||
+103
-5
@@ -173,7 +173,10 @@
|
||||
}
|
||||
|
||||
.el-dialog .el-col {
|
||||
// 取消列自身下边距,改由 form-item 统一控制行距;
|
||||
// flow-root 避免子级 margin 折叠穿透导致换行后行距失效、校验提示被下一行遮挡
|
||||
margin-bottom: 0;
|
||||
display: flow-root;
|
||||
}
|
||||
|
||||
.el-dialog .el-form-item,
|
||||
@@ -181,7 +184,7 @@
|
||||
.el-dialog .el-form-item--small.el-form-item,
|
||||
.el-dialog .el-form-item--large.el-form-item,
|
||||
.el-dialog .el-form-item--medium.el-form-item {
|
||||
//margin-bottom: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* 全站弹窗:底部操作栏浮动效果(参照 business/project-apply)
|
||||
@@ -362,9 +365,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 弹窗内表单项默认上下间距 16px(覆盖默认 18px / 22px,与 customer-archive 对齐)
|
||||
// 4) 弹窗内表单项上下间距:预留绝对定位校验提示(约 14~18px)的可视空间
|
||||
.el-dialog .el-form-item {
|
||||
//margin-bottom: 16px;
|
||||
//margin-bottom: 24px;
|
||||
}
|
||||
|
||||
// 校验提示浮在控件下方,提高层级避免被下一行输入框背景遮挡
|
||||
.el-form-item__error {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
// 5) 通用分组白卡组件(<section-card>):标题融入卡头
|
||||
@@ -446,7 +454,7 @@
|
||||
.dialog-form--carded {
|
||||
.el-form-item {
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
@@ -644,6 +652,28 @@
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
// 车辆调度新增/编辑/查看弹窗:使用部门 el-cascader 默认 display: inline-block、宽度按内容收缩,
|
||||
// 会比同行的 el-input(--el-input-width 默认 100%)窄一大截,需显式拉满列宽。
|
||||
// 两个必须注意的点(踩过):
|
||||
// 1) avue-crud 弹窗默认 append-to-body 到 body,页面内 `.vehicle-dispatch-page :deep(...)`
|
||||
// 这类以页面容器为祖先的选择器匹配不到弹窗内容,必须用弹窗自身 class(dialogCustomClass)在全局兜底;
|
||||
// 2) avue 表单项在 el-form-item__content 与控件之间还有两层包裹 div
|
||||
// (form/index3.js 渲染:content > div > [div|elTooltip] > 控件),
|
||||
// 所以要用后代选择器,不能写 `>` 直接子级。
|
||||
.vehicle-dispatch-dialog .el-form-item__content .el-cascader {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// 车辆调度弹窗 label 宽度统一:option 用 labelWidth: 'auto' 时 label 宽度随字数变化
|
||||
// (3 字 64px / 2 字 40px / 4 字带星号 78px),控件可用宽度随之变化,
|
||||
// 于是同一行两列控件宽度不一致(如车牌号 486px vs 使用部门 472px)。
|
||||
// 本弹窗左右两列最长 label 均为 4 个汉字(所属组织 / 使用部门 / 申请时间),故统一按 4em + 24px 取宽。
|
||||
.vehicle-dispatch-dialog .el-form-item__label {
|
||||
flex: 0 0 calc(4em + 24px) !important;
|
||||
width: calc(4em + 24px) !important;
|
||||
}
|
||||
|
||||
// 过程配置新增/编辑/查看弹窗(手写 el-dialog,参照 vehicle/credit-score-quantification):
|
||||
// 灰底弹窗体(全站 .el-dialog__body 已统一)内堆叠 <section-card> 分组白卡,
|
||||
// 卡间 12px 间距由 section-card 自身 margin-bottom 保证,与项目其他弹窗风格统一
|
||||
@@ -995,6 +1025,66 @@ td.el-table-fixed-column--right {
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
// 全站统一:内容型长文本列(文件名 / 单号 / 备注等)单元格内也放了 el-link / span.attachment-file-name,
|
||||
// 会被上面两条「含 el-link 的单元格」规则一并命中,导致:
|
||||
// ① 上面第 950 行的 display: flex 让链接成为 flex item,flex item 默认 min-width: auto 无法收缩,
|
||||
// 超长文本(如未规范命名的附件名)会直接把列撑破;
|
||||
// ② 豁免规则的 overflow: visible 让溢出内容不再被单元格裁切,直接压到相邻列上。
|
||||
// 因此对「内容列」做显式兜底:单元格自身恢复块级 + 省略号截断,链接参与收缩并显示省略号。
|
||||
// 用法:在 <el-table-column> 上配置 class-name="is-ellipsis-link"(动态列可 :class-name 绑定)。
|
||||
// 操作列不要加此类,否则会失去自动换行能力。
|
||||
.el-table .el-table__body .cell.is-ellipsis-link {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
.el-link {
|
||||
display: inline-block;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
|
||||
// Element Plus 的 el-link__inner 是 inline-flex,flex 容器上的 text-overflow 不生效,
|
||||
// 必须降级为块级容器,省略号才会真正渲染出来。
|
||||
.el-link__inner {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-file-name {
|
||||
display: inline-block;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
// 附件上传表格:文件名统一用 span,视觉与可点击行为对齐原 el-link primary
|
||||
.attachment-file-name {
|
||||
color: var(--el-color-primary, #409eff);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
color: var(--el-text-color-placeholder, #c0c4cc);
|
||||
cursor: not-allowed;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 全站统一:Avue 内置行操作按钮(查看 / 编辑 / 删除 / 复制等)视觉对齐为 el-link 链接样式。
|
||||
// 选择器仅命中表格行操作列 .avue-crud__menu 内的 is-text 按钮,不误伤顶部工具栏(.avue-crud__header)
|
||||
// 与弹窗按钮(.avue-crud__dialog__menu)。事件(rowView/rowEdit/rowDel)与权限(permission 指令)
|
||||
@@ -1073,7 +1163,12 @@ td.el-table-fixed-column--right {
|
||||
align-items: normal;
|
||||
}
|
||||
.el-form-item--default {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
// 独立表单页(非弹窗)同样避免 el-col 折叠吃掉行距
|
||||
.el-form .el-col {
|
||||
display: flow-root;
|
||||
}
|
||||
|
||||
// 配套:星号必须保持「高度 auto + 继承行高」,stretch 才会按首行排布;
|
||||
@@ -1248,3 +1343,6 @@ td.el-table-fixed-column--right {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
.is-ellipsis-link {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
+24
-1
@@ -6,8 +6,31 @@ const RefreshTokenKey = website.refreshTokenKey;
|
||||
const SessionId = 'JSESSIONID';
|
||||
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() {
|
||||
return Cookies.get(TokenKey);
|
||||
return Cookies.get(TokenKey);
|
||||
}
|
||||
|
||||
export function setToken(token) {
|
||||
|
||||
@@ -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 => {
|
||||
const rawValue = String(value ?? '').replace(/[^\d.-]/g, '');
|
||||
const isNegative = rawValue.startsWith('-');
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
const CURRENCY_NAME_MAP = {
|
||||
CNY: '人民币',
|
||||
RMB: '人民币',
|
||||
USD: '美元',
|
||||
EUR: '欧元',
|
||||
JPY: '日元',
|
||||
GBP: '英镑',
|
||||
};
|
||||
|
||||
const CURRENCY_UNIT_MAP = {
|
||||
CNY: '元',
|
||||
RMB: '元',
|
||||
USD: '美元',
|
||||
EUR: '欧元',
|
||||
JPY: '日元',
|
||||
GBP: '英镑',
|
||||
};
|
||||
|
||||
const CURRENCY_NAME_TO_UNIT = {
|
||||
人民币: '元',
|
||||
美元: '美元',
|
||||
欧元: '欧元',
|
||||
日元: '日元',
|
||||
英镑: '英镑',
|
||||
};
|
||||
|
||||
/**
|
||||
* 归一化币种代码(CNY → RMB)
|
||||
*/
|
||||
export function normalizeCurrencyCode(value, fallback = 'RMB') {
|
||||
const raw =
|
||||
typeof value === 'object'
|
||||
? value?.dictKey || value?.value || value?.code || value?.dictValue || ''
|
||||
: value;
|
||||
const normalized = String(raw || fallback)
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
const code = normalized.includes(' - ') ? normalized.split(' - ')[0].trim() : normalized;
|
||||
if (!code) return fallback;
|
||||
return code === 'CNY' ? 'RMB' : code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 币种中文名称
|
||||
*/
|
||||
export function currencyName(value) {
|
||||
const text = String(value || '');
|
||||
if (text.includes(' - ')) return text.slice(text.indexOf(' - ') + 3);
|
||||
return CURRENCY_NAME_MAP[text.toUpperCase()] || text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金额单位文案:RMB/CNY → 元,USD → 美元
|
||||
*/
|
||||
export function currencyUnit(value) {
|
||||
const text = String(value || '');
|
||||
let code = text;
|
||||
if (text.includes(' - ')) {
|
||||
code = text.split(' - ')[0].trim();
|
||||
}
|
||||
const upperCode = code.toUpperCase();
|
||||
if (CURRENCY_UNIT_MAP[upperCode]) {
|
||||
return CURRENCY_UNIT_MAP[upperCode];
|
||||
}
|
||||
if (CURRENCY_NAME_TO_UNIT[code]) {
|
||||
return CURRENCY_NAME_TO_UNIT[code];
|
||||
}
|
||||
return text || '元';
|
||||
}
|
||||
|
||||
/**
|
||||
* 将计价单位前缀替换为当前币种单位,如 元/吨 → 美元/吨
|
||||
*/
|
||||
export function remapPriceUnitCurrency(priceUnit, currency) {
|
||||
const text = String(priceUnit || '').trim();
|
||||
if (!text || !text.includes('/')) return text;
|
||||
const suffix = text.slice(text.indexOf('/'));
|
||||
return `${currencyUnit(currency)}${suffix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按数量单位生成默认计价单位
|
||||
*/
|
||||
export function resolvePriceUnitByQuantityUnit(quantityUnit, currency = 'RMB') {
|
||||
const unit = currencyUnit(currency);
|
||||
return (
|
||||
{
|
||||
吨: `${unit}/吨`,
|
||||
方: `${unit}/立方`,
|
||||
件: `${unit}/件`,
|
||||
}[String(quantityUnit || '').trim()] || ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字典中的计价单位选项映射到当前币种
|
||||
*/
|
||||
export function mapFeeUnitOptions(options = [], currency = 'RMB') {
|
||||
const unit = currencyUnit(currency);
|
||||
return (options || []).map(item => {
|
||||
const label = String(item.label || item.value || '');
|
||||
const value = String(item.value || item.label || '');
|
||||
return {
|
||||
...item,
|
||||
label: label.replace(/^元/, unit).replace(/^人民币/, unit),
|
||||
value: value.replace(/^元/, unit).replace(/^人民币/, unit),
|
||||
};
|
||||
});
|
||||
}
|
||||
+99
-22
@@ -6,7 +6,31 @@ import 'nprogress/nprogress.css';
|
||||
|
||||
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 => {
|
||||
// 已是业务 JSON(导入成功/失败提示)时,绝不能当失败明细 Excel 处理
|
||||
if (isBusinessJson(res?.data)) {
|
||||
return false;
|
||||
}
|
||||
// 只有文件流才可能是失败明细;普通对象没有 .text/.arrayBuffer
|
||||
if (!isBlobLike(res?.data)) {
|
||||
return false;
|
||||
}
|
||||
const headers = res.headers || {};
|
||||
const contentType = String(headers['content-type'] || res.data?.type || '').toLowerCase();
|
||||
const contentDisposition = String(headers['content-disposition'] || '').toLowerCase();
|
||||
@@ -23,8 +47,34 @@ const isExcelResponse = res => {
|
||||
);
|
||||
};
|
||||
|
||||
const parseBlobJson = async blob => {
|
||||
const text = await blob.text();
|
||||
/**
|
||||
* 解析导入接口响应体为 JSON 对象。
|
||||
* <p>
|
||||
* axios 拦截器在 Content-Type 为 JSON 时,会把 `responseType: 'blob'` 的 Blob
|
||||
* 提前解析成普通对象;此处需同时兼容 Blob / 字符串 / 已解析对象,避免再调
|
||||
* `blob.text()` 时出现「blob.text is not a function」。
|
||||
*/
|
||||
const parseBlobJson = async data => {
|
||||
if (data == null) {
|
||||
return {};
|
||||
}
|
||||
if (isBusinessJson(data)) {
|
||||
return data;
|
||||
}
|
||||
if (typeof data === 'object' && !isBlobLike(data) && !(data instanceof ArrayBuffer)) {
|
||||
return data;
|
||||
}
|
||||
if (typeof data === 'string') {
|
||||
return data ? JSON.parse(data) : {};
|
||||
}
|
||||
let text = '';
|
||||
if (typeof data.text === 'function') {
|
||||
text = await data.text();
|
||||
} else if (typeof data.arrayBuffer === 'function') {
|
||||
text = new TextDecoder().decode(await data.arrayBuffer());
|
||||
} else if (data instanceof ArrayBuffer) {
|
||||
text = new TextDecoder().decode(data);
|
||||
}
|
||||
return text ? JSON.parse(text) : {};
|
||||
};
|
||||
|
||||
@@ -39,18 +89,21 @@ const parseBlobJson = async blob => {
|
||||
* 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。
|
||||
*/
|
||||
const resolveErrorMessage = async error => {
|
||||
const blob = error?.response?.data ?? error?.data;
|
||||
if (blob instanceof Blob) {
|
||||
const payload = error?.response?.data ?? error?.data;
|
||||
if (isBusinessJson(payload) && payload.msg) {
|
||||
return payload.msg;
|
||||
}
|
||||
if (isBlobLike(payload) || typeof payload === 'string') {
|
||||
try {
|
||||
const result = await parseBlobJson(blob);
|
||||
const result = await parseBlobJson(payload);
|
||||
if (result?.msg) return result.msg;
|
||||
} catch (ignored) {
|
||||
// 响应体不是 JSON(如网关返回的 HTML 错误页),退回通用文案
|
||||
}
|
||||
}
|
||||
const fallback = error?.response?.data?.msg || error?.message;
|
||||
const fallback = payload?.msg || error?.message;
|
||||
// axios 的英文通用文案对用户没有意义,统一换成中文提示
|
||||
if (!fallback || /Request failed with status code/i.test(fallback)) {
|
||||
if (!fallback || /Request failed with status code|\.text is not a function/i.test(fallback)) {
|
||||
return '导入失败,请检查导入文件后重试';
|
||||
}
|
||||
return fallback;
|
||||
@@ -68,8 +121,11 @@ const resolveErrorMessage = async error => {
|
||||
*/
|
||||
const decorateFailExcel = async (blob, businessName, failDetailDecorator) => {
|
||||
if (typeof failDetailDecorator !== 'function') return blob;
|
||||
if (!isBlobLike(blob) && !(blob instanceof ArrayBuffer)) {
|
||||
return blob;
|
||||
}
|
||||
const ExcelJS = await import('exceljs');
|
||||
const buf = await blob.arrayBuffer();
|
||||
const buf = typeof blob.arrayBuffer === 'function' ? await blob.arrayBuffer() : blob;
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(buf);
|
||||
await failDetailDecorator(workbook, { businessName });
|
||||
@@ -77,6 +133,31 @@ const decorateFailExcel = async (blob, businessName, failDetailDecorator) => {
|
||||
return new Blob([out], { type: 'application/vnd.ms-excel' });
|
||||
};
|
||||
|
||||
/** 通知上传组件成功:只传业务响应体,避免其误调 axios 响应上的 .text() */
|
||||
const notifyUploadSuccess = (option, res) => {
|
||||
if (typeof option?.onSuccess !== 'function') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const body = isBusinessJson(res?.data) ? res.data : { code: 200, success: true };
|
||||
option.onSuccess(body);
|
||||
} catch (callbackError) {
|
||||
console.warn('upload onSuccess callback failed', callbackError);
|
||||
}
|
||||
};
|
||||
|
||||
/** 通知上传组件失败 */
|
||||
const notifyUploadError = (option, error) => {
|
||||
if (typeof option?.onError !== 'function') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
option.onError(error instanceof Error ? error : new Error(String(error?.message || error || '导入失败')));
|
||||
} catch (callbackError) {
|
||||
console.warn('upload onError callback failed', callbackError);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开导入弹窗
|
||||
* @param {Object} vm 组件实例
|
||||
@@ -108,9 +189,7 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
||||
const fileName = file && file.name ? file.name.toLowerCase() : '';
|
||||
if (!excelPattern.test(fileName)) {
|
||||
vm.$message.error('请上传 .xls,.xlsx 标准格式文件');
|
||||
if (typeof option.onError === 'function') {
|
||||
option.onError(new Error('请上传 .xls,.xlsx 标准格式文件'));
|
||||
}
|
||||
notifyUploadError(option, new Error('请上传 .xls,.xlsx 标准格式文件'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -130,12 +209,11 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
||||
);
|
||||
vm.$message.warning('部分数据导入失败,已下载失败明细');
|
||||
} else {
|
||||
const result = await parseBlobJson(res.data);
|
||||
if (result.code !== 200) {
|
||||
// 成功体常被 axios 拦截器提前解析成对象;优先走业务 JSON,避免再调 Blob.text
|
||||
const result = isBusinessJson(res?.data) ? res.data : await parseBlobJson(res?.data);
|
||||
if (Number(result.code) !== 200) {
|
||||
vm.$message.error(result.msg || '导入失败');
|
||||
if (typeof option.onError === 'function') {
|
||||
option.onError(new Error(result.msg || '导入失败'));
|
||||
}
|
||||
notifyUploadError(option, new Error(result.msg || '导入失败'));
|
||||
return;
|
||||
}
|
||||
vm.$message.success('导入完成');
|
||||
@@ -149,18 +227,17 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
||||
} else if (typeof vm.initTree === 'function') {
|
||||
vm.initTree();
|
||||
}
|
||||
if (typeof option.onSuccess === 'function') {
|
||||
option.onSuccess(res);
|
||||
}
|
||||
// 上传组件 onSuccess 约定接收「响应体」,不是完整 axios 响应。
|
||||
// 若传入 axios 响应对象,部分环境下会按 Fetch Response 调用 .text(),
|
||||
// 从而在导入已成功后仍弹出「e.text is not a function」。
|
||||
notifyUploadSuccess(option, res);
|
||||
} catch (error) {
|
||||
// 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次,
|
||||
// 只保留一次清晰的中文提示。
|
||||
if (!error?._notified) {
|
||||
vm.$message.error(await resolveErrorMessage(error));
|
||||
}
|
||||
if (typeof option.onError === 'function') {
|
||||
option.onError(error);
|
||||
}
|
||||
notifyUploadError(option, error);
|
||||
} finally {
|
||||
loading.close();
|
||||
NProgress.done();
|
||||
|
||||
@@ -0,0 +1,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 { processDelete, processSubmit } from '@/api/system/business-process';
|
||||
import { hasMkProcessInstance, processDelete, processSubmit } from '@/api/system/business-process';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
export const MK_BIZ = {
|
||||
@@ -7,43 +7,80 @@ export const MK_BIZ = {
|
||||
dictName: '提交客商审核流',
|
||||
subjectPrefix: '客商准入审批',
|
||||
rejected: ['rejected'],
|
||||
approved: ['approved'],
|
||||
},
|
||||
'project-apply': {
|
||||
dictName: '提交项目审核流',
|
||||
subjectPrefix: '项目审批',
|
||||
rejected: ['rejected', 'change_rejected', 'withdrawn'],
|
||||
approved: ['approved', 'change_approved'],
|
||||
},
|
||||
'contract-manage': {
|
||||
dictName: '提交合同审核流',
|
||||
subjectPrefix: '合同审批',
|
||||
rejected: ['rejected', 'change_rejected', 'withdrawn'],
|
||||
},
|
||||
'waybill-manage': {
|
||||
dictName: '提交运单审核流',
|
||||
subjectPrefix: '运单审批',
|
||||
rejected: ['rejected', 'returned'],
|
||||
approved: ['approved', 'change_approved'],
|
||||
},
|
||||
'pre-settlement': {
|
||||
dictName: '提交预结算审核流',
|
||||
subjectPrefix: '预结算审批',
|
||||
rejected: ['returned'],
|
||||
approved: ['approved'],
|
||||
},
|
||||
'formal-settlement': {
|
||||
dictName: '提交正式结算审核流',
|
||||
subjectPrefix: '正式结算审批',
|
||||
rejected: ['returned'],
|
||||
approved: ['approved'],
|
||||
},
|
||||
'payment-application': {
|
||||
dictName: '提交付款审核流',
|
||||
subjectPrefix: '付款审批',
|
||||
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 list = res?.data?.data || [];
|
||||
const matched = list.find(item => String(item.dictValue || '').trim() === dictName);
|
||||
return res?.data?.data || [];
|
||||
}
|
||||
|
||||
/** 业务字典 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;
|
||||
if (!templateCode) {
|
||||
ElMessage.warning(`未配置业务字典 mk_template「${dictName}」,无法提交审核流`);
|
||||
@@ -52,9 +89,26 @@ export async function resolveMkTemplateCode(dictName) {
|
||||
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({
|
||||
bizType,
|
||||
formInstanceId,
|
||||
bizId,
|
||||
subjectName = '',
|
||||
approvalStatus = '',
|
||||
}) {
|
||||
@@ -62,17 +116,34 @@ export async function submitMkApprovalFlow({
|
||||
if (!conf) {
|
||||
return Promise.reject(new Error(`不支持的MK业务类型:${bizType}`));
|
||||
}
|
||||
if ((conf.rejected || []).includes(approvalStatus)) {
|
||||
await processDelete({ formInstanceId: String(formInstanceId) });
|
||||
if (bizId == null || bizId === '') {
|
||||
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
|
||||
? `${conf.subjectPrefix}:${subjectName}`
|
||||
: `${conf.subjectPrefix}:${formInstanceId}`;
|
||||
: `${conf.subjectPrefix}:${bizIdText}`;
|
||||
return processSubmit({
|
||||
templateCode,
|
||||
formInstanceId: String(formInstanceId),
|
||||
bizId: bizIdText,
|
||||
subject,
|
||||
bizType,
|
||||
forceNewFormInstance,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -283,3 +283,31 @@ export function validatejson(val) {
|
||||
// 非对象、非数组、非字符串,或者字符串不是 JSON
|
||||
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"
|
||||
icon="el-icon-delete"
|
||||
plain
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
@click="handleDelete"
|
||||
>删除
|
||||
</el-button>
|
||||
@@ -43,19 +43,19 @@
|
||||
<template #menu="{ row }">
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="row.status === -1 && userInfo.authority.includes('admin')"
|
||||
v-if="row.status === -1 && isAdministrator"
|
||||
@click.stop="handleEnable(row)"
|
||||
>启用
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="row.status === 1 && userInfo.authority.includes('admin')"
|
||||
v-if="row.status === 1 && isAdministrator"
|
||||
@click.stop="handleDisable(row)"
|
||||
>禁用
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
@click.stop="handleRowLog(row)"
|
||||
>日志
|
||||
</el-link>
|
||||
@@ -522,7 +522,7 @@ sortable: true, span: 24, width: 180 },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo']),
|
||||
...mapGetters(['userInfo', 'isAdministrator']),
|
||||
ids() {
|
||||
let ids = [];
|
||||
this.selectionList.forEach(ele => {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
type="danger"
|
||||
icon="el-icon-delete"
|
||||
plain
|
||||
v-if="userInfo.authority.includes('administrator')"
|
||||
v-if="isAdministrator"
|
||||
@click="handleDelete"
|
||||
>删除
|
||||
</el-button>
|
||||
@@ -54,19 +54,19 @@
|
||||
<template #menu="{ row }">
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="row.status !== 1 && userInfo.authority.includes('administrator')"
|
||||
v-if="row.status !== 1 && isAdministrator"
|
||||
@click.stop="handleEnable(row)"
|
||||
>启用
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="row.status === 1 && userInfo.authority.includes('administrator')"
|
||||
v-if="row.status === 1 && isAdministrator"
|
||||
@click.stop="handleDisable(row)"
|
||||
>禁用
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="userInfo.authority.includes('administrator')"
|
||||
v-if="isAdministrator"
|
||||
@click.stop="handleRowLog(row)"
|
||||
>日志
|
||||
</el-link>
|
||||
@@ -401,7 +401,7 @@ sortable: true, span: 24, width: 170 },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo']),
|
||||
...mapGetters(['userInfo', 'isAdministrator']),
|
||||
ids() {
|
||||
let ids = [];
|
||||
this.selectionList.forEach(ele => {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<el-button
|
||||
icon="el-icon-setting"
|
||||
@click="handleRole"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
plain
|
||||
>权限设置
|
||||
</el-button>
|
||||
@@ -39,7 +39,7 @@
|
||||
<template #menu="{ row }">
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
style="border: 0; background-color: transparent !important"
|
||||
@click.stop="handleRowRole(row)"
|
||||
>权限设置
|
||||
@@ -354,7 +354,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo', 'permission']),
|
||||
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: this.validData(this.permission.role_add, false),
|
||||
|
||||
@@ -94,6 +94,17 @@
|
||||
/>
|
||||
</el-select>
|
||||
</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>
|
||||
<el-input
|
||||
v-model="form.longitude"
|
||||
@@ -121,19 +132,27 @@
|
||||
</template>
|
||||
</avue-form>
|
||||
</el-dialog>
|
||||
<address-map-picker
|
||||
v-model="addressMapPickerVisible"
|
||||
:address="form.detailAddress"
|
||||
:longitude="form.longitude"
|
||||
:latitude="form.latitude"
|
||||
@confirm="handleAddressMapConfirm"
|
||||
/>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/airport-master';
|
||||
import { getLazyTree } from '@/api/base/region';
|
||||
import { getDetail as getRegionDetail, getLazyTree } from '@/api/base/region';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
|
||||
@@ -181,6 +200,9 @@ const addExcelRequiredHeaderMarks = async blob => {
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddressMapPicker,
|
||||
},
|
||||
data() {
|
||||
const validateIataCode = (rule, value, callback) => {
|
||||
if (!/^[A-Z]{3}$/.test(String(value || '').toUpperCase())) {
|
||||
@@ -220,6 +242,7 @@ export default {
|
||||
loading: true,
|
||||
data: [],
|
||||
excelBox: false,
|
||||
addressMapPickerVisible: false,
|
||||
excelForm: {},
|
||||
provinceOptions: [],
|
||||
cityOptions: [],
|
||||
@@ -299,6 +322,20 @@ export default {
|
||||
prop: 'shortName',
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
label: '详细地址',
|
||||
prop: 'detailAddress',
|
||||
formslot: true,
|
||||
span: 24,
|
||||
minWidth: 220,
|
||||
overHidden: false,
|
||||
maxlength: 255,
|
||||
showWordLimit: true,
|
||||
rules: [
|
||||
{ required: true, message: '请选择详细地址', trigger: 'change' },
|
||||
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '所属省份',
|
||||
prop: 'provinceCode',
|
||||
@@ -365,21 +402,6 @@ export default {
|
||||
addDisplay: 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: '经度',
|
||||
prop: 'longitude',
|
||||
@@ -438,6 +460,13 @@ export default {
|
||||
showWordLimit: true,
|
||||
rules: [{ max: 200, message: '备注不能超过200字', trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '更新人',
|
||||
prop: 'updateUserName',
|
||||
@@ -526,10 +555,7 @@ export default {
|
||||
};
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo && this.userInfo.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
ids() {
|
||||
let ids = [];
|
||||
@@ -552,33 +578,51 @@ export default {
|
||||
return targetColumns.find(item => item.prop === targetProp) || null;
|
||||
},
|
||||
loadProvinceOptions() {
|
||||
getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
|
||||
return getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
|
||||
this.provinceOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||
this.findColumn('provinceCode').dicData = this.provinceOptions;
|
||||
const provinceColumn = this.findColumn('provinceCode');
|
||||
if (provinceColumn) {
|
||||
provinceColumn.dicData = this.provinceOptions;
|
||||
}
|
||||
return this.provinceOptions;
|
||||
});
|
||||
},
|
||||
loadCityOptions(provinceCode) {
|
||||
if (!provinceCode) {
|
||||
this.cityOptions = [];
|
||||
this.findColumn('cityCode').dicData = [];
|
||||
return Promise.resolve();
|
||||
const cityColumn = this.findColumn('cityCode');
|
||||
if (cityColumn) {
|
||||
cityColumn.dicData = [];
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return getLazyTree(provinceCode).then(res => {
|
||||
this.cityOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||
this.findColumn('cityCode').dicData = this.cityOptions;
|
||||
const cityColumn = this.findColumn('cityCode');
|
||||
if (cityColumn) {
|
||||
cityColumn.dicData = this.cityOptions;
|
||||
}
|
||||
return this.cityOptions;
|
||||
});
|
||||
},
|
||||
loadDistrictOptions(cityCode) {
|
||||
if (!cityCode) {
|
||||
this.districtOptions = [];
|
||||
this.findColumn('districtCode').dicData = [];
|
||||
return Promise.resolve();
|
||||
const districtColumn = this.findColumn('districtCode');
|
||||
if (districtColumn) {
|
||||
districtColumn.dicData = [];
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
this.districtLoading = true;
|
||||
return getLazyTree(cityCode)
|
||||
.then(res => {
|
||||
this.districtOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||
this.findColumn('districtCode').dicData = this.districtOptions;
|
||||
const districtColumn = this.findColumn('districtCode');
|
||||
if (districtColumn) {
|
||||
districtColumn.dicData = this.districtOptions;
|
||||
}
|
||||
return this.districtOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.districtLoading = false;
|
||||
@@ -605,6 +649,22 @@ export default {
|
||||
: String(row.districtCode),
|
||||
};
|
||||
},
|
||||
ensureCityOption(row) {
|
||||
if (!row.cityCode || !row.cityName) return;
|
||||
const exists = this.cityOptions.some(item => item.id === row.cityCode);
|
||||
if (exists) return;
|
||||
this.cityOptions = [
|
||||
...this.cityOptions,
|
||||
{
|
||||
id: row.cityCode,
|
||||
title: row.cityName,
|
||||
},
|
||||
];
|
||||
const cityColumn = this.findColumn('cityCode');
|
||||
if (cityColumn) {
|
||||
cityColumn.dicData = this.cityOptions;
|
||||
}
|
||||
},
|
||||
ensureDistrictOption(row) {
|
||||
if (!row.districtCode || !row.districtName) return;
|
||||
const exists = this.districtOptions.some(item => item.id === row.districtCode);
|
||||
@@ -616,7 +676,10 @@ export default {
|
||||
title: row.districtName,
|
||||
},
|
||||
];
|
||||
this.findColumn('districtCode').dicData = this.districtOptions;
|
||||
const districtColumn = this.findColumn('districtCode');
|
||||
if (districtColumn) {
|
||||
districtColumn.dicData = this.districtOptions;
|
||||
}
|
||||
},
|
||||
handleIataChange(value) {
|
||||
this.form.iataCode = String(value || '')
|
||||
@@ -669,6 +732,269 @@ export default {
|
||||
handleCoordinateInput(prop, 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) {
|
||||
row.code = row.iataCode ? `JC-${row.iataCode}` : row.code;
|
||||
row.regionCode = String(row.regionCode || '').trim();
|
||||
@@ -917,4 +1243,13 @@ export default {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-address-input {
|
||||
cursor: pointer;
|
||||
|
||||
:deep(.el-input__wrapper),
|
||||
:deep(.el-input__inner) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -182,8 +182,8 @@
|
||||
<el-input
|
||||
v-model="form.regionName"
|
||||
readonly
|
||||
disabled
|
||||
placeholder="行政区划自动带出"
|
||||
:disabled="dialogReadonly"
|
||||
/>
|
||||
<el-input
|
||||
v-model="form.detailAddress"
|
||||
@@ -233,8 +233,8 @@
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="map-picker-content">
|
||||
<map-search-results :results="mapSearchResults" @select="selectMapSearchResult" />
|
||||
<div ref="amap" class="common-address-page__map"></div>
|
||||
<map-search-results :results="mapSearchResults" @select="selectMapSearchResult" />
|
||||
</div>
|
||||
<div class="common-address-page__map-info">
|
||||
<span>{{ mapStatus }}</span>
|
||||
@@ -269,7 +269,8 @@ import { exportBlob } from '@/api/common';
|
||||
import { createOption } from '@/option/base/common-address';
|
||||
import { mapGetters } from 'vuex';
|
||||
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 NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
@@ -323,16 +324,16 @@ export default {
|
||||
};
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo && this.userInfo.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
ids() {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.isAdmin) {
|
||||
this.allDept = 1;
|
||||
}
|
||||
this.initDeptTree();
|
||||
},
|
||||
methods: {
|
||||
@@ -343,15 +344,21 @@ export default {
|
||||
getDeptTree(this.userInfo.tenantId).then(res => {
|
||||
this.deptOptions = this.flattenDept(res.data.data || []);
|
||||
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
|
||||
deptColumn.dicData = this.deptOptions;
|
||||
if (deptColumn) {
|
||||
deptColumn.dicData = this.deptOptions;
|
||||
}
|
||||
});
|
||||
},
|
||||
flattenDept(tree, level = 0) {
|
||||
const result = [];
|
||||
tree.forEach(item => {
|
||||
// 雪花 ID 必须保持字符串,避免 JS Number 精度丢失导致筛错组织。
|
||||
const id =
|
||||
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
|
||||
if (!id) return;
|
||||
result.push({
|
||||
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
||||
value: item.id,
|
||||
value: id,
|
||||
rawLabel: item.title || item.deptName || item.name,
|
||||
});
|
||||
if (item.children && item.children.length) {
|
||||
@@ -360,6 +367,19 @@ export default {
|
||||
});
|
||||
return result;
|
||||
},
|
||||
normalizeListQuery(params = {}) {
|
||||
// 搜索条件优先于缓存的 query,避免旧组织 ID 覆盖用户新选择的组织。
|
||||
const query = { ...this.query, ...params };
|
||||
const rawDeptId = query.deptId ?? query.searchDeptId;
|
||||
delete query.searchDeptId;
|
||||
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
|
||||
query.deptId = String(rawDeptId).split(',')[0].trim();
|
||||
} else {
|
||||
delete query.deptId;
|
||||
}
|
||||
query.allDept = this.isAdmin ? 1 : 0;
|
||||
return query;
|
||||
},
|
||||
getRecords(res) {
|
||||
const data = res && res.data && res.data.data;
|
||||
if (Array.isArray(data)) return data;
|
||||
@@ -966,13 +986,15 @@ export default {
|
||||
},
|
||||
searchReset() {
|
||||
this.query = {};
|
||||
this.allDept = this.isAdmin ? 1 : 0;
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
searchChange(params, done) {
|
||||
this.query = params;
|
||||
this.query = this.normalizeListQuery(params);
|
||||
this.allDept = this.query.allDept;
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page, params);
|
||||
this.onLoad(this.page, this.query);
|
||||
done();
|
||||
},
|
||||
selectionChange(list) {
|
||||
@@ -993,7 +1015,10 @@ export default {
|
||||
},
|
||||
onLoad(page, params = {}) {
|
||||
this.loading = true;
|
||||
getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: this.allDept })
|
||||
const query = this.normalizeListQuery(params);
|
||||
this.query = query;
|
||||
this.allDept = query.allDept;
|
||||
getList(page.currentPage, page.pageSize, query)
|
||||
.then(res => {
|
||||
const data = res.data.data;
|
||||
this.page.total = data.total;
|
||||
@@ -1026,8 +1051,7 @@ export default {
|
||||
},
|
||||
buildExportParams() {
|
||||
return {
|
||||
...this.query,
|
||||
allDept: this.allDept,
|
||||
...this.normalizeListQuery(this.query),
|
||||
ids: this.ids,
|
||||
[this.website.tokenHeader]: getToken(),
|
||||
};
|
||||
@@ -1111,36 +1135,32 @@ export default {
|
||||
this.$message.warning('请输入地址关键词');
|
||||
return;
|
||||
}
|
||||
if (this.mapLoading) return;
|
||||
this.mapSearchResults = [];
|
||||
this.mapLoading = true;
|
||||
this.loadAmap()
|
||||
.then(() => {
|
||||
this.mapLoading = true;
|
||||
this.ensureAmapGeocoder()
|
||||
.then(() => this.runAmapGeocode('location', keyword))
|
||||
.then(result => {
|
||||
this.mapSearchResults = (result.geocodes || []).map((item, index) => ({
|
||||
id: item.id || index,
|
||||
name: item.formattedAddress || keyword,
|
||||
address: item.formattedAddress || keyword,
|
||||
location: item.location,
|
||||
}));
|
||||
const point = this.resolveMapPoint(result);
|
||||
if (!point) {
|
||||
this.mapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
return;
|
||||
}
|
||||
this.pickMapPoint(point, keyword);
|
||||
})
|
||||
.catch(() => {
|
||||
this.mapStatus = '地图搜索失败';
|
||||
this.$message.error('地图搜索失败,请稍后重试');
|
||||
})
|
||||
.finally(() => {
|
||||
this.mapLoading = false;
|
||||
});
|
||||
.then(() => searchAmapPlaceText(keyword))
|
||||
.then(pois => {
|
||||
this.mapSearchResults = normalizeMapSearchResults(pois, keyword);
|
||||
const first = this.mapSearchResults[0];
|
||||
if (!first?.location) {
|
||||
this.mapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
return;
|
||||
}
|
||||
return this.$nextTick().then(() => {
|
||||
if (typeof this.amap?.resize === 'function') {
|
||||
this.amap.resize();
|
||||
}
|
||||
this.pickMapPoint(first.location, first.address || first.name || keyword);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
|
||||
this.mapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
})
|
||||
.finally(() => {
|
||||
this.mapLoading = false;
|
||||
});
|
||||
},
|
||||
selectMapSearchResult(item) {
|
||||
|
||||
@@ -238,6 +238,13 @@ export default {
|
||||
value: 'value',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
minWidth: 120,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '更新人',
|
||||
prop: 'updateUserName',
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getDetail, getList, remove, submit } from '@/api/base/insurance-ocr-template';
|
||||
import SectionCard from '@/components/section-card/main.vue';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
const insuranceFieldKeys = [
|
||||
'保险类型',
|
||||
@@ -203,7 +204,7 @@ export default {
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
return String(this.userInfo.authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
selectionIds() {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { changeStatus, getDetail, getList, remove, submit } from '@/api/base/measurement-unit';
|
||||
import { createOption } from '@/option/base/measurement-unit';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -111,7 +112,7 @@ export default {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
},
|
||||
isAdmin() {
|
||||
return String(this.userInfo?.authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -194,10 +194,11 @@
|
||||
<el-input
|
||||
v-model="form.detailAddress"
|
||||
class="detail-address-input"
|
||||
clearable
|
||||
readonly
|
||||
maxlength="255"
|
||||
show-word-limit
|
||||
placeholder="请输入"
|
||||
placeholder="点击选择地图地址"
|
||||
@click="openAddressMapPicker"
|
||||
/>
|
||||
</template>
|
||||
<template #remark-form>
|
||||
@@ -244,6 +245,13 @@
|
||||
</template>
|
||||
</avue-form>
|
||||
</el-dialog>
|
||||
<address-map-picker
|
||||
v-model="addressMapPickerVisible"
|
||||
:address="form.detailAddress"
|
||||
:longitude="form.longitude"
|
||||
:latitude="form.latitude"
|
||||
@confirm="handleAddressMapConfirm"
|
||||
/>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
@@ -256,20 +264,24 @@ import {
|
||||
changeStatus,
|
||||
getPortSelect,
|
||||
} from '@/api/base/port-terminal';
|
||||
import { getLazyTree } from '@/api/base/region';
|
||||
import { getDetail as getRegionDetail, getLazyTree } from '@/api/base/region';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
|
||||
const DEFAULT_COUNTRY_CODE = '+86';
|
||||
const newLocal = '请选择类型';
|
||||
export default {
|
||||
components: {
|
||||
AddressMapPicker,
|
||||
},
|
||||
data() {
|
||||
const validateCode = (rule, value, callback) => {
|
||||
const code = String(value || '').toUpperCase();
|
||||
@@ -315,6 +327,7 @@ export default {
|
||||
loading: true,
|
||||
data: [],
|
||||
excelBox: false,
|
||||
addressMapPickerVisible: false,
|
||||
excelForm: {},
|
||||
portOptions: [],
|
||||
countryOptions: [],
|
||||
@@ -571,12 +584,12 @@ export default {
|
||||
span: 12,
|
||||
minWidth: 220,
|
||||
overHidden: false,
|
||||
order: 85,
|
||||
placeholder: '请输入',
|
||||
order: 90,
|
||||
placeholder: '点击选择地图地址',
|
||||
maxlength: 255,
|
||||
showWordLimit: true,
|
||||
rules: [
|
||||
{ required: true, message: '请输入详细地址', trigger: 'blur' },
|
||||
{ required: true, message: '请选择详细地址', trigger: 'change' },
|
||||
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
|
||||
],
|
||||
},
|
||||
@@ -608,6 +621,13 @@ export default {
|
||||
showWordLimit: true,
|
||||
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '更新人',
|
||||
prop: 'updateUserName',
|
||||
@@ -695,10 +715,7 @@ export default {
|
||||
};
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo && this.userInfo.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
isParentRegionLocked() {
|
||||
return this.form.category === '码头';
|
||||
@@ -824,10 +841,60 @@ export default {
|
||||
},
|
||||
handleCountryNameChange(value) {
|
||||
const country = this.countryOptions.find(item => item.title === value);
|
||||
this.setProvinceOptions([]);
|
||||
this.setCityOptions([]);
|
||||
this.setDistrictOptions([]);
|
||||
this.loadProvinceOptions(country ? country.id : '');
|
||||
this.clearSearchCityAndDistrict();
|
||||
// 搜索栏为国家 → 城市 → 区县(无省份),选中国家后需直接加载该国下全部城市
|
||||
this.loadCityOptionsByCountry(country ? country.id : '');
|
||||
},
|
||||
clearSearchCityAndDistrict() {
|
||||
const searchForm = this.$refs.crud && this.$refs.crud.searchForm;
|
||||
if (searchForm) {
|
||||
searchForm.city = undefined;
|
||||
searchForm.districtName = undefined;
|
||||
}
|
||||
if (this.query) {
|
||||
this.query.city = undefined;
|
||||
this.query.districtName = undefined;
|
||||
}
|
||||
},
|
||||
loadCityOptionsByCountry(countryCode) {
|
||||
if (!countryCode) {
|
||||
this.setCityOptions([]);
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
this.cityLoading = true;
|
||||
return this.loadProvinceOptions(countryCode)
|
||||
.then(provinces =>
|
||||
Promise.all((provinces || []).map(province => this.fetchCityOptions(province.id)))
|
||||
)
|
||||
.then(cityGroups => {
|
||||
const cityMap = new Map();
|
||||
cityGroups.flat().forEach(city => {
|
||||
if (city && city.id && !cityMap.has(city.id)) {
|
||||
cityMap.set(city.id, city);
|
||||
}
|
||||
});
|
||||
const cities = Array.from(cityMap.values());
|
||||
this.setCityOptions(cities);
|
||||
return cities;
|
||||
})
|
||||
.finally(() => {
|
||||
this.cityLoading = false;
|
||||
});
|
||||
},
|
||||
fetchCityOptions(provinceCode) {
|
||||
if (!provinceCode) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
if (this.cityOptionCache[provinceCode]) {
|
||||
return Promise.resolve(this.cityOptionCache[provinceCode]);
|
||||
}
|
||||
return getLazyTree(provinceCode).then(res => {
|
||||
const options = (res.data.data || []).map(this.normalizeRegionOption);
|
||||
this.cityOptionCache[provinceCode] = options;
|
||||
return options;
|
||||
});
|
||||
},
|
||||
syncCountryName(countryCode) {
|
||||
const country = this.countryOptions.find(item => item.id === countryCode);
|
||||
@@ -908,10 +975,8 @@ export default {
|
||||
return Promise.resolve(this.cityOptionCache[provinceCode]);
|
||||
}
|
||||
this.cityLoading = true;
|
||||
return getLazyTree(provinceCode)
|
||||
.then(res => {
|
||||
const options = (res.data.data || []).map(this.normalizeRegionOption);
|
||||
this.cityOptionCache[provinceCode] = options;
|
||||
return this.fetchCityOptions(provinceCode)
|
||||
.then(options => {
|
||||
this.setCityOptions(options);
|
||||
return options;
|
||||
})
|
||||
@@ -1030,6 +1095,13 @@ export default {
|
||||
handleCityNameChange(value) {
|
||||
const city = this.cityOptions.find(item => item.title === value);
|
||||
this.setDistrictOptions([]);
|
||||
const searchForm = this.$refs.crud && this.$refs.crud.searchForm;
|
||||
if (searchForm) {
|
||||
searchForm.districtName = undefined;
|
||||
}
|
||||
if (this.query) {
|
||||
this.query.districtName = undefined;
|
||||
}
|
||||
this.loadDistrictOptions(city ? city.id : '');
|
||||
},
|
||||
handleDistrictChange(value) {
|
||||
@@ -1040,6 +1112,228 @@ export default {
|
||||
handleCoordinateInput(prop, 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) {
|
||||
if (!country) {
|
||||
return Promise.resolve('');
|
||||
@@ -1107,7 +1401,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
if (isBlank(row.detailAddress)) {
|
||||
this.$message.warning('请输入详细地址');
|
||||
this.$message.warning('请选择详细地址');
|
||||
return false;
|
||||
}
|
||||
if (isBlank(row.longitude)) {
|
||||
@@ -1219,13 +1513,50 @@ export default {
|
||||
this.$message({ type: 'success', message: '操作成功!' });
|
||||
});
|
||||
},
|
||||
createEmptyPortTerminalForm() {
|
||||
return {
|
||||
category: '港口',
|
||||
dataSource: '手动录入',
|
||||
status: 1,
|
||||
parentId: undefined,
|
||||
parentCode: '',
|
||||
parentName: '',
|
||||
name: '',
|
||||
code: '',
|
||||
terminalCode: '',
|
||||
country: '',
|
||||
countryCode: '',
|
||||
provinceCode: undefined,
|
||||
provinceName: '',
|
||||
cityCode: undefined,
|
||||
city: '',
|
||||
districtCode: undefined,
|
||||
districtName: '',
|
||||
regionCode: '',
|
||||
detailAddress: '',
|
||||
longitude: '',
|
||||
latitude: '',
|
||||
remark: '',
|
||||
};
|
||||
},
|
||||
resetAddForm() {
|
||||
// 新增必须整表重置,避免沿用上一次编辑/查看残留
|
||||
this.form = this.createEmptyPortTerminalForm();
|
||||
this.setProvinceOptions([]);
|
||||
this.setCityOptions([]);
|
||||
this.setDistrictOptions([]);
|
||||
},
|
||||
beforeOpen(done, type) {
|
||||
if (type === 'add') {
|
||||
this.form.category = this.form.category || '港口';
|
||||
this.form.dataSource = this.form.dataSource || '手动录入';
|
||||
this.form.status = this.form.status || 1;
|
||||
this.resetAddForm();
|
||||
this.handleCategoryChange('港口', false);
|
||||
done();
|
||||
this.$nextTick(() => {
|
||||
this.$refs.crud?.clearValidate?.();
|
||||
setTimeout(() => this.disableDialogAutocomplete(), 50);
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.handleCategoryChange(this.form.category || '港口');
|
||||
if (['edit', 'view'].includes(type)) {
|
||||
getDetail(this.form.id).then(res => {
|
||||
this.form = res.data.data;
|
||||
@@ -1260,35 +1591,7 @@ export default {
|
||||
setTimeout(() => this.disableDialogAutocomplete(), 50);
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.resolveCountryCode(this.form.country)
|
||||
.then(countryCode => {
|
||||
this.form.countryCode = countryCode;
|
||||
this.syncCountryName(countryCode);
|
||||
return this.loadProvinceOptions(countryCode);
|
||||
})
|
||||
.then(() => {
|
||||
this.form.provinceCode =
|
||||
String(this.form.provinceCode || '') ||
|
||||
this.resolveProvinceCode(this.form.provinceName);
|
||||
return this.loadCityOptions(this.form.provinceCode);
|
||||
})
|
||||
.then(() => {
|
||||
this.form.cityCode = this.resolveCityCode(this.form.city, this.form.regionCode);
|
||||
return this.loadDistrictOptions(this.form.cityCode);
|
||||
})
|
||||
.then(() => {
|
||||
this.form.districtCode = this.resolveDistrictCode(
|
||||
this.form.districtName,
|
||||
this.form.regionCode
|
||||
);
|
||||
if (!this.form.regionCode) {
|
||||
this.handleDistrictChange(this.form.districtCode);
|
||||
}
|
||||
done();
|
||||
setTimeout(() => this.disableDialogAutocomplete(), 50);
|
||||
});
|
||||
},
|
||||
disableDialogAutocomplete() {
|
||||
this.$nextTick(() => {
|
||||
@@ -1399,45 +1702,26 @@ export default {
|
||||
line-height: 20px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.detail-address-input {
|
||||
cursor: pointer;
|
||||
|
||||
:deep(.el-input__wrapper),
|
||||
:deep(.el-input__inner) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* 港口码头编辑/新增弹窗:对齐 railway-station 统一风格(灰底 + 白卡表单 + 浮动底栏,padding 24px 与全局弹窗标准一致) */
|
||||
//.port-terminal-edit-dialog {
|
||||
// /* 灰底内容区 */
|
||||
// .el-dialog__body {
|
||||
// background-color: #f2f2f2;
|
||||
// padding: 8px;
|
||||
// }
|
||||
// /* 表单白卡(与全局 .el-dialog .avue-form 一致,padding 24px 对齐 customer-archive / railway-station) */
|
||||
// .avue-form {
|
||||
// background: #fff;
|
||||
// border-radius: 6px;
|
||||
// box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
// padding: 24px !important;
|
||||
// margin-bottom: 0;
|
||||
// }
|
||||
// /* 底部按钮区白卡(Avue 弹窗 footer 在 body 内,接续白卡) */
|
||||
// .avue-dialog__footer {
|
||||
// background: #fff;
|
||||
// border-radius: 0 0 6px 6px;
|
||||
// box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
// padding: 16px 24px !important;
|
||||
// display: flex;
|
||||
// justify-content: flex-end;
|
||||
// }
|
||||
// /* label 宽度统一 108px(对齐 railway-station,标签对齐更整齐) */
|
||||
// .el-form-item__label {
|
||||
// width: 108px !important;
|
||||
// }
|
||||
// /* 所有 input 文本框、select 下拉控件统一宽度 250px */
|
||||
// .el-input,
|
||||
// .el-select {
|
||||
// width: 250px !important;
|
||||
// }
|
||||
// /* 备注宽度 85% */
|
||||
// .remark-input {
|
||||
// width: 85% !important;
|
||||
// }
|
||||
//}
|
||||
/* 港口码头编辑/新增弹窗:与全局表单项行距保持一致 */
|
||||
.port-terminal-edit-dialog {
|
||||
.el-form-item,
|
||||
.el-form-item--default.el-form-item,
|
||||
.el-form-item--small.el-form-item,
|
||||
.el-form-item--large.el-form-item,
|
||||
.el-form-item--medium.el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -94,6 +94,17 @@
|
||||
/>
|
||||
</el-select>
|
||||
</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>
|
||||
<el-input
|
||||
v-model="form.longitude"
|
||||
@@ -121,25 +132,36 @@
|
||||
</template>
|
||||
</avue-form>
|
||||
</el-dialog>
|
||||
<address-map-picker
|
||||
v-model="addressMapPickerVisible"
|
||||
:address="form.detailAddress"
|
||||
:longitude="form.longitude"
|
||||
:latitude="form.latitude"
|
||||
@confirm="handleAddressMapConfirm"
|
||||
/>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getList, getDetail, submit, remove, changeStatus } from '@/api/base/railway-station';
|
||||
import { getLazyTree } from '@/api/base/region';
|
||||
import { getDetail as getRegionDetail, getLazyTree } from '@/api/base/region';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
|
||||
const DEFAULT_COUNTRY_CODE = '+86';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddressMapPicker,
|
||||
},
|
||||
data() {
|
||||
const validateTmisCode = (rule, value, callback) => {
|
||||
if (!/^\d{5}$/.test(value || '')) {
|
||||
@@ -149,7 +171,12 @@ export default {
|
||||
}
|
||||
};
|
||||
const validateTelegraphCode = (rule, value, callback) => {
|
||||
if (!/^[A-Z]{3}$/.test(String(value || '').toUpperCase())) {
|
||||
const telegraphCode = String(value || '').trim();
|
||||
if (!telegraphCode) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (!/^[A-Z]{3}$/.test(telegraphCode.toUpperCase())) {
|
||||
callback(new Error('电报略码格式不正确或已存在'));
|
||||
} else {
|
||||
callback();
|
||||
@@ -185,6 +212,7 @@ export default {
|
||||
loading: true,
|
||||
data: [],
|
||||
excelBox: false,
|
||||
addressMapPickerVisible: false,
|
||||
excelForm: {},
|
||||
provinceOptions: [],
|
||||
cityOptions: [],
|
||||
@@ -250,10 +278,7 @@ export default {
|
||||
prop: 'telegraphCode',
|
||||
minWidth: 100,
|
||||
maxlength: 3,
|
||||
rules: [
|
||||
{ required: true, message: '电报略码格式不正确或已存在', trigger: 'blur' },
|
||||
{ validator: validateTelegraphCode, trigger: 'blur' },
|
||||
],
|
||||
rules: [{ validator: validateTelegraphCode, trigger: 'blur' }],
|
||||
change: ({ value }) => this.handleTelegraphChange(value),
|
||||
blur: ({ value }) => this.handleTelegraphChange(value),
|
||||
},
|
||||
@@ -270,6 +295,20 @@ export default {
|
||||
// prop: 'railwayLine',
|
||||
// minWidth: 130,
|
||||
// },
|
||||
{
|
||||
label: '详细地址',
|
||||
prop: 'detailAddress',
|
||||
formslot: true,
|
||||
span: 24,
|
||||
minWidth: 220,
|
||||
overHidden: false,
|
||||
maxlength: 255,
|
||||
showWordLimit: true,
|
||||
rules: [
|
||||
{ required: true, message: '请选择详细地址', trigger: 'change' },
|
||||
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '所属省份',
|
||||
prop: 'provinceCode',
|
||||
@@ -337,21 +376,6 @@ export default {
|
||||
addDisplay: 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: '经度',
|
||||
prop: 'longitude',
|
||||
@@ -411,6 +435,13 @@ export default {
|
||||
showWordLimit: true,
|
||||
rules: [{ max: 200, message: '备注不能超过200字', trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '更新人',
|
||||
prop: 'updateUserName',
|
||||
@@ -497,10 +528,7 @@ export default {
|
||||
};
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo && this.userInfo.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
ids() {
|
||||
let ids = [];
|
||||
@@ -523,33 +551,51 @@ export default {
|
||||
return targetColumns.find(item => item.prop === targetProp) || null;
|
||||
},
|
||||
loadProvinceOptions() {
|
||||
getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
|
||||
return getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
|
||||
this.provinceOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||
this.findColumn('provinceCode').dicData = this.provinceOptions;
|
||||
const provinceColumn = this.findColumn('provinceCode');
|
||||
if (provinceColumn) {
|
||||
provinceColumn.dicData = this.provinceOptions;
|
||||
}
|
||||
return this.provinceOptions;
|
||||
});
|
||||
},
|
||||
loadCityOptions(provinceCode) {
|
||||
if (!provinceCode) {
|
||||
this.cityOptions = [];
|
||||
this.findColumn('cityCode').dicData = [];
|
||||
return Promise.resolve();
|
||||
const cityColumn = this.findColumn('cityCode');
|
||||
if (cityColumn) {
|
||||
cityColumn.dicData = [];
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return getLazyTree(provinceCode).then(res => {
|
||||
this.cityOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||
this.findColumn('cityCode').dicData = this.cityOptions;
|
||||
const cityColumn = this.findColumn('cityCode');
|
||||
if (cityColumn) {
|
||||
cityColumn.dicData = this.cityOptions;
|
||||
}
|
||||
return this.cityOptions;
|
||||
});
|
||||
},
|
||||
loadDistrictOptions(cityCode) {
|
||||
if (!cityCode) {
|
||||
this.districtOptions = [];
|
||||
this.findColumn('districtCode').dicData = [];
|
||||
return Promise.resolve();
|
||||
const districtColumn = this.findColumn('districtCode');
|
||||
if (districtColumn) {
|
||||
districtColumn.dicData = [];
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
this.districtLoading = true;
|
||||
return getLazyTree(cityCode)
|
||||
.then(res => {
|
||||
this.districtOptions = (res.data.data || []).map(this.normalizeRegionOption);
|
||||
this.findColumn('districtCode').dicData = this.districtOptions;
|
||||
const districtColumn = this.findColumn('districtCode');
|
||||
if (districtColumn) {
|
||||
districtColumn.dicData = this.districtOptions;
|
||||
}
|
||||
return this.districtOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.districtLoading = false;
|
||||
@@ -652,8 +698,272 @@ export default {
|
||||
handleCoordinateInput(prop, 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) {
|
||||
row.code = row.tmisCode ? `TL${row.tmisCode}` : row.code;
|
||||
row.telegraphCode = String(row.telegraphCode || '').trim() || undefined;
|
||||
row.regionCode = String(row.regionCode || '').trim();
|
||||
row.regionName = String(row.regionName || '').trim();
|
||||
row.districtCode = String(row.districtCode || '').trim();
|
||||
@@ -686,14 +996,15 @@ export default {
|
||||
return records.some(item => item[prop] === row[prop] && String(item.id || '') !== rowId);
|
||||
});
|
||||
};
|
||||
return Promise.all([
|
||||
hasDuplicate({ tmisCode: row.tmisCode }, 'tmisCode'),
|
||||
hasDuplicate({ telegraphCode: row.telegraphCode }, 'telegraphCode'),
|
||||
]).then(([tmisExists, telegraphExists]) => {
|
||||
if (tmisExists) {
|
||||
const uniqueChecks = [hasDuplicate({ tmisCode: row.tmisCode }, 'tmisCode')];
|
||||
if (row.telegraphCode) {
|
||||
uniqueChecks.push(hasDuplicate({ telegraphCode: row.telegraphCode }, 'telegraphCode'));
|
||||
}
|
||||
return Promise.all(uniqueChecks).then(results => {
|
||||
if (results[0]) {
|
||||
return Promise.reject(new Error('该TMIS编码已存在'));
|
||||
}
|
||||
if (telegraphExists) {
|
||||
if (results[1]) {
|
||||
return Promise.reject(new Error('电报略码格式不正确或已存在'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
@@ -910,9 +1221,16 @@ export default {
|
||||
:deep(.el-table td .cell) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.detail-address-input {
|
||||
cursor: pointer;
|
||||
|
||||
:deep(.el-input__wrapper),
|
||||
:deep(.el-input__inner) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
+55
-42
@@ -26,6 +26,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="区域层级">
|
||||
<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="2" />
|
||||
<el-option label="区县" :value="3" />
|
||||
@@ -42,6 +43,13 @@
|
||||
</section-card>
|
||||
|
||||
<el-button-group>
|
||||
<el-button
|
||||
v-if="permission.region_add"
|
||||
type="primary"
|
||||
icon="el-icon-plus"
|
||||
@click="addCountry"
|
||||
>新增国家
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="permission.region_add"
|
||||
type="primary"
|
||||
@@ -112,14 +120,7 @@
|
||||
placeholder="请输入国家编码,如 +86"
|
||||
v-model="regionForm.code"
|
||||
/>
|
||||
<el-input
|
||||
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>
|
||||
<el-input v-else placeholder="请输入区划编号" v-model="regionForm.subCode" />
|
||||
</template>
|
||||
</avue-form>
|
||||
<el-dialog title="行政区划数据导入" append-to-body v-model="excelBox" width="555px">
|
||||
@@ -253,9 +254,10 @@ export default {
|
||||
dicUrl: '/blade-system/dict/dictionary?code=region',
|
||||
dicFormatter: response => {
|
||||
const list = Array.isArray(response) ? response : response?.data;
|
||||
return (Array.isArray(list) ? list : [])
|
||||
.filter(item => Number(item.dictKey) !== 0)
|
||||
.map(item => ({ ...item, dictKey: Number(item.dictKey) }));
|
||||
return (Array.isArray(list) ? list : []).map(item => ({
|
||||
...item,
|
||||
dictKey: Number(item.dictKey),
|
||||
}));
|
||||
},
|
||||
props: {
|
||||
label: 'dictValue',
|
||||
@@ -410,9 +412,6 @@ export default {
|
||||
isCountryRegion() {
|
||||
return Number(this.regionForm.regionLevel) === 0;
|
||||
},
|
||||
isProvinceRegion() {
|
||||
return Number(this.regionForm.regionLevel) === 1;
|
||||
},
|
||||
isEditRegion() {
|
||||
return !validatenull(this.regionForm.originalCode);
|
||||
},
|
||||
@@ -476,10 +475,7 @@ export default {
|
||||
getDetail(this.treeCode).then(res => {
|
||||
this.regionForm = res.data.data;
|
||||
this.regionForm.originalCode = this.regionForm.code;
|
||||
this.regionForm.subCode =
|
||||
this.regionForm.parentCode === this.topCode
|
||||
? this.regionForm.code
|
||||
: this.regionForm.code.replace(this.regionForm.parentCode, '');
|
||||
this.regionForm.subCode = this.regionForm.code;
|
||||
this.loadParentOptions(this.regionForm.parentCode, this.regionForm.parentName);
|
||||
});
|
||||
},
|
||||
@@ -500,25 +496,31 @@ export default {
|
||||
};
|
||||
},
|
||||
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('请先选择一项区划');
|
||||
return;
|
||||
}
|
||||
if (Number(this.regionForm.regionLevel) >= 3) {
|
||||
if (Number(current.regionLevel) >= 3) {
|
||||
this.$message.warning('区县已是末级,不能新增下级');
|
||||
return;
|
||||
}
|
||||
const parentCode = this.regionForm.code;
|
||||
const parentName = this.regionForm.name;
|
||||
const parentGroupCode = this.regionForm.parentCode || this.topCode;
|
||||
const parentName = current.name;
|
||||
const parentGroupCode = current.parentCode || this.topCode;
|
||||
this.setRegionColumnDisplay('code', true);
|
||||
this.regionForm.parentCode = parentCode;
|
||||
this.regionForm.parentName = parentName;
|
||||
this.regionForm.code = '';
|
||||
this.regionForm.subCode = '';
|
||||
this.regionForm.originalCode = '';
|
||||
this.regionForm.name = '';
|
||||
this.regionForm.regionLevel = Number(this.regionForm.regionLevel) + 1;
|
||||
this.regionForm = {
|
||||
parentCode,
|
||||
parentName,
|
||||
code: '',
|
||||
subCode: '',
|
||||
originalCode: '',
|
||||
name: '',
|
||||
regionLevel: Number(current.regionLevel) + 1,
|
||||
sort: 1,
|
||||
remark: '',
|
||||
status: 1,
|
||||
};
|
||||
this.loadParentOptionsByGroup(parentGroupCode, parentCode, parentName);
|
||||
},
|
||||
setRegionColumnDisplay(prop, display) {
|
||||
@@ -536,8 +538,7 @@ export default {
|
||||
if (this.isCountryRegion) {
|
||||
return;
|
||||
}
|
||||
const parentCode = this.isProvinceRegion ? '' : this.regionForm.parentCode || '';
|
||||
this.regionForm.code = parentCode + (this.regionForm.subCode || '');
|
||||
this.regionForm.code = this.regionForm.subCode || '';
|
||||
},
|
||||
getRootParentOptions() {
|
||||
return [
|
||||
@@ -586,16 +587,28 @@ export default {
|
||||
this.parentOptions = this.ensureSelectedParent(options, selectedCode, selectedName);
|
||||
});
|
||||
},
|
||||
handleSubmit(form, done) {
|
||||
if (Number(form.regionLevel) === 0) {
|
||||
form.parentCode = this.topCode;
|
||||
form.ancestors = this.topCode;
|
||||
form.subCode = form.code;
|
||||
} else {
|
||||
const parentCode = Number(form.regionLevel) === 1 ? '' : form.parentCode;
|
||||
form.code = parentCode + form.subCode;
|
||||
buildRegionPayload(form) {
|
||||
const source = validatenull(this.regionForm) ? form || {} : this.regionForm;
|
||||
const regionLevel = Number(source.regionLevel);
|
||||
const isNew = validatenull(source.originalCode);
|
||||
const payload = {
|
||||
code: regionLevel === 0 ? source.code : source.subCode,
|
||||
parentCode: regionLevel === 0 ? this.topCode : source.parentCode,
|
||||
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({
|
||||
type: 'success',
|
||||
@@ -774,6 +787,6 @@ export default {
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
.el-form-item{
|
||||
margin-bottom: 9px !important;
|
||||
margin-bottom: 22px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -204,7 +204,7 @@ import { exportBlob } from '@/api/common';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import { config, excelOption, formatCargoValue, option } from '@/option/business/common-cargo';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -250,16 +250,14 @@ export default {
|
||||
};
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo && this.userInfo.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
ids() {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.applyDefaultDeptSearch();
|
||||
this.initDeptTree();
|
||||
this.loadPriceUnitOptions();
|
||||
},
|
||||
@@ -273,6 +271,26 @@ export default {
|
||||
if (Array.isArray(data)) return data;
|
||||
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() {
|
||||
getDeptTree(this.userInfo.tenantId).then(res => {
|
||||
this.deptOptions = this.flattenDept(res.data.data || []);
|
||||
@@ -280,6 +298,18 @@ export default {
|
||||
if (deptColumn) {
|
||||
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() {
|
||||
@@ -307,9 +337,13 @@ export default {
|
||||
flattenDept(tree, level = 0) {
|
||||
const result = [];
|
||||
tree.forEach(item => {
|
||||
// 雪花 ID 必须保持字符串,避免 JS Number 精度丢失导致筛错组织。
|
||||
const id =
|
||||
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
|
||||
if (!id) return;
|
||||
result.push({
|
||||
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
||||
value: item.id,
|
||||
value: id,
|
||||
});
|
||||
if (item.children && item.children.length) {
|
||||
result.push(...this.flattenDept(item.children, level + 1));
|
||||
@@ -317,6 +351,19 @@ export default {
|
||||
});
|
||||
return result;
|
||||
},
|
||||
normalizeListQuery(params = {}) {
|
||||
// 搜索条件优先于缓存的 query,避免默认组织 ID 覆盖用户新选择的组织。
|
||||
const query = { ...this.query, ...params };
|
||||
const rawDeptId = query.deptId ?? query.searchDeptId;
|
||||
delete query.searchDeptId;
|
||||
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
|
||||
query.deptId = String(rawDeptId).split(',')[0].trim();
|
||||
} else {
|
||||
delete query.deptId;
|
||||
}
|
||||
query.allDept = this.isAdmin ? 1 : 0;
|
||||
return query;
|
||||
},
|
||||
normalizeCargoTypeOptions(list = []) {
|
||||
return list
|
||||
.map(item => ({
|
||||
@@ -485,7 +532,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
if (row.cargoValue && !/^\d+(\.\d{1,2})?$/.test(String(row.cargoValue))) {
|
||||
this.$message.warning('货值只能输入数字,最多保留2位小数');
|
||||
this.$message.warning('单价只能输入数字,最多保留2位小数');
|
||||
return false;
|
||||
}
|
||||
if (!row.model) {
|
||||
@@ -619,13 +666,14 @@ export default {
|
||||
},
|
||||
searchReset() {
|
||||
this.query = {};
|
||||
this.applyDefaultDeptSearch();
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page);
|
||||
this.onLoad(this.page, this.query);
|
||||
},
|
||||
searchChange(params, done) {
|
||||
this.query = params;
|
||||
this.query = this.normalizeListQuery(params);
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page, params);
|
||||
this.onLoad(this.page, this.query);
|
||||
done();
|
||||
},
|
||||
selectionChange(list) {
|
||||
@@ -648,8 +696,10 @@ export default {
|
||||
},
|
||||
onLoad(page, params = {}) {
|
||||
this.loading = true;
|
||||
const query = this.normalizeListQuery(params);
|
||||
this.query = query;
|
||||
api
|
||||
.getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: 0 })
|
||||
.getList(page.currentPage, page.pageSize, query)
|
||||
.then(res => {
|
||||
const result = res.data.data;
|
||||
this.page.total = result.total;
|
||||
@@ -662,8 +712,7 @@ export default {
|
||||
},
|
||||
buildExportParams() {
|
||||
return {
|
||||
...this.query,
|
||||
allDept: 0,
|
||||
...this.normalizeListQuery(this.query),
|
||||
ids: this.ids,
|
||||
[this.website.tokenHeader]: getToken(),
|
||||
};
|
||||
|
||||
@@ -289,8 +289,8 @@
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="map-picker-content">
|
||||
<map-search-results :results="routeMapSearchResults" @select="selectRouteMapSearchResult" />
|
||||
<div ref="routeAmap" class="common-route-page__map"></div>
|
||||
<map-search-results :results="routeMapSearchResults" @select="selectRouteMapSearchResult" />
|
||||
</div>
|
||||
<div class="common-route-page__map-info">
|
||||
<span>{{ routeMapStatus }}</span>
|
||||
@@ -321,8 +321,9 @@ import { exportBlob } from '@/api/common';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import { addressTypeOptions } from '@/option/base/common-address';
|
||||
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 { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
import { List, Search } from '@element-plus/icons-vue';
|
||||
@@ -389,10 +390,7 @@ export default {
|
||||
};
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo && this.userInfo.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
ids() {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
@@ -417,19 +415,27 @@ export default {
|
||||
});
|
||||
},
|
||||
buildDeptTree(tree) {
|
||||
return tree.map(item => ({
|
||||
label: item.title || item.deptName || item.name,
|
||||
value: item.id,
|
||||
children:
|
||||
item.children && item.children.length ? this.buildDeptTree(item.children) : undefined,
|
||||
}));
|
||||
return tree.map(item => {
|
||||
// 雪花 ID 必须保持字符串,避免 JS Number 精度丢失导致筛错组织。
|
||||
const id =
|
||||
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
|
||||
return {
|
||||
label: item.title || item.deptName || item.name,
|
||||
value: id,
|
||||
children:
|
||||
item.children && item.children.length ? this.buildDeptTree(item.children) : undefined,
|
||||
};
|
||||
}).filter(item => item.value);
|
||||
},
|
||||
flattenDept(tree, level = 0) {
|
||||
const result = [];
|
||||
tree.forEach(item => {
|
||||
const id =
|
||||
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
|
||||
if (!id) return;
|
||||
result.push({
|
||||
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
||||
value: item.id,
|
||||
value: id,
|
||||
});
|
||||
if (item.children && item.children.length) {
|
||||
result.push(...this.flattenDept(item.children, level + 1));
|
||||
@@ -438,7 +444,26 @@ export default {
|
||||
return result;
|
||||
},
|
||||
defaultDeptId() {
|
||||
return (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
|
||||
const raw = (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
|
||||
return (
|
||||
String(raw)
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)[0] || ''
|
||||
);
|
||||
},
|
||||
normalizeListQuery(params = {}) {
|
||||
// 搜索条件优先于缓存的 query,避免默认组织 ID 覆盖用户新选择的组织。
|
||||
const query = { ...this.query, ...params };
|
||||
const 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() {
|
||||
const deptId = this.defaultDeptId();
|
||||
@@ -489,7 +514,7 @@ export default {
|
||||
this.addressLoading = true;
|
||||
getCommonAddressList(this.addressPage.currentPage, this.addressPage.pageSize, {
|
||||
...this.addressQuery,
|
||||
allDept: 0,
|
||||
allDept: this.isAdmin ? 1 : 0,
|
||||
})
|
||||
.then(res => {
|
||||
const data = res.data.data || {};
|
||||
@@ -609,30 +634,29 @@ export default {
|
||||
this.$message.warning('请输入地址关键词');
|
||||
return;
|
||||
}
|
||||
if (this.routeMapLoading) return;
|
||||
this.routeMapSearchResults = [];
|
||||
this.routeMapLoading = true;
|
||||
this.loadAmap()
|
||||
.then(() => {
|
||||
this.routeMapLoading = true;
|
||||
return this.ensureRouteAmapGeocoder();
|
||||
})
|
||||
.then(() => this.runAmapGeocode('location', keyword))
|
||||
.then(result => {
|
||||
this.routeMapSearchResults = (result.geocodes || []).map((item, index) => ({
|
||||
id: item.id || index,
|
||||
name: item.formattedAddress || keyword,
|
||||
address: item.formattedAddress || keyword,
|
||||
location: item.location,
|
||||
}));
|
||||
const point = this.resolveMapPoint(result);
|
||||
if (!point) {
|
||||
.then(() => searchAmapPlaceText(keyword))
|
||||
.then(pois => {
|
||||
this.routeMapSearchResults = normalizeMapSearchResults(pois, keyword);
|
||||
const first = this.routeMapSearchResults[0];
|
||||
if (!first?.location) {
|
||||
this.routeMapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
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(() => {
|
||||
this.routeMapStatus = '地图搜索失败';
|
||||
this.$message.error('地图搜索失败,请稍后重试');
|
||||
this.routeMapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
})
|
||||
.finally(() => {
|
||||
this.routeMapLoading = false;
|
||||
@@ -1002,9 +1026,9 @@ export default {
|
||||
this.onLoad(this.page, this.query);
|
||||
},
|
||||
searchChange(params, done) {
|
||||
this.query = params;
|
||||
this.query = this.normalizeListQuery(params);
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page, params);
|
||||
this.onLoad(this.page, this.query);
|
||||
done();
|
||||
},
|
||||
selectionChange(list) {
|
||||
@@ -1027,8 +1051,10 @@ export default {
|
||||
},
|
||||
onLoad(page, params = {}) {
|
||||
this.loading = true;
|
||||
const query = this.normalizeListQuery(params);
|
||||
this.query = query;
|
||||
api
|
||||
.getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: 0 })
|
||||
.getList(page.currentPage, page.pageSize, query)
|
||||
.then(res => {
|
||||
const result = res.data.data;
|
||||
this.page.total = result.total;
|
||||
@@ -1041,8 +1067,7 @@ export default {
|
||||
},
|
||||
buildExportParams() {
|
||||
return {
|
||||
...this.query,
|
||||
allDept: 0,
|
||||
...this.normalizeListQuery(this.query),
|
||||
ids: this.ids,
|
||||
[this.website.tokenHeader]: getToken(),
|
||||
};
|
||||
|
||||
@@ -162,16 +162,16 @@
|
||||
v-model="row.billingUnit"
|
||||
clearable
|
||||
filterable
|
||||
:disabled="!row.billingElement"
|
||||
:disabled="!canSelectBillingUnit(row)"
|
||||
:loading="unitLoading"
|
||||
:placeholder="row.billingElement ? '请选择' : '请先选择计费要素'"
|
||||
:placeholder="billingUnitPlaceholder(row)"
|
||||
><el-option
|
||||
v-for="item in unitOptionsFor(row)"
|
||||
:key="item.id || item.value"
|
||||
:label="item.label"
|
||||
:value="item.value" /></el-select></template
|
||||
></el-table-column>
|
||||
<el-table-column label="单价(元)" width="180"
|
||||
<el-table-column :label="unitPriceColumnLabel" width="180"
|
||||
><template #default="{ row }"
|
||||
><div v-if="readonly && usesRangeUnitPrice(row)" class="limit-list">
|
||||
<span v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex">{{
|
||||
@@ -379,13 +379,24 @@ import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import SectionCard from '@/components/section-card/main.vue';
|
||||
import { currencyUnit } from '@/utils/currency';
|
||||
|
||||
const clone = value => JSON.parse(JSON.stringify(value));
|
||||
/** 计费要素与计量单位维度的对应关系 */
|
||||
/** 计费要素与计量单位维度的对应关系(这些要素可选计费单位) */
|
||||
const BILLING_ELEMENT_DIMENSION_MAP = {
|
||||
按重量: '重量',
|
||||
按体积: '体积',
|
||||
按车辆: '数量',
|
||||
按数量: '数量',
|
||||
按里程: '里程',
|
||||
'按吨·公里': '吨·公里',
|
||||
'固定金额(整单一口价)': '整车',
|
||||
};
|
||||
const BILLING_UNIT_SELECTABLE_ELEMENTS = Object.keys(BILLING_ELEMENT_DIMENSION_MAP);
|
||||
/** 对应维度下无计量单位时的默认计费单位 */
|
||||
const DEFAULT_BILLING_UNIT_MAP = {
|
||||
按里程: '公里',
|
||||
'按吨·公里': '吨·公里',
|
||||
'固定金额(整单一口价)': '单',
|
||||
};
|
||||
const defaultRule = () => ({
|
||||
feeType: '',
|
||||
@@ -421,6 +432,7 @@ export default {
|
||||
value: { type: Object, default: () => ({}) },
|
||||
index: { type: Number, default: -1 },
|
||||
readonly: Boolean,
|
||||
settlementCurrency: { type: String, default: 'RMB' },
|
||||
},
|
||||
emits: ['update:modelValue', 'save'],
|
||||
data() {
|
||||
@@ -436,7 +448,6 @@ export default {
|
||||
billingElements: [
|
||||
'按重量',
|
||||
'按体积',
|
||||
'按车辆',
|
||||
'按里程',
|
||||
'按吨·公里',
|
||||
'固定金额(整单一口价)',
|
||||
@@ -445,7 +456,6 @@ export default {
|
||||
typeMap: {
|
||||
按重量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
|
||||
按体积: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
|
||||
按车辆: ['固定单价'],
|
||||
按里程: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
|
||||
'按吨·公里': ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
|
||||
'固定金额(整单一口价)': ['固定一口价'],
|
||||
@@ -492,6 +502,9 @@ export default {
|
||||
emitPath: true,
|
||||
};
|
||||
},
|
||||
unitPriceColumnLabel() {
|
||||
return `单价(${currencyUnit(this.settlementCurrency)})`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadDictionaries();
|
||||
@@ -560,6 +573,7 @@ export default {
|
||||
next.limitRanges = next.limitRanges.map(item => ({ ...item, minimumBillingWeight: '' }));
|
||||
this.syncLegacyLimit(next);
|
||||
next.matchCondition = { ...defaultRule().matchCondition, ...(rule.matchCondition || {}) };
|
||||
this.syncBillingUnit(next);
|
||||
return next;
|
||||
},
|
||||
normalizeRanges(row) {
|
||||
@@ -646,36 +660,61 @@ export default {
|
||||
}),
|
||||
]).finally(() => {
|
||||
this.unitLoading = false;
|
||||
(this.draft.rules || []).forEach(row => this.syncBillingUnit(row));
|
||||
});
|
||||
},
|
||||
measurementDimension(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) {
|
||||
const dimension = this.measurementDimension(row?.billingElement);
|
||||
if (dimension) {
|
||||
return this.measurementUnits
|
||||
.filter(item => String(item.dimension || '').trim() === dimension)
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
label: item.unitName,
|
||||
value: item.unitName,
|
||||
}))
|
||||
.filter(item => item.value);
|
||||
if (!this.canSelectBillingUnit(row)) {
|
||||
return [];
|
||||
}
|
||||
return (this.unitOptions || [])
|
||||
.map(item => ({
|
||||
id: item.id || item.dictKey || item.dictValue,
|
||||
label: item.dictValue,
|
||||
value: item.dictValue,
|
||||
}))
|
||||
.filter(item => item.value);
|
||||
const dimension = this.measurementDimension(row?.billingElement);
|
||||
const defaultUnit = this.defaultBillingUnit(row?.billingElement);
|
||||
const options = dimension
|
||||
? this.measurementUnits
|
||||
.filter(item => String(item.dimension || '').trim() === dimension)
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
label: item.unitName,
|
||||
value: item.unitName,
|
||||
}))
|
||||
.filter(item => item.value)
|
||||
: [];
|
||||
// 对应维度未配置计量单位时,下拉仅展示默认值
|
||||
if (!options.length && defaultUnit) {
|
||||
return [{ id: `default-${defaultUnit}`, label: defaultUnit, value: defaultUnit }];
|
||||
}
|
||||
return options;
|
||||
},
|
||||
syncBillingUnit(row) {
|
||||
const options = this.unitOptionsFor(row);
|
||||
if (!options.some(item => String(item.value) === String(row.billingUnit || ''))) {
|
||||
if (!this.canSelectBillingUnit(row)) {
|
||||
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) {
|
||||
const option = this.feeCategories.find(
|
||||
@@ -892,7 +931,6 @@ export default {
|
||||
['taxRate', '税率'],
|
||||
['billingElement', '计费要素'],
|
||||
['billingType', '计费类型'],
|
||||
['billingUnit', '计费单位'],
|
||||
];
|
||||
for (const [i, row] of this.draft.rules.entries()) {
|
||||
const empty = required.find(
|
||||
@@ -902,6 +940,15 @@ export default {
|
||||
this.$message.warning(`第${i + 1}行${empty[1]}不能为空`);
|
||||
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();
|
||||
if (feeItemSet.has(feeItem)) {
|
||||
this.$message.warning(`费用项“${feeItem}”不能重复`);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,94 +1,108 @@
|
||||
<template>
|
||||
<section
|
||||
class="contract-manage-form__section contract-manage-form__section--panel contract-manage-form__section--attachment"
|
||||
>
|
||||
<div class="contract-manage-form__attachment-head">
|
||||
<div class="dialog-section-title">{{ title }}</div>
|
||||
<el-button type="primary" :disabled="!rows.length" @click="batchDownload">批量下载</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
:data="rows"
|
||||
border
|
||||
class="contract-manage-form__attachment-table"
|
||||
@selection-change="selected = $event"
|
||||
<div class="contract-attachment-section">
|
||||
<section
|
||||
v-if="!dialogOnly"
|
||||
class="contract-manage-form__section contract-manage-form__section--panel contract-manage-form__section--attachment"
|
||||
>
|
||||
<el-table-column v-if="!readonly" type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column v-if="attachmentType" label="附件类型" min-width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="readonly || isRowLocked(row)">{{ row.fileType || '-' }}</span>
|
||||
<el-select
|
||||
v-else
|
||||
:model-value="row.fileType"
|
||||
placeholder="请选择"
|
||||
:teleported="false"
|
||||
:fit-input-width="true"
|
||||
@update:model-value="value => updateRowFileType(row, value)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in contractAttachmentTypeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="240"
|
||||
align="left"
|
||||
header-align="left"
|
||||
show-overflow-tooltip
|
||||
<div class="contract-manage-form__attachment-head">
|
||||
<div class="dialog-section-title">{{ title }}</div>
|
||||
<el-button type="primary" :disabled="!rows.length" @click="batchDownload"
|
||||
>批量下载</el-button
|
||||
>
|
||||
</div>
|
||||
<el-table
|
||||
:data="rows"
|
||||
border
|
||||
class="contract-manage-form__attachment-table"
|
||||
@selection-change="selected = $event"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="preview?.(row, rows)">{{ attachmentName(row) }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="description" label="附件描述" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<span v-if="readonly || isRowLocked(row)">{{ row.description || '-' }}</span>
|
||||
<el-input
|
||||
v-else
|
||||
:model-value="row.description"
|
||||
maxlength="200"
|
||||
@update:model-value="value => updateRowDescription(row, value)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文件大小" width="120" align="center">
|
||||
<template #default="{ row }">{{ formatSize(row.size) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
||||
<el-table-column prop="uploadTime" label="上传时间" width="180" align="center" sortable />
|
||||
<el-table-column v-if="!readonly" label="操作" width="100" align="center" fixed="right">
|
||||
<template #default="{ row, $index }">
|
||||
<span v-if="isRowLocked(row)">-</span>
|
||||
<el-link v-else type="danger" @click="remove($index)">删除</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column v-if="attachmentType" label="附件类型" min-width="160" align="center">
|
||||
<template #default="{ row, $index }">
|
||||
<span v-if="readonly || isRowLocked(row)">{{ row.fileType || '-' }}</span>
|
||||
<el-select
|
||||
v-else
|
||||
:model-value="row.fileType"
|
||||
placeholder="请选择"
|
||||
style="width: 100%"
|
||||
teleported
|
||||
clearable
|
||||
@update:model-value="value => updateRowFileType($index, value)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in contractAttachmentTypeOptions"
|
||||
:key="`${item.value}-${item.label}`"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="240"
|
||||
align="left"
|
||||
header-align="left"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="preview?.(row, rows)"
|
||||
@keydown.enter="preview?.(row, rows)"
|
||||
@keydown.space.prevent="preview?.(row, rows)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="description" label="附件描述" min-width="220">
|
||||
<template #default="{ row, $index }">
|
||||
<span v-if="readonly || isRowLocked(row)">{{ row.description || '-' }}</span>
|
||||
<el-input
|
||||
v-else
|
||||
:model-value="row.description"
|
||||
maxlength="200"
|
||||
@update:model-value="value => updateRowDescription($index, value)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文件大小" width="120" align="center">
|
||||
<template #default="{ row }">{{ formatSize(row.size) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
||||
<el-table-column prop="uploadTime" label="上传时间" width="180" align="center" sortable />
|
||||
<el-table-column v-if="!readonly" label="操作" width="100" align="center" fixed="right">
|
||||
<template #default="{ row, $index }">
|
||||
<span v-if="isRowLocked(row)">-</span>
|
||||
<el-link v-else type="danger" @click="remove($index)">删除</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div v-if="!readonly" class="contract-manage-form__attachment-upload">
|
||||
<template v-if="useUploadDialog">
|
||||
<el-button type="primary" plain @click="openUploadDialog">上传附件</el-button>
|
||||
<span class="contract-manage-form__attachment-tip">
|
||||
支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过50M
|
||||
</span>
|
||||
</template>
|
||||
<vehicle-attachment-upload
|
||||
v-else
|
||||
:model-value="rows"
|
||||
:file-types="attachmentFileTypes"
|
||||
:max-size="50"
|
||||
show-tip
|
||||
tip="支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过50M"
|
||||
:show-file-list="false"
|
||||
button-text="上传附件"
|
||||
@update:model-value="handleChange"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!readonly" class="contract-manage-form__attachment-upload">
|
||||
<template v-if="useUploadDialog">
|
||||
<el-button type="primary" plain @click="openUploadDialog">上传附件</el-button>
|
||||
<span class="contract-manage-form__attachment-tip">
|
||||
支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过50M
|
||||
</span>
|
||||
</template>
|
||||
<vehicle-attachment-upload
|
||||
v-else
|
||||
:model-value="rows"
|
||||
:file-types="attachmentFileTypes"
|
||||
:max-size="50"
|
||||
show-tip
|
||||
tip="支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过50M"
|
||||
:show-file-list="false"
|
||||
button-text="上传附件"
|
||||
@change="handleChange"
|
||||
@success="handleUploadSuccess"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="uploadDialogVisible"
|
||||
@@ -103,8 +117,18 @@
|
||||
<div class="contract-attachment-upload-dialog__body">
|
||||
<div class="contract-attachment-upload-dialog__field">
|
||||
<span class="contract-attachment-upload-dialog__label">附件位置</span>
|
||||
<el-select :model-value="attachmentLocation" disabled style="width: 100%">
|
||||
<el-option :label="attachmentLocation" :value="attachmentLocation" />
|
||||
<el-select
|
||||
v-model="uploadDialogLocation"
|
||||
placeholder="请选择附件位置"
|
||||
style="width: 100%"
|
||||
@change="syncUploadDialogTypeByLocation"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in resolvedLocationOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="contract-attachment-upload-dialog__field">
|
||||
@@ -144,7 +168,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -154,14 +178,42 @@ import { getUploadHeaders } from '@/utils/upload';
|
||||
import { downloadFileByUrl } from '@/utils/util';
|
||||
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 = [
|
||||
{ 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 isContractSealMaterialLocation = location => {
|
||||
const text = String(location || '');
|
||||
return text === CONTRACT_SEAL_MATERIAL_LOCATION || text === '合同文件';
|
||||
};
|
||||
export const attachmentFileTypes = [
|
||||
'pdf',
|
||||
'bmp',
|
||||
@@ -201,21 +253,35 @@ const normalizeAttachmentTypeText = value => {
|
||||
.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 => {
|
||||
if (fileType === '双章归档文件') return ['双章归档文件', '双章归档', '双章'];
|
||||
if (fileType === '合同文件') return ['合同文件', '合同'];
|
||||
return [fileType];
|
||||
const text = String(fileType || '');
|
||||
if (text.includes('双章')) return ['双章归档文件', '双章归档', '双章'];
|
||||
if (
|
||||
text.includes('用印') ||
|
||||
(text.includes('合同') && !text.includes('其他') && !text.includes('其它'))
|
||||
) {
|
||||
return ['用印材料', '合同文件', '用印', '合同'];
|
||||
}
|
||||
if (isOtherAttachmentTypeOption({ label: text, value: text })) return [];
|
||||
return text ? [text] : [];
|
||||
};
|
||||
|
||||
const resolveOtherAttachmentType = (options = []) => {
|
||||
const list = options.length ? options : defaultContractAttachmentTypeOptions;
|
||||
const matched = list.find(
|
||||
item =>
|
||||
String(item.value) === CONTRACT_ATTACHMENT_TYPE_OTHER ||
|
||||
String(item.label).includes('其它') ||
|
||||
String(item.label).includes('其他')
|
||||
);
|
||||
return matched?.value || list[list.length - 1]?.value || CONTRACT_ATTACHMENT_TYPE_OTHER;
|
||||
const matched = list.find(isOtherAttachmentTypeOption);
|
||||
return matched?.value || CONTRACT_ATTACHMENT_TYPE_OTHER;
|
||||
};
|
||||
|
||||
export const resolveContractAttachmentType = (
|
||||
@@ -223,22 +289,32 @@ export const resolveContractAttachmentType = (
|
||||
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 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;
|
||||
const matched = list
|
||||
.filter(item => item.value !== otherType)
|
||||
.filter(item => !isOtherAttachmentTypeOption(item))
|
||||
.flatMap(item =>
|
||||
attachmentTypeKeywords(item.value).map(keyword => ({
|
||||
value: item.value,
|
||||
keyword: normalizeAttachmentTypeText(keyword),
|
||||
}))
|
||||
attachmentTypeKeywords(item.value)
|
||||
.concat(attachmentTypeKeywords(item.label))
|
||||
.map(keyword => ({
|
||||
value: item.value,
|
||||
keyword: normalizeAttachmentTypeText(keyword),
|
||||
}))
|
||||
)
|
||||
.filter(item => item.keyword)
|
||||
.sort((a, b) => b.keyword.length - a.keyword.length)
|
||||
.find(item => fileName.includes(item.keyword));
|
||||
// 文件名未命中任何附件类型时,默认「其他文件」
|
||||
return matched?.value || otherType;
|
||||
};
|
||||
|
||||
@@ -249,17 +325,32 @@ export const normalizeContractFileRows = (rows, options = defaultContractAttachm
|
||||
description: row.description || '',
|
||||
}));
|
||||
|
||||
const mapDictOptions = response => {
|
||||
const mapDictOptions = (response, dictCode = CONTRACT_ATTACHMENT_TYPE_DICT) => {
|
||||
const data = response?.data?.data || response?.data || [];
|
||||
const records = Array.isArray(data) ? data : data.records || [];
|
||||
const applySealAlias = dictCode === CONTRACT_ATTACHMENT_TYPE_DICT;
|
||||
return records
|
||||
.map(item => ({
|
||||
label: item.dictValue,
|
||||
value: item.dictKey,
|
||||
}))
|
||||
.map(item => {
|
||||
const rawLabel = String(item.dictValue || '');
|
||||
const rawValue = String(item.dictKey || '');
|
||||
const label = applySealAlias
|
||||
? CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawLabel] ||
|
||||
(rawLabel === '合同文件' ? CONTRACT_SEAL_MATERIAL_LOCATION : rawLabel)
|
||||
: rawLabel;
|
||||
const value = applySealAlias
|
||||
? CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawValue] ||
|
||||
(rawValue === '合同文件' ? CONTRACT_SEAL_MATERIAL_LOCATION : rawValue)
|
||||
: rawValue;
|
||||
return { label, value };
|
||||
})
|
||||
.filter(item => item.label && item.value);
|
||||
};
|
||||
|
||||
const defaultOptionsByDictCode = dictCode =>
|
||||
dictCode === CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT
|
||||
? [...defaultContractSupportingAttachmentTypeOptions]
|
||||
: [...defaultContractAttachmentTypeOptions];
|
||||
|
||||
export default {
|
||||
name: 'ContractAttachmentSection',
|
||||
components: { UploadFilled },
|
||||
@@ -272,16 +363,25 @@ export default {
|
||||
lockApproved: Boolean,
|
||||
markApprovedOnUpload: 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 },
|
||||
},
|
||||
emits: ['update:rows'],
|
||||
emits: ['update:rows', 'upload-to-location', 'upload-confirmed'],
|
||||
data() {
|
||||
return {
|
||||
selected: [],
|
||||
attachmentFileTypes,
|
||||
contractAttachmentTypeOptions: [...defaultContractAttachmentTypeOptions],
|
||||
activeAttachmentTypeDictCode: CONTRACT_ATTACHMENT_TYPE_DICT,
|
||||
uploadDialogVisible: false,
|
||||
uploadDialogLocation: CONTRACT_SEAL_MATERIAL_LOCATION,
|
||||
uploadDialogType: resolveOtherAttachmentType(defaultContractAttachmentTypeOptions),
|
||||
uploadDialogFiles: [],
|
||||
uploadDialogFileList: [],
|
||||
@@ -300,47 +400,186 @@ export default {
|
||||
defaultAttachmentType() {
|
||||
return resolveOtherAttachmentType(this.contractAttachmentTypeOptions);
|
||||
},
|
||||
resolvedAttachmentTypeDictCode() {
|
||||
return this.attachmentTypeDictCode || CONTRACT_ATTACHMENT_TYPE_DICT;
|
||||
},
|
||||
resolvedLocationOptions() {
|
||||
const options = (this.attachmentLocationOptions || []).filter(Boolean);
|
||||
if (options.length) {
|
||||
return options.map(item => {
|
||||
if (item === '合同文件') return CONTRACT_SEAL_MATERIAL_LOCATION;
|
||||
if (item === '其它附件' || item === '其他附件') {
|
||||
return CONTRACT_SUPPORTING_ATTACHMENT_LOCATION;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return [CONTRACT_SEAL_MATERIAL_LOCATION, CONTRACT_SUPPORTING_ATTACHMENT_LOCATION];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
attachmentTypeDictCode: {
|
||||
immediate: false,
|
||||
handler() {
|
||||
this.loadAttachmentTypeOptions(this.resolvedAttachmentTypeDictCode, true);
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadAttachmentTypeOptions();
|
||||
this.loadAttachmentTypeOptions(this.resolvedAttachmentTypeDictCode, true);
|
||||
},
|
||||
methods: {
|
||||
attachmentName,
|
||||
loadAttachmentTypeOptions() {
|
||||
return getBizDictionary({ code: 'contract_attachment_type' })
|
||||
currentUploadUserName() {
|
||||
const raw = this.$store.getters.userInfo;
|
||||
const userInfo = raw && !Array.isArray(raw) ? raw : {};
|
||||
// 与顶部栏一致优先 real_name;用印材料 / 佐证材料共用
|
||||
return (
|
||||
userInfo.real_name ||
|
||||
userInfo.realName ||
|
||||
userInfo.nick_name ||
|
||||
userInfo.nickName ||
|
||||
userInfo.user_name ||
|
||||
userInfo.userName ||
|
||||
userInfo.account ||
|
||||
''
|
||||
);
|
||||
},
|
||||
loadAttachmentTypeOptions(dictCode = this.resolvedAttachmentTypeDictCode, alignRows = false) {
|
||||
const code = dictCode || CONTRACT_ATTACHMENT_TYPE_DICT;
|
||||
this.activeAttachmentTypeDictCode = code;
|
||||
const fallbackOptions = defaultOptionsByDictCode(code);
|
||||
return getBizDictionary({ code })
|
||||
.then(res => {
|
||||
const options = mapDictOptions(res);
|
||||
// 切换字典期间若已发起更新请求,忽略过期响应
|
||||
if (this.activeAttachmentTypeDictCode !== code) return;
|
||||
const options = mapDictOptions(res, code);
|
||||
if (options.length) {
|
||||
this.contractAttachmentTypeOptions = options;
|
||||
if (
|
||||
!options.some(item => String(item.value) === String(this.uploadDialogType))
|
||||
) {
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
}
|
||||
// 字典缺少「其他文件」时补上,保证未匹配文件名可默认选中
|
||||
this.contractAttachmentTypeOptions = options.some(isOtherAttachmentTypeOption)
|
||||
? options
|
||||
: [...options, { label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER }];
|
||||
} else {
|
||||
this.contractAttachmentTypeOptions = fallbackOptions;
|
||||
}
|
||||
if (
|
||||
!this.contractAttachmentTypeOptions.some(
|
||||
item => String(item.value) === String(this.uploadDialogType)
|
||||
)
|
||||
) {
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
}
|
||||
// 字典加载后,把已有行的中文类型名对齐到 dictKey,保证下拉可选中
|
||||
if (alignRows && this.attachmentType && (this.rows || []).length) {
|
||||
const aligned = (this.rows || []).map(row => ({
|
||||
...row,
|
||||
fileType: resolveContractAttachmentType(row, this.contractAttachmentTypeOptions),
|
||||
}));
|
||||
const changed = aligned.some(
|
||||
(row, index) => row.fileType !== this.rows[index]?.fileType
|
||||
);
|
||||
if (changed) this.update(aligned);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 字典加载失败时保留默认选项
|
||||
if (this.activeAttachmentTypeDictCode !== code) return;
|
||||
this.contractAttachmentTypeOptions = fallbackOptions;
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
});
|
||||
},
|
||||
isRowLocked(row) {
|
||||
return this.lockApproved && isAttachmentApproved(row);
|
||||
},
|
||||
update(rows) {
|
||||
this.$emit('update:rows', rows);
|
||||
this.$emit('update:rows', Array.isArray(rows) ? rows.map(item => ({ ...item })) : []);
|
||||
},
|
||||
updateRowFileType(row, value) {
|
||||
if (this.isRowLocked(row)) return;
|
||||
row.fileType = value;
|
||||
this.update([...this.rows]);
|
||||
enrichUploadedRow(row, existing) {
|
||||
const uploadUserName = this.currentUploadUserName();
|
||||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
const next = {
|
||||
...existing,
|
||||
...row,
|
||||
description: row.description || existing?.description || '',
|
||||
// 新上传行始终写入当前用户;已有行保留原上传人
|
||||
uploadUserName: existing?.uploadUserName || 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) {
|
||||
if (this.isRowLocked(row)) return;
|
||||
row.description = value;
|
||||
this.update([...this.rows]);
|
||||
findExistingRow(row, rows = this.rows) {
|
||||
const fileUrl = attachmentUrl(row);
|
||||
return (rows || []).find(item => {
|
||||
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() {
|
||||
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.uploadDialogFileList = [];
|
||||
this.uploadDialogVisible = true;
|
||||
@@ -348,7 +587,8 @@ export default {
|
||||
resetUploadDialog() {
|
||||
this.uploadDialogFiles = [];
|
||||
this.uploadDialogFileList = [];
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
this.uploadDialogLocation = this.attachmentLocation;
|
||||
this.syncUploadDialogTypeByLocation();
|
||||
},
|
||||
beforeUpload(file) {
|
||||
const extension = String(file.name || '')
|
||||
@@ -391,13 +631,18 @@ export default {
|
||||
...this.uploadDialogFiles.filter(item => String(item.uid) !== String(file.uid)),
|
||||
normalized,
|
||||
];
|
||||
this.uploadDialogType = resolveContractAttachmentType(
|
||||
{
|
||||
...normalized,
|
||||
fileType: '',
|
||||
},
|
||||
this.contractAttachmentTypeOptions
|
||||
);
|
||||
const locationText = String(this.uploadDialogLocation || '');
|
||||
if (!(locationText.includes('其它') || locationText.includes('其他'))) {
|
||||
this.uploadDialogType = resolveContractAttachmentType(
|
||||
{
|
||||
...normalized,
|
||||
fileType: '',
|
||||
},
|
||||
this.contractAttachmentTypeOptions
|
||||
);
|
||||
} else {
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
}
|
||||
this.$message.success('上传成功');
|
||||
},
|
||||
handleDialogError() {
|
||||
@@ -416,57 +661,57 @@ export default {
|
||||
this.$message.warning('请先上传附件');
|
||||
return;
|
||||
}
|
||||
if (!this.uploadDialogLocation) {
|
||||
this.$message.warning('请选择附件位置');
|
||||
return;
|
||||
}
|
||||
if (!this.uploadDialogType) {
|
||||
this.$message.warning('请选择附件类型');
|
||||
return;
|
||||
}
|
||||
const uploadUserName = this.$store.getters.userInfo?.realName || '';
|
||||
const uploadUserName = this.currentUploadUserName();
|
||||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
const appended = this.uploadDialogFiles.map(row => ({
|
||||
...row,
|
||||
fileType: this.uploadDialogType,
|
||||
description: row.description || '',
|
||||
uploadUserName: row.uploadUserName || uploadUserName,
|
||||
uploadUserName: uploadUserName || row.uploadUserName || '',
|
||||
uploadTime: row.uploadTime || uploadTime,
|
||||
...(this.markApprovedOnUpload ? { approved: true } : {}),
|
||||
}));
|
||||
this.update([...(this.rows || []), ...appended]);
|
||||
// 统一交给父组件按附件位置写入对应列表,避免同位置双写或跨位置不显示
|
||||
this.$emit('upload-to-location', {
|
||||
location: this.uploadDialogLocation,
|
||||
rows: appended,
|
||||
});
|
||||
this.uploadDialogVisible = false;
|
||||
this.$emit('upload-confirmed', {
|
||||
location: this.uploadDialogLocation,
|
||||
rows: appended,
|
||||
});
|
||||
},
|
||||
handleChange(rows) {
|
||||
const uploadUserName = this.$store.getters.userInfo?.realName || '';
|
||||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
handleChange(list) {
|
||||
if (!Array.isArray(list)) return;
|
||||
// 上传组件偶发回传空列表时,禁止清空已有表格数据
|
||||
if (!list.length) return;
|
||||
const existingRows = this.rows || [];
|
||||
this.update(
|
||||
(rows || []).map(row => {
|
||||
const fileUrl = attachmentUrl(row);
|
||||
const existing = existingRows.find(item => {
|
||||
const sameUid = row.uid && item.uid && String(row.uid) === String(item.uid);
|
||||
const sameUrl = fileUrl && attachmentUrl(item) === fileUrl;
|
||||
return sameUid || sameUrl;
|
||||
});
|
||||
if (existing && this.isRowLocked(existing)) return { ...existing };
|
||||
const next = {
|
||||
...existing,
|
||||
...row,
|
||||
description: row.description || existing?.description || '',
|
||||
uploadUserName: existing?.uploadUserName || row.uploadUserName || uploadUserName,
|
||||
uploadTime: existing?.uploadTime || row.uploadTime || uploadTime,
|
||||
approved: existing?.approved || row.approved || false,
|
||||
};
|
||||
if (this.attachmentType) {
|
||||
next.fileType = resolveContractAttachmentType(
|
||||
{
|
||||
...next,
|
||||
fileType: existing?.fileType || row.fileType,
|
||||
},
|
||||
this.contractAttachmentTypeOptions
|
||||
);
|
||||
}
|
||||
if (this.markApprovedOnUpload && !existing) next.approved = true;
|
||||
return next;
|
||||
})
|
||||
);
|
||||
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) {
|
||||
const row = this.rows[index];
|
||||
@@ -474,7 +719,7 @@ export default {
|
||||
this.$message.warning('已审核通过的附件不允许删除');
|
||||
return;
|
||||
}
|
||||
const rows = [...this.rows];
|
||||
const rows = [...(this.rows || [])];
|
||||
rows.splice(index, 1);
|
||||
this.update(rows);
|
||||
},
|
||||
@@ -487,7 +732,15 @@ export default {
|
||||
downloadFileByUrl(url, attachmentName(row));
|
||||
},
|
||||
batchDownload() {
|
||||
(this.selected.length ? this.selected : this.rows).forEach(this.download);
|
||||
const sourceRows = this.selected.length ? this.selected : this.rows || [];
|
||||
const downloadableRows = sourceRows.filter(row => attachmentUrl(row));
|
||||
if (!downloadableRows.length) {
|
||||
this.$message.warning(this.selected.length ? '所选附件暂无可下载地址' : '暂无可下载附件');
|
||||
return;
|
||||
}
|
||||
downloadableRows.forEach((row, index) => {
|
||||
window.setTimeout(() => this.download(row), index * 400);
|
||||
});
|
||||
},
|
||||
formatSize(size) {
|
||||
const value = Number(size);
|
||||
@@ -504,7 +757,7 @@ export default {
|
||||
.contract-manage-form__section {
|
||||
margin: 12px 0 0;
|
||||
padding: 14px 16px 16px;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="master-detail">
|
||||
<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">
|
||||
<div>
|
||||
<dt>客户</dt>
|
||||
@@ -34,19 +44,22 @@
|
||||
<dt>附件</dt>
|
||||
<dd>
|
||||
<template v-if="attachments.length">
|
||||
<el-link
|
||||
<span
|
||||
v-for="file in attachments"
|
||||
:key="file.url || file.link || file.name || file.originalName"
|
||||
type="primary"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(file)"
|
||||
>{{ attachmentName(file) }}</el-link
|
||||
>
|
||||
@keydown.enter="previewAttachment(file)"
|
||||
@keydown.space.prevent="previewAttachment(file)"
|
||||
>{{ attachmentName(file) }}</span>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="route-map"><template v-for="(route, index) in segments" :key="route.segmentNo"><div class="route-map__node"><span :class="['route-badge', index ? 'middle' : 'start']">{{ index ? '经' : '起' }}</span><strong>{{ segmentLocationName(route, 'departure') }}</strong><small>{{ route.departureAddress || '-' }}</small><small class="route-map__progress">{{ index ? `已到达 ${quantity(segments[index - 1].arrivedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(segments[index - 1])}` : `已调度 ${quantity(route.dispatchedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(route)}` }}</small><small v-if="index" class="route-map__progress">已调度 {{ quantity(route.dispatchedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div><div class="route-map__line"></div><div v-if="index === segments.length - 1" class="route-map__node"><span class="route-badge end">终</span><strong>{{ segmentLocationName(route, 'arrival') }}</strong><small>{{ route.arrivalAddress || '-' }}</small><small class="route-map__progress">已到达 {{ quantity(route.arrivedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div></template></div>
|
||||
<div class="route-map"><template v-for="(route, index) in segments" :key="route.segmentNo"><div class="route-map__node"><span :class="['route-badge', index ? 'middle' : 'start']">{{ index ? '经' : '起' }}</span><strong>{{ segmentLocationName(route, 'departure') }}</strong><small>{{ formatRegionAddress(route.departureAddress) || '-' }}</small><small class="route-map__progress">{{ index ? `已到达 ${quantity(segments[index - 1].arrivedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(segments[index - 1])}` : `已调度 ${quantity(route.dispatchedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(route)}` }}</small><small v-if="index" class="route-map__progress">已调度 {{ quantity(route.dispatchedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div><div class="route-map__line"></div><div v-if="index === segments.length - 1" class="route-map__node"><span class="route-badge end">终</span><strong>{{ segmentLocationName(route, 'arrival') }}</strong><small>{{ formatRegionAddress(route.arrivalAddress) || '-' }}</small><small class="route-map__progress">已到达 {{ quantity(route.arrivedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div></template></div>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="documentPreviewVisible" :title="previewFile.name || '附件预览'" append-to-body destroy-on-close width="90%" top="4vh">
|
||||
@@ -69,14 +82,21 @@
|
||||
<section v-if="master" class="master-goods-card">
|
||||
<div class="master-goods-card__heading"><h3>货物信息</h3></div>
|
||||
<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="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 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="specification" label="规格" min-width="150" />
|
||||
<el-table-column prop="model" label="型号" min-width="150" />
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
|
||||
<el-table-column prop="specification" label="规格" min-width="120" />
|
||||
<el-table-column prop="model" label="型号" min-width="120" />
|
||||
<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-empty v-if="!(master.goods || []).length" description="暂无货物信息" :image-size="56" />
|
||||
</section>
|
||||
@@ -90,6 +110,7 @@ import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archiv
|
||||
import { CaretBottom, CaretTop } from '@element-plus/icons-vue';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
import { formatRegionAddress } from '@/utils/region-address';
|
||||
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
|
||||
import '@open-file-viewer/core/style.css';
|
||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||
@@ -132,6 +153,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)}` : '-'; },
|
||||
},
|
||||
async mounted() {
|
||||
if (!this.id) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await api.getDetail(this.id);
|
||||
@@ -143,6 +165,7 @@ export default {
|
||||
} finally { this.loading = false; }
|
||||
},
|
||||
methods: {
|
||||
formatRegionAddress,
|
||||
quantity(value) { const number = Number(value || 0); return Number.isInteger(number) ? number : number.toFixed(3).replace(/\.?0+$/, ''); },
|
||||
segmentNumber(route, index) { return String(route.segmentNo || '').replace(/^段/, '') || index + 1; },
|
||||
isSegmentExpanded(segmentNo) { return Boolean(this.expandedSegments[segmentNo]); },
|
||||
@@ -282,9 +305,37 @@ export default {
|
||||
<style scoped lang="scss">
|
||||
.master-detail { padding-bottom: 20px; color: #303133; }
|
||||
.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; } }
|
||||
.transport-flow-tag { margin-left: 8px; }
|
||||
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #303133; font-size: 14px; word-break: break-all; } :deep(.el-link) { font-size: 14px; } }
|
||||
.detail-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;
|
||||
}
|
||||
|
||||
: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__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; }
|
||||
@@ -305,5 +356,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; } }
|
||||
.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 :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)); } }
|
||||
</style>
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
<div class="route-overview__node">
|
||||
<span :class="['route-badge', index === 0 ? 'start' : 'middle']">{{ index === 0 ? '起' : '经' }}</span>
|
||||
<el-tooltip :content="route.departureName || '-'" placement="top" :show-after="120">
|
||||
<strong>{{ route.departureName || '-' }}</strong>
|
||||
<strong>{{ dispatchLocationName(route, 'departure') }}</strong>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="route.departureAddress || '-'" placement="top" :show-after="120">
|
||||
<small>{{ route.departureAddress || '-' }}</small>
|
||||
<small>{{ formatRegionAddress(route.departureAddress) || '-' }}</small>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="contactText(route.departureContact, route.departurePhone)" placement="top" :show-after="120">
|
||||
<small>{{ contactText(route.departureContact, route.departurePhone) }}</small>
|
||||
@@ -34,10 +34,10 @@
|
||||
<div v-if="index === routes.length - 1" class="route-overview__node">
|
||||
<span class="route-badge end">终</span>
|
||||
<el-tooltip :content="route.arrivalName || '-'" placement="top" :show-after="120">
|
||||
<strong>{{ route.arrivalName || '-' }}</strong>
|
||||
<strong>{{ dispatchLocationName(route, 'arrival') }}</strong>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="route.arrivalAddress || '-'" placement="top" :show-after="120">
|
||||
<small>{{ route.arrivalAddress || '-' }}</small>
|
||||
<small>{{ formatRegionAddress(route.arrivalAddress) || '-' }}</small>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="contactText(route.arrivalContact, route.arrivalPhone)" placement="top" :show-after="120">
|
||||
<small>{{ contactText(route.arrivalContact, route.arrivalPhone) }}</small>
|
||||
@@ -51,9 +51,29 @@
|
||||
<section v-for="(route, index) in routes" :key="route.segmentNo" class="dispatch-section" :data-segment="route.segmentNo">
|
||||
<header class="segment-header">
|
||||
<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-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>{{ route.segmentNo }}:</span>
|
||||
<span class="segment-checkbox-no">{{ route.segmentNo }}:</span>
|
||||
<el-tooltip
|
||||
:content="`${dispatchLocationName(route, 'departure')} → ${dispatchLocationName(route, 'arrival')}`"
|
||||
placement="top"
|
||||
@@ -65,7 +85,15 @@
|
||||
</span>
|
||||
</el-checkbox>
|
||||
</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>
|
||||
<div v-show="route.selected" class="segment-content">
|
||||
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form">
|
||||
@@ -78,16 +106,28 @@
|
||||
<el-col :span="12">
|
||||
<el-form-item label="发货地址" required><el-input :model-value="addressText(route.departureName, route.departureAddress)" disabled /></el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4"><el-form-item label="联系人"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="4"><el-form-item label="联系方式"><el-input v-model="route.departurePhone" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="4"
|
||||
><el-form-item label="联系人" 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-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="收货地址" required><el-input :model-value="addressText(route.arrivalName, route.arrivalAddress)" disabled /></el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4"><el-form-item label="联系人"><el-input v-model="route.arrivalContact" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="4"><el-form-item label="联系方式"><el-input v-model="route.arrivalPhone" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="4"
|
||||
><el-form-item label="联系人" 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-row>
|
||||
</el-form>
|
||||
@@ -156,16 +196,59 @@
|
||||
<el-row :gutter="16">
|
||||
<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-autocomplete :ref="element => setDriverInput(route.segmentNo, element)" v-model="route.driverName" :debounce="300" :fetch-suggestions="fetchDriverSuggestions" clearable placeholder="请输入司机" :loading="driverLoading" @select="item => handleDriverSuggestionSelect(route, item)" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item :label="route.carrierType === '承运商' ? '手机号' : '司机手机号'" :required="route.carrierType !== '承运商'"><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="车牌号" 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" :debounce="300" :fetch-suggestions="fetchEscortSuggestions" clearable placeholder="请输入" :loading="escortLoading" @select="item => handleEscortSuggestionSelect(route, item)" /></el-form-item></el-col>
|
||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人手机号"><el-input v-model="route.escortPhone" placeholder="请输入" /></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"
|
||||
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 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><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.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>
|
||||
@@ -183,7 +266,7 @@
|
||||
<el-col :span="6">
|
||||
<el-form-item label="单价">
|
||||
<el-input :model-value="freightGroupUnitPrice(group)" inputmode="decimal" placeholder="请输入" @input="value => handleFreightGroupUnitPriceInput(group, value)" @clear="() => handleFreightGroupUnitPriceInput(group, '')">
|
||||
<template #append><el-select :model-value="freightGroupPriceUnit(group)" class="freight-unit-select" @change="value => handleFreightGroupPriceUnitChange(group, value)"><el-option label="元/吨" value="元/吨" /><el-option label="元/件" value="元/件" /><el-option label="元/方" value="元/方" /></el-select></template>
|
||||
<template #append><el-select :model-value="freightGroupPriceUnit(group)" class="freight-unit-select" @change="value => handleFreightGroupPriceUnitChange(group, value)"><el-option v-for="item in priceUnitOptions(route)" :key="item.value" :label="item.label" :value="item.value" /></el-select></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -233,7 +316,15 @@
|
||||
|
||||
<script>
|
||||
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 { formatRegionAddress } from '@/utils/region-address';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
|
||||
const unwrapRecords = res => {
|
||||
@@ -249,7 +340,24 @@ export default {
|
||||
props: { id: [String, Number] },
|
||||
emits: ['back'],
|
||||
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: {
|
||||
totalQuantity() { return (this.master?.goods || []).reduce((sum, item) => sum + Number(item.quantity || 0), 0); },
|
||||
@@ -290,11 +398,26 @@ export default {
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await Promise.all([this.load(), this.loadCarrierOptions(), this.loadDriverOptions()]);
|
||||
await this.loadQuantityUnitOptions();
|
||||
await Promise.all([this.load(), this.loadCarrierOptions()]);
|
||||
this.applySelfOperatedCarrierDefaults();
|
||||
},
|
||||
methods: {
|
||||
loadQuantityUnitOptions() {
|
||||
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
|
||||
this.quantityUnitLoading = true;
|
||||
this.quantityUnitLoadPromise = loadMeasurementUnitOptions()
|
||||
.then(options => {
|
||||
this.quantityUnitOptions = options.map(item => item.value);
|
||||
return this.quantityUnitOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.quantityUnitLoading = false;
|
||||
});
|
||||
return this.quantityUnitLoadPromise;
|
||||
},
|
||||
async load() {
|
||||
if (!this.id) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await api.getDetail(this.id);
|
||||
@@ -312,8 +435,10 @@ export default {
|
||||
},
|
||||
async loadContractCurrency() {
|
||||
const directCurrency = this.master?.settlementCurrency || this.master?.settlementCurrencyCode || this.master?.currency;
|
||||
const previousCurrency = this.master?.settlementCurrency;
|
||||
if (!this.master?.projectId || !this.master?.contractId) {
|
||||
this.master.settlementCurrency = this.normalizeCurrencyValue(directCurrency || 'RMB');
|
||||
this.syncRouteCurrency(previousCurrency, this.master.settlementCurrency);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -327,6 +452,20 @@ export default {
|
||||
this.master.customerContractPartyB = '';
|
||||
this.master.settlementCurrency = 'RMB';
|
||||
}
|
||||
this.syncRouteCurrency(previousCurrency, this.master.settlementCurrency);
|
||||
},
|
||||
syncRouteCurrency(_previousCurrency, nextCurrency) {
|
||||
const currency = this.normalizeCurrencyValue(nextCurrency || 'RMB');
|
||||
(this.routes || []).forEach(route => {
|
||||
route.currency = currency;
|
||||
(route.freightItems || []).forEach(item => {
|
||||
if (item.priceUnit) {
|
||||
item.priceUnit = remapPriceUnitCurrency(item.priceUnit, currency);
|
||||
} else if (item.quantityUnit) {
|
||||
item.priceUnit = resolvePriceUnitByQuantityUnit(item.quantityUnit, currency);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
dispatchRouteNodes() {
|
||||
const routes = Array.isArray(this.master?.routes) ? this.master.routes : [];
|
||||
@@ -452,12 +591,29 @@ export default {
|
||||
dispatchedQuantity(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) {
|
||||
return Math.max(
|
||||
0,
|
||||
this.totalQuantity -
|
||||
this.dispatchedQuantity(route)
|
||||
);
|
||||
const masterRemaining = Math.max(0, this.totalQuantity - this.dispatchedQuantity(route));
|
||||
const previousArrived = this.previousArrivedQuantity(route);
|
||||
if (previousArrived === null) return masterRemaining;
|
||||
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+$/, ''); },
|
||||
formatAmount(value) { return Number(value || 0).toFixed(2); },
|
||||
@@ -484,8 +640,23 @@ export default {
|
||||
const prices = (group.rows || []).map(item => String(item.unitPrice ?? '').trim()).filter(Boolean);
|
||||
return !prices.length || prices.some(price => price !== prices[0]) ? '' : prices[0];
|
||||
},
|
||||
freightGroupPriceUnit(group = {}) { return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : ''); },
|
||||
freightGroupPriceUnit(group = {}) {
|
||||
if (group.rows?.[0]?.priceUnit) return group.rows[0].priceUnit;
|
||||
if (!group.quantityUnit) return '';
|
||||
return (
|
||||
resolvePriceUnitByQuantityUnit(group.quantityUnit, this.master?.settlementCurrency || 'RMB') ||
|
||||
`${currencyUnit(this.master?.settlementCurrency || 'RMB')}/${group.quantityUnit}`
|
||||
);
|
||||
},
|
||||
freightGroupAmount(group = {}) { return (group.rows || []).reduce((sum, item) => sum + this.freightAmount(item), 0); },
|
||||
priceUnitOptions(route = {}) {
|
||||
const unit = currencyUnit(route.currency || this.master?.settlementCurrency || 'RMB');
|
||||
return [
|
||||
{ label: `${unit}/吨`, value: `${unit}/吨` },
|
||||
{ label: `${unit}/件`, value: `${unit}/件` },
|
||||
{ label: `${unit}/方`, value: `${unit}/方` },
|
||||
];
|
||||
},
|
||||
handleFreightGroupUnitPriceInput(group, value) {
|
||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||
const normalized = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||
@@ -495,6 +666,10 @@ export default {
|
||||
handleFreightGroupQuantityUnitChange(route, group, value) {
|
||||
(group.rows || []).forEach(item => {
|
||||
item.quantityUnit = value || '';
|
||||
item.priceUnit = resolvePriceUnitByQuantityUnit(
|
||||
value,
|
||||
route.currency || this.master?.settlementCurrency || 'RMB'
|
||||
) || item.priceUnit;
|
||||
const goods = (route.goods || []).find(row => String(row.sourceIndex) === String(item.sourceIndex));
|
||||
if (goods) goods.quantityUnit = value || '';
|
||||
});
|
||||
@@ -517,7 +692,7 @@ export default {
|
||||
})),
|
||||
});
|
||||
},
|
||||
currencyLabel(value) { return ({ CNY: '人民币', RMB: '人民币', USD: '美元', EUR: '欧元' })[value] || value || '人民币'; },
|
||||
currencyLabel(value) { return currencyUnit(value); },
|
||||
quantityNumber(value) { return Number(value || 0); },
|
||||
goodsKey(goods = {}, includeUnit = true) {
|
||||
return [
|
||||
@@ -562,13 +737,14 @@ export default {
|
||||
}, 0);
|
||||
},
|
||||
validatePreviousSegmentCompletedQuantity(route, goods) {
|
||||
const routeIndex = this.routes.indexOf(route);
|
||||
if (routeIndex <= 0) return true;
|
||||
const previousCompletedQuantity = this.quantityNumber(
|
||||
this.routes[routeIndex - 1]?.arrivedQuantity
|
||||
);
|
||||
const previousCompletedQuantity = this.previousArrivedQuantity(route);
|
||||
if (previousCompletedQuantity === null) return true;
|
||||
if (previousCompletedQuantity <= 0) {
|
||||
this.$message.warning(`${route.segmentNo}需等上一段运单完成到达后才能调度`);
|
||||
return false;
|
||||
}
|
||||
const currentDispatchQuantity = (goods || []).reduce(
|
||||
(sum, item) => sum + this.quantityNumber(item.dispatchQuantity),
|
||||
(sum, item) => sum + this.quantityNumber(item.dispatchQuantity ?? item.quantity),
|
||||
0
|
||||
);
|
||||
const dispatchTotal =
|
||||
@@ -577,7 +753,7 @@ export default {
|
||||
currentDispatchQuantity;
|
||||
if (dispatchTotal <= previousCompletedQuantity) return true;
|
||||
this.$message.warning(
|
||||
`${route.segmentNo}调度总量不能大于上一段已完成数量(${this.formatQuantity(previousCompletedQuantity)} ${this.quantityUnitLabel})`
|
||||
`${route.segmentNo}调度总量不能大于上一段已到达数量(${this.formatQuantity(previousCompletedQuantity)} ${this.quantityUnitLabel})`
|
||||
);
|
||||
return false;
|
||||
},
|
||||
@@ -676,7 +852,12 @@ export default {
|
||||
const mileage = Number(value);
|
||||
return Number.isFinite(mileage) && mileage >= 0 ? String(Math.trunc(mileage)) : '';
|
||||
},
|
||||
addressText(name, address) { return [name, address].filter(Boolean).join(' / ') || '-'; },
|
||||
formatRegionAddress,
|
||||
addressText(name, address) {
|
||||
// 名称与详细地址都要按「市 区县」展示;两者格式化后相同时只展示一次
|
||||
const values = [this.formatRegionAddress(name), this.formatRegionAddress(address)].filter(Boolean);
|
||||
return [...new Set(values)].join(' / ') || '-';
|
||||
},
|
||||
contactText(name, phone) { return [name, phone].filter(Boolean).join(' - ') || '-'; },
|
||||
dateStartLabel(route) { return route.documentType === '计划单' ? '计划开始日期' : '预计发货日期'; },
|
||||
dateEndLabel(route) { return route.documentType === '计划单' ? '计划结束日期' : '预计完成日期'; },
|
||||
@@ -706,6 +887,7 @@ export default {
|
||||
removeGoods(route, index) { route.goods.splice(index, 1); this.syncFreightItems(route); },
|
||||
syncFreightItems(route) {
|
||||
const oldItems = route.freightItems || [];
|
||||
const currency = route.currency || this.master?.settlementCurrency || 'RMB';
|
||||
route.freightItems = (route.goods || [])
|
||||
.filter(item => item.sourceIndex !== undefined && item.sourceIndex !== '')
|
||||
.map(item => {
|
||||
@@ -717,7 +899,10 @@ export default {
|
||||
quantity: item.dispatchQuantity || '',
|
||||
quantityUnit: item.quantityUnit || '',
|
||||
unitPrice: old.unitPrice || '',
|
||||
priceUnit: old.priceUnit || `元/${item.quantityUnit || '吨'}`,
|
||||
priceUnit:
|
||||
old.priceUnit ||
|
||||
resolvePriceUnitByQuantityUnit(item.quantityUnit || '吨', currency) ||
|
||||
`${currencyUnit(currency)}/${item.quantityUnit || '吨'}`,
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -726,6 +911,7 @@ export default {
|
||||
route.carrierName = '';
|
||||
route.carrierId = '';
|
||||
route.carrierContractId = '';
|
||||
this.clearRouteDriverVehicleFields(route);
|
||||
if (value === '承运商') {
|
||||
route.trailerVehicleNo = '';
|
||||
route.escortName = '';
|
||||
@@ -745,15 +931,35 @@ export default {
|
||||
route.carrierContractId = route.carrierType !== '自运' ? contract?.contractId || '' : '';
|
||||
route.carrierId = route.carrierType !== '自运' ? contract?.carrierId || '' : '';
|
||||
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 = {}) {
|
||||
return item.contractId || item.id || item.value || item.carrierName || '';
|
||||
},
|
||||
carrierOptionLabel(item) {
|
||||
const duplicateCount = this.carrierOptions.filter(option => option.carrierName === item.carrierName).length;
|
||||
return duplicateCount > 1
|
||||
? `${item.carrierName}(${item.contractName || item.contractId})`
|
||||
: item.carrierName;
|
||||
if (duplicateCount <= 1) {
|
||||
return item.carrierName;
|
||||
}
|
||||
const contractLabel = formatContractOptionLabel(item);
|
||||
return contractLabel ? `${item.carrierName}(${contractLabel})` : item.carrierName;
|
||||
},
|
||||
driverOptionLabel(item) { return item.driverName || item.name || ''; },
|
||||
driverOptionKey(item) { return item.id || item.driverId || this.driverOptionLabel(item) || item.mobile; },
|
||||
@@ -816,29 +1022,57 @@ export default {
|
||||
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;
|
||||
try {
|
||||
this.driverOptions = unwrapRecords(
|
||||
await getDriverList(1, 20, { driverName: keyword, posts: '司机' })
|
||||
this.driverOptions = await fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
);
|
||||
return this.driverOptions;
|
||||
} finally {
|
||||
this.driverLoading = false;
|
||||
}
|
||||
},
|
||||
fetchDriverSuggestions(queryString, callback) {
|
||||
this.loadDriverOptions(String(queryString || '').trim())
|
||||
.then(() =>
|
||||
const route = this.activeSuggestRoute || {};
|
||||
const carrier = this.getRouteCarrierFilter(route);
|
||||
const keyword = String(queryString || '').trim();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
if (!keyword) this.$message.warning('请先选择承运商');
|
||||
return;
|
||||
}
|
||||
this.driverLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
)
|
||||
.then(records => {
|
||||
this.driverOptions = records;
|
||||
callback(
|
||||
this.driverOptions.map(item => ({
|
||||
records.map(item => ({
|
||||
...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.driverId = item.id || item.driverId || '';
|
||||
route.driverPhone = item.mobile || item.driverPhone || item.phone || route.driverPhone || '';
|
||||
@@ -846,12 +1080,22 @@ export default {
|
||||
if (drivingVehicle) route.vehicleNo = drivingVehicle;
|
||||
},
|
||||
fetchEscortSuggestions(queryString, callback) {
|
||||
const route = this.activeSuggestRoute || {};
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getRouteCarrierFilter(route);
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
if (!keyword) this.$message.warning('请先选择承运商');
|
||||
return;
|
||||
}
|
||||
this.escortLoading = true;
|
||||
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '押运员' })
|
||||
.then(res => {
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
|
||||
carrier
|
||||
)
|
||||
.then(records => {
|
||||
callback(
|
||||
unwrapRecords(res).map(item => ({
|
||||
records.map(item => ({
|
||||
...item,
|
||||
value: this.driverOptionLabel(item),
|
||||
}))
|
||||
@@ -862,7 +1106,9 @@ export default {
|
||||
this.escortLoading = false;
|
||||
});
|
||||
},
|
||||
handleEscortSuggestionSelect(route, item = {}) {
|
||||
handleEscortSuggestionSelect(item = {}) {
|
||||
const route = this.activeSuggestRoute;
|
||||
if (!route) return;
|
||||
route.escortName = this.driverOptionLabel(item);
|
||||
const phone = item.mobile || item.driverPhone || item.phone || '';
|
||||
if (phone) route.escortPhone = phone;
|
||||
@@ -887,6 +1133,10 @@ export default {
|
||||
addToPending(route, continueDispatch) {
|
||||
const goods = route.goods.filter(item => Number(item.dispatchQuantity) > 0);
|
||||
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;
|
||||
this.syncFreightItems(route);
|
||||
if (!this.validateFreightItems(route)) return;
|
||||
@@ -1001,9 +1251,11 @@ export default {
|
||||
.pending-list-spacer { height: min(436px, calc(100vh - 130px)); }
|
||||
.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; }
|
||||
.segment-header :deep(.el-checkbox.is-checked .el-checkbox__label) { font-size: 16px; font-weight: 700; }
|
||||
.segment-checkbox-label { display: inline-flex; align-items: center; min-width: 0; }
|
||||
.segment-checkbox-path { display: inline-block; white-space: nowrap; }
|
||||
.segment-checkbox-wrap { display: inline-flex; max-width: 100%; }
|
||||
.segment-checkbox-label { display: inline-flex; align-items: center; min-width: 0; font-size: 14px; font-weight: 400; color: #303133; }
|
||||
/* 仅「段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-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; } }
|
||||
@@ -1014,7 +1266,13 @@ export default {
|
||||
.route-overview__line { flex: 0 1 56px; min-width: 24px; height: 32px; margin-top: 0; border-bottom: 6px solid #e4e7ed; }
|
||||
.segment-header { min-height: 46px; padding: 0 20px; border-bottom: 1px solid #eff1f7; font-weight: 600; em { color: #409eff; font-style: normal; } }
|
||||
.segment-content { padding: 16px 24px; }
|
||||
.dispatch-form { :deep(.el-form-item) { margin-bottom: 14px; } :deep(.el-input), :deep(.el-select), :deep(.el-date-editor), :deep(.el-input-number) { width: 100%; } :deep(.el-input-group__append .el-select) { width: auto; } }
|
||||
.dispatch-form { :deep(.el-form-item) { margin-bottom: 24px; } :deep(.el-input), :deep(.el-select), :deep(.el-date-editor), :deep(.el-input-number) { width: 100%; } :deep(.el-input-group__append .el-select) { width: auto; } }
|
||||
/* 短标签按自身宽度,避免被「预计发货日期」等长标签把左侧撑开 */
|
||||
.dispatch-form__compact-label {
|
||||
:deep(.el-form-item__label) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
.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; } }
|
||||
.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
|
||||
v-model="form.projectId"
|
||||
filterable
|
||||
:disabled="isBasicRouteReadonly"
|
||||
placeholder="请输入并选择"
|
||||
@change="projectChange"
|
||||
><el-option
|
||||
@@ -20,13 +21,13 @@
|
||||
><el-form-item label="客户合同" prop="contractName"
|
||||
><el-select
|
||||
v-model="form.contractId"
|
||||
:disabled="!form.projectId"
|
||||
:disabled="isBasicRouteReadonly || !form.projectId"
|
||||
placeholder="选择项目后带出"
|
||||
@change="contractChange"
|
||||
><el-option
|
||||
v-for="contract in contracts"
|
||||
:key="contract.id"
|
||||
:label="contract.contractName"
|
||||
:label="formatContractOptionLabel(contract)"
|
||||
:value="contract.id" /></el-select></el-form-item></el-col
|
||||
><el-col :span="6"
|
||||
><el-form-item label="总单号" required
|
||||
@@ -41,6 +42,7 @@
|
||||
><el-form-item label="备注"
|
||||
><el-input
|
||||
v-model="form.remark"
|
||||
:disabled="isBasicRouteReadonly"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入" /></el-form-item></el-col></el-row
|
||||
@@ -49,7 +51,7 @@
|
||||
<section>
|
||||
<div class="section-head">
|
||||
<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="openRouteDialog">选择线路</el-link>
|
||||
</div>
|
||||
@@ -62,13 +64,13 @@
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="发货地" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-if="isNonRoadAddress('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 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 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 label="计划开始" class="route-date-form-item"
|
||||
><el-date-picker
|
||||
@@ -76,6 +78,7 @@
|
||||
type="date"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
:disabled="isBasicRouteReadonly"
|
||||
placeholder="请选择"
|
||||
/></el-form-item>
|
||||
</div>
|
||||
@@ -83,7 +86,7 @@
|
||||
</div>
|
||||
<template v-for="(route, index) in form.routes" :key="index">
|
||||
<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 class="route-flow-row">
|
||||
<span class="route-flow-marker route-flow-marker--middle">经</span>
|
||||
@@ -91,16 +94,16 @@
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="途经地" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="!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 label="联系人" class="route-contact-form-item"><el-input v-model="route.departureContact" 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-contact-form-item"><el-input v-model="route.departureContact" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
|
||||
<el-form-item label="联系方式" class="route-phone-form-item"><div class="route-contact-actions"><el-input v-model="route.departurePhone" :disabled="isBasicRouteReadonly" placeholder="请输入" /><template v-if="!isBasicRouteReadonly"><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></template></div></el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<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 class="route-flow-row">
|
||||
<span class="route-flow-marker route-flow-marker--end">终</span>
|
||||
@@ -108,18 +111,19 @@
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="收货地" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="!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 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-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-date-picker
|
||||
v-model="form.planEndTime"
|
||||
type="date"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
:disabled="isBasicRouteReadonly"
|
||||
placeholder="请选择"
|
||||
/></el-form-item>
|
||||
</div>
|
||||
@@ -144,6 +148,7 @@
|
||||
><el-autocomplete
|
||||
v-model="row.cargoName"
|
||||
clearable
|
||||
:disabled="isGoodsCategoryLocked(row)"
|
||||
placeholder="请输入货物名称"
|
||||
:debounce="300"
|
||||
:fetch-suggestions="
|
||||
@@ -168,22 +173,29 @@
|
||||
:placeholder="row.cargoType || '请选择'"
|
||||
clearable
|
||||
filterable
|
||||
:disabled="isGoodsCategoryLocked(row)"
|
||||
:loading="cargoTypeLoading"
|
||||
@visible-change="visible => visible && loadCargoTypeOptions()"
|
||||
@change="value => handleCargoTypeChange(row, value)"
|
||||
/></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 #default="{ row }"
|
||||
><el-input
|
||||
v-model="row.quantity"
|
||||
inputmode="decimal"
|
||||
placeholder="请输入"
|
||||
@input="value => handleQuantityInput(row, value)" /></template></el-table-column
|
||||
><div class="master-editor__quantity-field"
|
||||
><el-input
|
||||
v-model="row.quantity"
|
||||
inputmode="decimal"
|
||||
placeholder="请输入"
|
||||
@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"
|
||||
><template #header><span>数量单位<span class="required-asterisk">*</span></span></template>
|
||||
<template #default="{ row }"
|
||||
><el-select v-model="row.quantityUnit"
|
||||
><el-select v-model="row.quantityUnit" :disabled="isGoodsCategoryLocked(row)"
|
||||
><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"
|
||||
><template #default="{ row }"
|
||||
@@ -218,7 +230,7 @@
|
||||
>新增</el-link
|
||||
><el-link
|
||||
type="danger"
|
||||
:disabled="form.goods.length === 1"
|
||||
:disabled="form.goods.length === 1 || isGoodsCategoryLocked(form.goods[$index])"
|
||||
@click="removeGoods($index)"
|
||||
>删除</el-link
|
||||
></div
|
||||
@@ -249,11 +261,22 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="240" align="center" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="240"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220" align="center">
|
||||
@@ -272,10 +295,9 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
||||
<el-table-column prop="uploadTime" label="上传时间" width="170" align="center" sortable />
|
||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ row, $index }">
|
||||
<el-table-column label="操作" width="80" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-link type="danger" @click="removeAttachment($index)">删除</el-link>
|
||||
<el-link type="primary" @click="downloadAttachment(row)">下载</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -402,8 +424,8 @@
|
||||
>
|
||||
</div>
|
||||
<div class="map-picker-content">
|
||||
<map-search-results :results="mapSearchResults" @select="selectAddressMapSearchResult" />
|
||||
<div ref="addressMap" class="address-map" />
|
||||
<map-search-results :results="mapSearchResults" @select="selectAddressMapSearchResult" />
|
||||
</div>
|
||||
<div class="address-map-status">
|
||||
{{ mapStatus
|
||||
@@ -578,6 +600,7 @@ import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||
import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
|
||||
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
||||
import { exportBlob, importBlob } from '@/api/common';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { OfficeBuilding, Search } from '@element-plus/icons-vue';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
@@ -586,10 +609,10 @@ import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from
|
||||
import '@open-file-viewer/core/style.css';
|
||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||
|
||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||
const quantityUnitOptions = ['吨', '件', '方'];
|
||||
const attachmentViewerPlugins = [
|
||||
imagePlugin(),
|
||||
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
|
||||
@@ -601,6 +624,7 @@ let amapLoader;
|
||||
import { getList as getCommonRouteList } from '@/api/business/common-route';
|
||||
import * as api from '@/api/business/master-order';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
export default {
|
||||
components: {
|
||||
@@ -691,10 +715,11 @@ export default {
|
||||
'zip',
|
||||
],
|
||||
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
|
||||
quantityUnitOptions,
|
||||
quantityUnitOptions: [],
|
||||
quantityUnitLoading: false,
|
||||
form: {
|
||||
routes: [{ segmentNo: '段1', transportType: '' }],
|
||||
goods: [{ quantityUnit: quantityUnitOptions[0] }],
|
||||
goods: [{ quantityUnit: '吨' }],
|
||||
attachmentsJson: '[]',
|
||||
transportOrganizationType: '多式联运',
|
||||
},
|
||||
@@ -706,6 +731,31 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...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() {
|
||||
return this.form.routes[this.form.routes.length - 1]?.departureName || '途经地';
|
||||
},
|
||||
@@ -736,6 +786,7 @@ export default {
|
||||
async mounted() {
|
||||
this.loading = true;
|
||||
try {
|
||||
await this.loadQuantityUnitOptions();
|
||||
await Promise.all([this.loadProjects(), this.loadCargoTypeOptions(), this.loadRegionOptions()]);
|
||||
if (this.id) {
|
||||
await this.loadDetail(this.id);
|
||||
@@ -745,6 +796,23 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
defaultQuantityUnit() {
|
||||
return this.quantityUnitOptions[0] || '吨';
|
||||
},
|
||||
loadQuantityUnitOptions() {
|
||||
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
|
||||
this.quantityUnitLoading = true;
|
||||
this.quantityUnitLoadPromise = loadMeasurementUnitOptions()
|
||||
.then(options => {
|
||||
this.quantityUnitOptions = options.map(item => item.value);
|
||||
return this.quantityUnitOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.quantityUnitLoading = false;
|
||||
});
|
||||
return this.quantityUnitLoadPromise;
|
||||
},
|
||||
responseData(response) {
|
||||
return response?.data?.data || response?.data || response || {};
|
||||
},
|
||||
@@ -921,7 +989,7 @@ export default {
|
||||
finalTransportType: hasFinalSegment
|
||||
? finalSegment.transportType
|
||||
: detail.finalTransportType || '',
|
||||
goods: detail.goods || [{ quantityUnit: quantityUnitOptions[0] }],
|
||||
goods: detail.goods || [{ quantityUnit: this.defaultQuantityUnit() }],
|
||||
attachmentsJson: detail.attachmentsJson || '[]',
|
||||
transportOrganizationType: '多式联运',
|
||||
};
|
||||
@@ -1104,6 +1172,7 @@ export default {
|
||||
return row.regionCode || row.districtCode || row.areaCode || row.adcode || '';
|
||||
},
|
||||
openCommonAddress(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
this.addressTarget = target;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.stationQuery = {};
|
||||
@@ -1209,6 +1278,7 @@ export default {
|
||||
this.loadRegionOptions().then(() => this.syncAddressRegionPath(this.addressTarget));
|
||||
},
|
||||
handleAddressDetailClick(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.openCommonAddress(target);
|
||||
return;
|
||||
@@ -1216,6 +1286,7 @@ export default {
|
||||
this.openAddressMap(target);
|
||||
},
|
||||
openAddressMap(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.$message.info('非公路运输地址不能使用地图选择');
|
||||
return;
|
||||
@@ -1273,30 +1344,31 @@ export default {
|
||||
.catch(() => this.$message.error('高德地图组件加载失败'));
|
||||
},
|
||||
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()
|
||||
.then(() => this.ensureAddressGeocoder())
|
||||
.then(
|
||||
() =>
|
||||
new Promise((resolve, reject) =>
|
||||
this.addressMapGeocoder.getLocation(this.mapKeyword, (status, result) =>
|
||||
status === 'complete' ? resolve(result) : reject(result)
|
||||
)
|
||||
)
|
||||
)
|
||||
.then(result => {
|
||||
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);
|
||||
.then(() => searchAmapPlaceText(keyword))
|
||||
.then(pois => {
|
||||
this.mapSearchResults = normalizeMapSearchResults(pois, keyword);
|
||||
const first = this.mapSearchResults[0];
|
||||
if (!first?.location) {
|
||||
this.mapStatus = '未找到匹配地址';
|
||||
return this.$message.warning('地图搜索无匹配地址');
|
||||
}
|
||||
return this.$nextTick().then(() => {
|
||||
this.pickAddressMap(first.location, first.address || first.name || keyword);
|
||||
});
|
||||
})
|
||||
.catch(() => this.$message.error('地图搜索失败'));
|
||||
.catch(() => {
|
||||
this.mapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
})
|
||||
.finally(() => {
|
||||
this.mapLoading = false;
|
||||
});
|
||||
},
|
||||
selectAddressMapSearchResult(item) {
|
||||
if (item && item.location) {
|
||||
@@ -1618,7 +1690,7 @@ export default {
|
||||
'',
|
||||
cargoTypePath,
|
||||
quantity: row.quantity || '',
|
||||
quantityUnit: row.quantityUnit || quantityUnitOptions[0],
|
||||
quantityUnit: row.quantityUnit || this.defaultQuantityUnit(),
|
||||
packageType: row.packageType || row.package || '',
|
||||
brand: row.brand || '',
|
||||
specification: row.specification || row.spec || '',
|
||||
@@ -1662,6 +1734,7 @@ export default {
|
||||
});
|
||||
},
|
||||
openRouteDialog() {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
this.routeQuery = {};
|
||||
this.routePage.current = 1;
|
||||
this.routeDialogVisible = true;
|
||||
@@ -1719,21 +1792,44 @@ export default {
|
||||
return { segmentNo: `段${index + 1}`, transportType: '' };
|
||||
},
|
||||
addRoute() {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
this.form.routes.push(this.createRoute());
|
||||
},
|
||||
removeRoute(index) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
this.form.routes.splice(index, 1);
|
||||
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) {
|
||||
this.form.goods.splice(index + 1, 0, {
|
||||
quantityUnit: quantityUnitOptions[0],
|
||||
quantityUnit: this.defaultQuantityUnit(),
|
||||
materialCode: '',
|
||||
deviceCode: '',
|
||||
remark: '',
|
||||
});
|
||||
},
|
||||
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);
|
||||
},
|
||||
handleQuantityInput(row, value) {
|
||||
@@ -1741,6 +1837,42 @@ export default {
|
||||
const [integer = '', ...decimals] = text.split('.');
|
||||
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() {
|
||||
this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
|
||||
},
|
||||
@@ -1863,6 +1995,7 @@ export default {
|
||||
return this.$message.warning(`请完整填写第${invalidRouteIndex + 1}段途经地及运输方式`);
|
||||
if (!this.form.finalTransportType) return this.$message.warning('请填写最后一段运输方式');
|
||||
}
|
||||
if (!this.validateGoodsDispatchedQuantity()) return false;
|
||||
const payload = {
|
||||
...this.form,
|
||||
planStartTime: this.normalizeDateTimeValue(this.form.planStartTime),
|
||||
@@ -1945,9 +2078,19 @@ export default {
|
||||
font-size: 16px;
|
||||
}
|
||||
.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 {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
@@ -2199,8 +2342,15 @@ export default {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.el-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
.address-map {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 440px;
|
||||
}
|
||||
.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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user