Merge branch 'master' into feature/bfi-api-rate
# Conflicts: # src/components/vehicle-attachment-table/main.vue # src/option/payment/paymentApplication.js # src/views/payment/payment-application.vue # src/views/payment/receipt-claim-record.vue # src/views/vehicle/customer-archive.vue
This commit is contained in:
@@ -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。
|
||||
|
||||
Generated
+41
-72
@@ -4,9 +4,6 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
patchedDependencies:
|
||||
'@smallwei/avue': 5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -31,16 +28,16 @@ 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))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)
|
||||
version: 3.9.2(@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
|
||||
avue-plugin-ueditor:
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4(axios@1.16.0(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34)
|
||||
version: 1.0.4(axios@1.16.0)(vue@3.5.34)
|
||||
axios:
|
||||
specifier: ^1.13.5
|
||||
version: 1.16.0(debug@4.4.3(supports-color@7.2.0))
|
||||
version: 1.16.0
|
||||
codemirror:
|
||||
specifier: ^5.65.21
|
||||
version: 5.65.21
|
||||
@@ -128,7 +125,7 @@ importers:
|
||||
version: 5.4.21(sass@1.99.0)(terser@5.46.2)
|
||||
vite-plugin-compression:
|
||||
specifier: ^0.5.1
|
||||
version: 0.5.1(supports-color@7.2.0)(vite@5.4.21(sass@1.99.0)(terser@5.46.2))
|
||||
version: 0.5.1(vite@5.4.21(sass@1.99.0)(terser@5.46.2))
|
||||
vite-plugin-vue-setup-extend:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(vite@5.4.21(sass@1.99.0)(terser@5.46.2))
|
||||
@@ -407,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==}
|
||||
@@ -511,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==}
|
||||
@@ -613,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==}
|
||||
@@ -2654,10 +2627,10 @@ snapshots:
|
||||
countup.js: 2.10.0
|
||||
signature_pad: 5.1.3
|
||||
|
||||
'@smallwei/avue@3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)':
|
||||
'@smallwei/avue@3.9.2(@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(debug@4.4.3(supports-color@7.2.0))
|
||||
axios: 0.21.4
|
||||
countup.js: 1.9.3
|
||||
dayjs: 1.11.20
|
||||
element-plus: 2.14.2(vue@3.5.34)
|
||||
@@ -2793,25 +2766,25 @@ snapshots:
|
||||
dependencies:
|
||||
vue: 3.5.34
|
||||
|
||||
'@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
'@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)':
|
||||
dependencies:
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
|
||||
dom7: 3.0.0
|
||||
is-url: 1.2.4
|
||||
lodash.throttle: 4.1.1
|
||||
nanoid: 5.1.11
|
||||
slate: 0.123.0
|
||||
nanoid: 3.3.12
|
||||
slate: 0.72.8
|
||||
snabbdom: 3.6.3
|
||||
|
||||
'@wangeditor-next/code-highlight@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
'@wangeditor-next/code-highlight@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
dependencies:
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
|
||||
dom7: 4.0.6
|
||||
prismjs: 1.30.0
|
||||
slate: 0.123.0
|
||||
snabbdom: 3.6.3
|
||||
|
||||
'@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
'@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)':
|
||||
dependencies:
|
||||
'@types/event-emitter': 0.3.5
|
||||
'@uppy/core': 2.3.4
|
||||
@@ -2827,9 +2800,9 @@ snapshots:
|
||||
lodash.foreach: 4.5.0
|
||||
lodash.throttle: 4.1.1
|
||||
lodash.toarray: 4.4.0
|
||||
nanoid: 5.1.11
|
||||
nanoid: 3.3.12
|
||||
scroll-into-view-if-needed: 3.1.0
|
||||
slate: 0.123.0
|
||||
slate: 0.72.8
|
||||
slate-history: 0.115.0(slate@0.123.0)
|
||||
snabbdom: 3.6.3
|
||||
|
||||
@@ -2837,13 +2810,13 @@ snapshots:
|
||||
dependencies:
|
||||
'@uppy/core': 2.3.4
|
||||
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
|
||||
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/code-highlight': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/list-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/table-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/upload-image-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/video-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/code-highlight': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/list-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/table-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/upload-image-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/video-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
dom7: 4.0.6
|
||||
is-hotkey: 0.2.0
|
||||
lodash.camelcase: 4.3.0
|
||||
@@ -2856,16 +2829,16 @@ snapshots:
|
||||
slate: 0.123.0
|
||||
snabbdom: 3.6.3
|
||||
|
||||
'@wangeditor-next/list-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
'@wangeditor-next/list-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
dependencies:
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
|
||||
dom7: 4.0.6
|
||||
slate: 0.123.0
|
||||
snabbdom: 3.6.3
|
||||
|
||||
'@wangeditor-next/table-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
'@wangeditor-next/table-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
dependencies:
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
|
||||
dom7: 4.0.6
|
||||
lodash.debounce: 4.0.8
|
||||
lodash.throttle: 4.1.1
|
||||
@@ -2873,22 +2846,22 @@ snapshots:
|
||||
slate: 0.123.0
|
||||
snabbdom: 3.6.3
|
||||
|
||||
'@wangeditor-next/upload-image-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
'@wangeditor-next/upload-image-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
dependencies:
|
||||
'@uppy/core': 2.3.4
|
||||
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
|
||||
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
|
||||
dom7: 4.0.6
|
||||
lodash.foreach: 4.5.0
|
||||
slate: 0.123.0
|
||||
snabbdom: 3.6.3
|
||||
|
||||
'@wangeditor-next/video-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
'@wangeditor-next/video-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
dependencies:
|
||||
'@uppy/core': 2.3.4
|
||||
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
|
||||
dom7: 4.0.6
|
||||
nanoid: 5.1.11
|
||||
slate: 0.123.0
|
||||
@@ -3065,22 +3038,22 @@ snapshots:
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
avue-plugin-ueditor@1.0.4(axios@1.16.0(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34):
|
||||
avue-plugin-ueditor@1.0.4(axios@1.16.0)(vue@3.5.34):
|
||||
dependencies:
|
||||
'@wangeditor/editor': 5.1.23
|
||||
'@wangeditor/editor-for-vue': 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34)
|
||||
axios: 1.16.0(debug@4.4.3(supports-color@7.2.0))
|
||||
axios: 1.16.0
|
||||
vue: 3.5.34
|
||||
|
||||
axios@0.21.4(debug@4.4.3(supports-color@7.2.0)):
|
||||
axios@0.21.4:
|
||||
dependencies:
|
||||
follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0))
|
||||
follow-redirects: 1.16.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
axios@1.16.0(debug@4.4.3(supports-color@7.2.0)):
|
||||
axios@1.16.0:
|
||||
dependencies:
|
||||
follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0))
|
||||
follow-redirects: 1.16.0
|
||||
form-data: 4.0.5
|
||||
proxy-from-env: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
@@ -3248,11 +3221,9 @@ snapshots:
|
||||
|
||||
dayjs@1.11.20: {}
|
||||
|
||||
debug@4.4.3(supports-color@7.2.0):
|
||||
debug@4.4.3:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 7.2.0
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
@@ -3460,9 +3431,7 @@ snapshots:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
||||
follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)):
|
||||
optionalDependencies:
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
follow-redirects@1.16.0: {}
|
||||
|
||||
form-data@4.0.5:
|
||||
dependencies:
|
||||
@@ -4157,10 +4126,10 @@ snapshots:
|
||||
|
||||
uuid@8.3.2: {}
|
||||
|
||||
vite-plugin-compression@0.5.1(supports-color@7.2.0)(vite@5.4.21(sass@1.99.0)(terser@5.46.2)):
|
||||
vite-plugin-compression@0.5.1(vite@5.4.21(sass@1.99.0)(terser@5.46.2)):
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3
|
||||
fs-extra: 10.1.0
|
||||
vite: 5.4.21(sass@1.99.0)(terser@5.46.2)
|
||||
transitivePeerDependencies:
|
||||
|
||||
+3
-3
@@ -2,9 +2,9 @@ packages:
|
||||
- '.'
|
||||
|
||||
allowBuilds:
|
||||
'@parcel/watcher': set this to true or false
|
||||
es5-ext: set this to true or false
|
||||
esbuild: set this to true or false
|
||||
'@parcel/watcher': true
|
||||
es5-ext: true
|
||||
esbuild: true
|
||||
|
||||
patchedDependencies:
|
||||
'@smallwei/avue': patches/@smallwei__avue.patch
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
+12
-2
@@ -57,26 +57,36 @@ export const clearNonTopDept = signal => {
|
||||
});
|
||||
};
|
||||
|
||||
export const syncOaCompany = (current = 1, size = 20, signal) => {
|
||||
export const syncOaCompany = (current = 1, size = 20, signal, incremental = false, modifiedSince) => {
|
||||
return request({
|
||||
url: '/blade-system/dept/sync-oa-company',
|
||||
method: 'post',
|
||||
params: {
|
||||
current,
|
||||
size,
|
||||
incremental: !!incremental,
|
||||
modifiedSince: modifiedSince || undefined,
|
||||
},
|
||||
timeout: 60000,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const syncOaDepartment = (current = 1, size = 20, signal) => {
|
||||
export const syncOaDepartment = (
|
||||
current = 1,
|
||||
size = 20,
|
||||
signal,
|
||||
incremental = false,
|
||||
modifiedSince
|
||||
) => {
|
||||
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;
|
||||
|
||||
@@ -16,7 +16,11 @@
|
||||
<div class="address-map-picker__info">{{ status }}</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selected.longitude || resolving" @click="confirm">
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!hasCoordinateValue(selected.longitude) || !hasCoordinateValue(selected.latitude) || resolving"
|
||||
@click="confirm"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -271,7 +275,12 @@ export default {
|
||||
this.runGeocode('address', lnglat)
|
||||
.then(result => {
|
||||
const detailAddress = result.regeocode?.formattedAddress || fallbackAddress;
|
||||
this.selected = { ...this.selected, address: detailAddress };
|
||||
const region = this.resolveRegionFromGeocode(result);
|
||||
this.selected = {
|
||||
...this.selected,
|
||||
address: detailAddress,
|
||||
...region,
|
||||
};
|
||||
this.keyword = detailAddress || this.keyword;
|
||||
this.status = detailAddress || '已选点,可确认回填';
|
||||
})
|
||||
@@ -285,6 +294,26 @@ export default {
|
||||
this.resolving = false;
|
||||
});
|
||||
},
|
||||
resolveRegionFromGeocode(result = {}) {
|
||||
const component = result.regeocode?.addressComponent || {};
|
||||
const city = Array.isArray(component.city) ? '' : String(component.city || '').trim();
|
||||
const province = String(component.province || '').trim();
|
||||
const district = String(component.district || '').trim();
|
||||
const country = String(component.country || '').trim();
|
||||
const adcode = String(component.adcode || '').trim();
|
||||
return {
|
||||
country,
|
||||
province,
|
||||
city: city || province,
|
||||
district,
|
||||
adcode,
|
||||
regionCode: adcode,
|
||||
provinceName: province,
|
||||
cityName: city || province,
|
||||
districtName: district,
|
||||
countryName: country,
|
||||
};
|
||||
},
|
||||
renderMarker(point) {
|
||||
this.clearMarker();
|
||||
this.marker = new window.AMap.Marker({ position: point });
|
||||
@@ -297,8 +326,31 @@ export default {
|
||||
this.marker = null;
|
||||
}
|
||||
},
|
||||
syncSelectedFromMarker() {
|
||||
// 确认时以当前标记点坐标为准,避免换地址后仍回填上一次经纬度
|
||||
if (!this.marker || typeof this.marker.getPosition !== 'function') {
|
||||
return;
|
||||
}
|
||||
const point = this.marker.getPosition();
|
||||
if (!point) {
|
||||
return;
|
||||
}
|
||||
const longitude =
|
||||
typeof point.getLng === 'function' ? point.getLng() : point.lng ?? point.longitude;
|
||||
const latitude =
|
||||
typeof point.getLat === 'function' ? point.getLat() : point.lat ?? point.latitude;
|
||||
if (!Number.isFinite(Number(longitude)) || !Number.isFinite(Number(latitude))) {
|
||||
return;
|
||||
}
|
||||
this.selected = {
|
||||
...this.selected,
|
||||
longitude: Number(longitude),
|
||||
latitude: Number(latitude),
|
||||
};
|
||||
},
|
||||
confirm() {
|
||||
if (!this.selected.longitude || !this.selected.latitude) {
|
||||
this.syncSelectedFromMarker();
|
||||
if (!this.hasCoordinateValue(this.selected.longitude) || !this.hasCoordinateValue(this.selected.latitude)) {
|
||||
this.$message.warning('请先搜索或点击地图完成选点');
|
||||
return;
|
||||
}
|
||||
@@ -308,8 +360,18 @@ export default {
|
||||
}
|
||||
this.$emit('confirm', {
|
||||
address: this.selected.address,
|
||||
longitude: this.selected.longitude,
|
||||
latitude: this.selected.latitude,
|
||||
longitude: Number(this.selected.longitude),
|
||||
latitude: Number(this.selected.latitude),
|
||||
country: this.selected.country || this.selected.countryName || '',
|
||||
province: this.selected.province || this.selected.provinceName || '',
|
||||
city: this.selected.city || this.selected.cityName || '',
|
||||
district: this.selected.district || this.selected.districtName || '',
|
||||
adcode: this.selected.adcode || this.selected.regionCode || '',
|
||||
regionCode: this.selected.regionCode || this.selected.adcode || '',
|
||||
countryName: this.selected.countryName || this.selected.country || '',
|
||||
provinceName: this.selected.provinceName || this.selected.province || '',
|
||||
cityName: this.selected.cityName || this.selected.city || '',
|
||||
districtName: this.selected.districtName || this.selected.district || '',
|
||||
});
|
||||
this.visible = false;
|
||||
},
|
||||
|
||||
@@ -576,7 +576,7 @@ export default {
|
||||
}
|
||||
|
||||
.mode-content :deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mode-content :deep(.el-form-item:last-child) {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="avue-crud__right table-header-tools">
|
||||
<el-button v-if="showRefresh" icon="el-icon-refresh" circle @click="$emit('refresh')" />
|
||||
<el-button
|
||||
v-if="showColumn"
|
||||
icon="el-icon-operation"
|
||||
circle
|
||||
@click="columnSettingVisible = true"
|
||||
/>
|
||||
<el-button
|
||||
v-if="showSearch"
|
||||
icon="el-icon-search"
|
||||
circle
|
||||
@click="$emit('update:searchVisible', !searchVisible)"
|
||||
/>
|
||||
<el-button v-if="showGrid" icon="el-icon-grid" circle @click="toggleTableSize" />
|
||||
|
||||
<el-dialog
|
||||
v-if="showColumn"
|
||||
v-model="columnSettingVisible"
|
||||
title="列设置"
|
||||
width="360px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<el-checkbox-group
|
||||
class="table-header-tools__column-setting"
|
||||
:model-value="visibleColumnKeys"
|
||||
@update:model-value="$emit('update:visibleColumnKeys', $event)"
|
||||
>
|
||||
<el-checkbox v-for="item in columns" :key="item.prop" :label="item.prop">
|
||||
{{ item.label }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<template #footer>
|
||||
<el-button @click="columnSettingVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const TABLE_SIZE_OPTIONS = ['default', 'large', 'small'];
|
||||
|
||||
export default {
|
||||
name: 'TableHeaderTools',
|
||||
props: {
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
visibleColumnKeys: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
tableSize: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
searchVisible: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showRefresh: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showColumn: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showSearch: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showGrid: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['refresh', 'update:visibleColumnKeys', 'update:tableSize', 'update:searchVisible'],
|
||||
data() {
|
||||
return {
|
||||
columnSettingVisible: false,
|
||||
};
|
||||
},
|
||||
deactivated() {
|
||||
// keep-alive 缓存页面时关闭列设置弹窗,避免 Teleport 到 body 的弹窗切页后残留
|
||||
this.columnSettingVisible = false;
|
||||
},
|
||||
methods: {
|
||||
toggleTableSize() {
|
||||
const index = TABLE_SIZE_OPTIONS.indexOf(this.tableSize);
|
||||
const next = TABLE_SIZE_OPTIONS[(index + 1) % TABLE_SIZE_OPTIONS.length];
|
||||
this.$emit('update:tableSize', next);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.table-header-tools.avue-crud__right {
|
||||
width: auto;
|
||||
margin: 0 0 0 auto;
|
||||
flex: none;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.table-header-tools__column-setting {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 360px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -9,18 +9,21 @@
|
||||
<el-table :data="rows" border @selection-change="selectedRows = $event">
|
||||
<el-table-column v-if="!readonly" type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="240" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="240"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span
|
||||
class="vehicle-attachment-table__file-name"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>
|
||||
{{ attachmentName(row) }}
|
||||
</span>
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220">
|
||||
|
||||
@@ -305,6 +305,10 @@ export const createOption = () => ({
|
||||
searchOrder: 3,
|
||||
searchPlaceholder: '请选择',
|
||||
dicData: [],
|
||||
props: {
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
},
|
||||
filterable: true,
|
||||
hide: true,
|
||||
display: false,
|
||||
|
||||
@@ -2,6 +2,9 @@ const dimensionOptions = [
|
||||
{ label: '重量', value: '重量' },
|
||||
{ label: '体积', value: '体积' },
|
||||
{ label: '数量', value: '数量' },
|
||||
{ label: '整车', value: '整车' },
|
||||
{ label: '里程', value: '里程' },
|
||||
{ label: '吨·公里', value: '吨·公里' },
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
|
||||
@@ -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],
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -209,7 +212,7 @@ export const config = {
|
||||
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: '是否范本',
|
||||
@@ -617,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,
|
||||
@@ -626,7 +655,7 @@ export const option = createCrudOption([
|
||||
order: 40,
|
||||
maxlength: 2000,
|
||||
showWordLimit: true,
|
||||
rules: textRule('备注', 2000),
|
||||
rules: textRule('其他事项说明', 2000),
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
|
||||
@@ -71,7 +71,7 @@ export const config = {
|
||||
action: 'mkAudit',
|
||||
label: '流程',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing', 'change_reviewing'],
|
||||
status: ['reviewing', 'change_reviewing', 'approved', 'change_approved'],
|
||||
permission: 'project_apply_approve',
|
||||
mkOnly: true,
|
||||
},
|
||||
@@ -185,7 +185,7 @@ export const option = createCrudOption([
|
||||
searchPlaceholder: '请输入',
|
||||
searchOrder: 8,
|
||||
minWidth: 150,
|
||||
rules: textRule('平台公司', 50, true),
|
||||
rules: textRule('平台公司', 500, true),
|
||||
},
|
||||
{
|
||||
label: '平台公司ID',
|
||||
|
||||
@@ -400,7 +400,6 @@ export const option = {
|
||||
span: 6,
|
||||
order: 350,
|
||||
minWidth: 150,
|
||||
rules: [{ required: true, message: '请选择计划开始日期', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '计划结束日期',
|
||||
@@ -411,7 +410,6 @@ export const option = {
|
||||
span: 6,
|
||||
order: 340,
|
||||
minWidth: 150,
|
||||
rules: [{ required: true, message: '请选择计划结束日期', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '数据来源',
|
||||
|
||||
@@ -98,6 +98,24 @@ const getProcessConfigNodes = row => {
|
||||
|
||||
const isAcceptProcessNode = node => node.key === 'accept' || node.name === '接单';
|
||||
|
||||
/**
|
||||
* 过程节点列显示文案。
|
||||
*
|
||||
* 该列在库里存的是「原始值」,且历史上写入了两种格式:司机打卡推进写的是配置的
|
||||
* key(unload / dispatch / load …),配载单新建默认值写的是中文名(接单)。
|
||||
* 故这里按两种格式都认一次,认不出就原样显示,不把值吞掉。
|
||||
*
|
||||
* 运单创建后、司机首次打卡前该字段为空,属正常——显示「-」,避免留白看不出是没值还是没渲染。
|
||||
*/
|
||||
const formatCurrentProcessNode = row => {
|
||||
const value = String(getFirstValue(row, ['currentProcessNode', 'currentNode']) || '').trim();
|
||||
if (!value) return '-';
|
||||
const node = getProcessConfigNodes(row).find(
|
||||
item => item.key === value || item.name === value
|
||||
);
|
||||
return node?.name || node?.key || value;
|
||||
};
|
||||
|
||||
const isTruthyFlag = value => {
|
||||
if (value === true || value === 1) return true;
|
||||
if (value === false || value === 0 || value === null || value === undefined || value === '') {
|
||||
@@ -608,6 +626,7 @@ export const option = {
|
||||
{
|
||||
label: '过程节点',
|
||||
prop: 'currentProcessNode',
|
||||
formatter: row => formatCurrentProcessNode(row),
|
||||
minWidth: 140,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
|
||||
@@ -1,22 +1,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 },
|
||||
{ prop: 'kingdeeLockStatus', label: '金蝶锁状态', minWidth: 150 },
|
||||
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
|
||||
];
|
||||
/**
|
||||
* 付款申请列表(avue-crud)配置
|
||||
* ------------------------------------------------------------------
|
||||
* 列表页与其它模块统一使用 avue-crud,头部 4 个按钮(刷新 / 列显隐 / 搜索显隐 /
|
||||
* 表格卡片切换)由 avue-crud 自带,不做二次封装。
|
||||
*
|
||||
* 搜索项与表格列 label 不一致的(如表格列「收款方」/ 搜索项「客户名称」),
|
||||
* 单独定义一个 hide 的搜索列,并用 searchProp 映射到后端字段。
|
||||
*
|
||||
* ⚠️ avue 的 searchOrder 为降序排序,值越大越靠前(见 header-search2.js:
|
||||
* `list.sort((a, b) => (b.searchOrder || 0) - (a.searchOrder || 0))`)。
|
||||
*/
|
||||
|
||||
const textFormatter = (row, value) =>
|
||||
value === null || value === undefined || value === '' ? '-' : value;
|
||||
|
||||
const moneyFormatter = (row, value) => Number(value || 0).toFixed(2);
|
||||
|
||||
// 仅用于搜索、不在表格中出现的列
|
||||
const searchOnlyColumn = ({ label, prop, searchOrder, searchProp, dicData }) => {
|
||||
const column = {
|
||||
label,
|
||||
prop,
|
||||
search: true,
|
||||
searchOrder,
|
||||
searchPlaceholder: '请输入',
|
||||
hide: true,
|
||||
showColumn: false,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
};
|
||||
if (searchProp) column.searchProp = searchProp;
|
||||
if (dicData) {
|
||||
column.type = 'select';
|
||||
column.searchPlaceholder = '请选择';
|
||||
column.dicData = dicData;
|
||||
}
|
||||
return column;
|
||||
};
|
||||
|
||||
export const paymentApplicationOption = ({
|
||||
paymentTypeOptions = [],
|
||||
approvalStatusOptions = [],
|
||||
kingdeeStatusOptions = [],
|
||||
} = {}) => ({
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
tip: false,
|
||||
searchBtnText: '查询',
|
||||
emptyBtnText: '重置',
|
||||
searchShow: true,
|
||||
searchIcon: true,
|
||||
searchLabelWidth: 88,
|
||||
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 },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
const identityDic = [
|
||||
{ label: '司机', value: 'driver' },
|
||||
{ label: '管理员', value: 'admin' },
|
||||
];
|
||||
|
||||
const statusDic = [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '停用', value: 0 },
|
||||
];
|
||||
|
||||
const validatePhone = (rule, value, callback) => {
|
||||
const phone = String(value || '').trim();
|
||||
if (!phone) {
|
||||
callback(new Error('请输入手机号'));
|
||||
return;
|
||||
}
|
||||
if (!/^1\d{10}$/.test(phone)) {
|
||||
callback(new Error('请输入正确的手机号'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
const validateRemark = (rule, value, callback) => {
|
||||
if (String(value || '').length > 200) {
|
||||
callback(new Error('备注不能超过200字'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
height: 'auto',
|
||||
calcHeight: 22,
|
||||
dialogWidth: 760,
|
||||
labelPosition: 'right',
|
||||
labelWidth: 'auto',
|
||||
tip: false,
|
||||
searchBtnText: '查询',
|
||||
emptyBtnText: '重置',
|
||||
saveBtnText: '提交',
|
||||
updateBtnText: '提交',
|
||||
searchShow: true,
|
||||
searchIcon: true,
|
||||
searchLabelWidth: 160,
|
||||
searchMenuSpan: 24,
|
||||
searchIndex: 4,
|
||||
searchMenuPosition: 'right',
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 70,
|
||||
viewBtn: false,
|
||||
delBtn: false,
|
||||
editBtn: false,
|
||||
addBtn: false,
|
||||
selection: true,
|
||||
dialogClickModal: false,
|
||||
menuWidth: 320,
|
||||
column: [
|
||||
{
|
||||
label: '手机号',
|
||||
prop: 'phone',
|
||||
minWidth: 130,
|
||||
search: true,
|
||||
searchOrder: 4,
|
||||
searchSpan: 6,
|
||||
searchPlaceholder: '请输入',
|
||||
maxlength: 11,
|
||||
showWordLimit: true,
|
||||
order: 100,
|
||||
rules: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ validator: validatePhone, trigger: 'blur' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '账号',
|
||||
prop: 'account',
|
||||
minWidth: 130,
|
||||
search: true,
|
||||
searchOrder: 3,
|
||||
searchSpan: 6,
|
||||
searchPlaceholder: '请输入',
|
||||
maxlength: 45,
|
||||
order: 90,
|
||||
placeholder: '默认与手机号相同',
|
||||
},
|
||||
{
|
||||
label: '姓名',
|
||||
prop: 'realName',
|
||||
minWidth: 100,
|
||||
search: true,
|
||||
searchOrder: 2,
|
||||
searchSpan: 6,
|
||||
searchPlaceholder: '请输入',
|
||||
maxlength: 10,
|
||||
showWordLimit: true,
|
||||
order: 80,
|
||||
},
|
||||
{
|
||||
label: '昵称',
|
||||
prop: 'name',
|
||||
minWidth: 100,
|
||||
maxlength: 20,
|
||||
showWordLimit: true,
|
||||
order: 70,
|
||||
},
|
||||
{
|
||||
label: '小程序身份',
|
||||
prop: 'identities',
|
||||
type: 'select',
|
||||
multiple: true,
|
||||
dataType: 'string',
|
||||
dicData: identityDic,
|
||||
minWidth: 140,
|
||||
search: true,
|
||||
searchOrder: 1,
|
||||
searchSpan: 6,
|
||||
searchPlaceholder: '请选择',
|
||||
order: 60,
|
||||
rules: [{ required: true, message: '请选择小程序身份', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '性别',
|
||||
prop: 'sex',
|
||||
type: 'select',
|
||||
dicUrl: '/blade-system/dict/dictionary?code=sex',
|
||||
props: {
|
||||
label: 'dictValue',
|
||||
value: 'dictKey',
|
||||
},
|
||||
dataType: 'number',
|
||||
minWidth: 80,
|
||||
hide: true,
|
||||
order: 50,
|
||||
},
|
||||
{
|
||||
label: 'openid',
|
||||
prop: 'openid',
|
||||
minWidth: 180,
|
||||
overHidden: true,
|
||||
hide: true,
|
||||
addDisplay: false,
|
||||
editDisplay: true,
|
||||
viewDisplay: true,
|
||||
order: 40,
|
||||
placeholder: '可清空以解绑微信',
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
dicData: statusDic,
|
||||
dataType: 'number',
|
||||
minWidth: 90,
|
||||
search: true,
|
||||
searchOrder: 0,
|
||||
searchSpan: 6,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
value: 1,
|
||||
order: 30,
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
prop: 'remark',
|
||||
type: 'textarea',
|
||||
span: 24,
|
||||
minRows: 2,
|
||||
maxlength: 200,
|
||||
showWordLimit: true,
|
||||
hide: true,
|
||||
order: 20,
|
||||
rules: [{ validator: validateRemark, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
minWidth: 170,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: true,
|
||||
order: 10,
|
||||
},
|
||||
{
|
||||
label: '更新人',
|
||||
prop: 'updateUserName',
|
||||
minWidth: 100,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: true,
|
||||
order: 5,
|
||||
},
|
||||
{
|
||||
label: '更新时间',
|
||||
prop: 'updateTime',
|
||||
minWidth: 170,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: true,
|
||||
order: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -27,7 +27,8 @@ export const userOption = safe => {
|
||||
index: true,
|
||||
selection: true,
|
||||
addBtn: false,
|
||||
viewBtn: true,
|
||||
viewBtn: false,
|
||||
menuWidth: 160,
|
||||
dialogType: 'dialog',
|
||||
dialogWidth: '90%',
|
||||
labelPosition: 'right',
|
||||
@@ -37,8 +38,11 @@ export const userOption = safe => {
|
||||
{
|
||||
label: '账号名',
|
||||
prop: 'account',
|
||||
slot: true,
|
||||
minWidth: 180,
|
||||
search: true,
|
||||
searchOrder: 2,
|
||||
searchPlaceholder: '支持模糊搜索',
|
||||
display: false,
|
||||
},
|
||||
{
|
||||
@@ -46,6 +50,7 @@ export const userOption = safe => {
|
||||
prop: 'realName',
|
||||
search: true,
|
||||
searchOrder: 4,
|
||||
searchPlaceholder: '支持模糊搜索',
|
||||
display: false,
|
||||
},
|
||||
{
|
||||
@@ -53,6 +58,7 @@ export const userOption = safe => {
|
||||
prop: 'phone',
|
||||
search: true,
|
||||
searchOrder: 3,
|
||||
searchPlaceholder: '支持模糊搜索',
|
||||
display: false,
|
||||
},
|
||||
{ label: '工号', prop: 'code', display: false },
|
||||
@@ -60,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',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -95,6 +95,8 @@ export const option = {
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
// 这里是「最新一次提交的状态」缓存,不等于该车当前是否已认证:
|
||||
// 已认证由生效快照指针决定,曾审核通过即永久保持,重新提交不会让它退回审核中
|
||||
label: '认证状态',
|
||||
prop: 'certificationStatus',
|
||||
slot: true,
|
||||
@@ -104,9 +106,10 @@ export const option = {
|
||||
type: 'select',
|
||||
dicData: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '认证中', value: 0 },
|
||||
{ label: '已认证', value: 1 },
|
||||
{ label: '审核中', value: 0 },
|
||||
{ label: '已通过', value: 1 },
|
||||
{ label: '已驳回', value: 2 },
|
||||
{ label: '已作废', value: 3 },
|
||||
],
|
||||
minWidth: 110,
|
||||
align: 'center',
|
||||
|
||||
@@ -12,6 +12,9 @@ export const option = {
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
dialogWidth: 1200,
|
||||
// 弹窗根节点 class:avue-crud 弹窗默认 append-to-body 到 body,
|
||||
// 页面内 scoped 样式命中不到弹窗内容,故用该 class 在全局样式里做兜底(见 styles/element-ui.scss)
|
||||
dialogCustomClass: 'vehicle-dispatch-dialog',
|
||||
labelPosition: 'right',
|
||||
labelWidth: 'auto',
|
||||
tip: false,
|
||||
@@ -101,6 +104,7 @@ export const option = {
|
||||
minWidth: 120,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '当前节点',
|
||||
@@ -108,6 +112,7 @@ export const option = {
|
||||
minWidth: 120,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '当前处理人',
|
||||
@@ -115,6 +120,7 @@ export const option = {
|
||||
minWidth: 130,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
@@ -122,6 +128,7 @@ export const option = {
|
||||
minWidth: 120,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
@@ -132,6 +139,7 @@ export const option = {
|
||||
minWidth: 170,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
},
|
||||
// ========== 新增/编辑表单专用(不进表格)==========
|
||||
{
|
||||
@@ -165,6 +173,7 @@ export const option = {
|
||||
type: 'textarea',
|
||||
hide: true,
|
||||
span: 24,
|
||||
minRows: 2,
|
||||
maxlength: 200,
|
||||
showWordLimit: true,
|
||||
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
||||
|
||||
+103
-5
@@ -173,7 +173,10 @@
|
||||
}
|
||||
|
||||
.el-dialog .el-col {
|
||||
// 取消列自身下边距,改由 form-item 统一控制行距;
|
||||
// flow-root 避免子级 margin 折叠穿透导致换行后行距失效、校验提示被下一行遮挡
|
||||
margin-bottom: 0;
|
||||
display: flow-root;
|
||||
}
|
||||
|
||||
.el-dialog .el-form-item,
|
||||
@@ -181,7 +184,7 @@
|
||||
.el-dialog .el-form-item--small.el-form-item,
|
||||
.el-dialog .el-form-item--large.el-form-item,
|
||||
.el-dialog .el-form-item--medium.el-form-item {
|
||||
//margin-bottom: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* 全站弹窗:底部操作栏浮动效果(参照 business/project-apply)
|
||||
@@ -362,9 +365,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 弹窗内表单项默认上下间距 16px(覆盖默认 18px / 22px,与 customer-archive 对齐)
|
||||
// 4) 弹窗内表单项上下间距:预留绝对定位校验提示(约 14~18px)的可视空间
|
||||
.el-dialog .el-form-item {
|
||||
//margin-bottom: 16px;
|
||||
//margin-bottom: 24px;
|
||||
}
|
||||
|
||||
// 校验提示浮在控件下方,提高层级避免被下一行输入框背景遮挡
|
||||
.el-form-item__error {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
// 5) 通用分组白卡组件(<section-card>):标题融入卡头
|
||||
@@ -446,7 +454,7 @@
|
||||
.dialog-form--carded {
|
||||
.el-form-item {
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
@@ -644,6 +652,28 @@
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
// 车辆调度新增/编辑/查看弹窗:使用部门 el-cascader 默认 display: inline-block、宽度按内容收缩,
|
||||
// 会比同行的 el-input(--el-input-width 默认 100%)窄一大截,需显式拉满列宽。
|
||||
// 两个必须注意的点(踩过):
|
||||
// 1) avue-crud 弹窗默认 append-to-body 到 body,页面内 `.vehicle-dispatch-page :deep(...)`
|
||||
// 这类以页面容器为祖先的选择器匹配不到弹窗内容,必须用弹窗自身 class(dialogCustomClass)在全局兜底;
|
||||
// 2) avue 表单项在 el-form-item__content 与控件之间还有两层包裹 div
|
||||
// (form/index3.js 渲染:content > div > [div|elTooltip] > 控件),
|
||||
// 所以要用后代选择器,不能写 `>` 直接子级。
|
||||
.vehicle-dispatch-dialog .el-form-item__content .el-cascader {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// 车辆调度弹窗 label 宽度统一:option 用 labelWidth: 'auto' 时 label 宽度随字数变化
|
||||
// (3 字 64px / 2 字 40px / 4 字带星号 78px),控件可用宽度随之变化,
|
||||
// 于是同一行两列控件宽度不一致(如车牌号 486px vs 使用部门 472px)。
|
||||
// 本弹窗左右两列最长 label 均为 4 个汉字(所属组织 / 使用部门 / 申请时间),故统一按 4em + 24px 取宽。
|
||||
.vehicle-dispatch-dialog .el-form-item__label {
|
||||
flex: 0 0 calc(4em + 24px) !important;
|
||||
width: calc(4em + 24px) !important;
|
||||
}
|
||||
|
||||
// 过程配置新增/编辑/查看弹窗(手写 el-dialog,参照 vehicle/credit-score-quantification):
|
||||
// 灰底弹窗体(全站 .el-dialog__body 已统一)内堆叠 <section-card> 分组白卡,
|
||||
// 卡间 12px 间距由 section-card 自身 margin-bottom 保证,与项目其他弹窗风格统一
|
||||
@@ -995,6 +1025,66 @@ td.el-table-fixed-column--right {
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
// 全站统一:内容型长文本列(文件名 / 单号 / 备注等)单元格内也放了 el-link / span.attachment-file-name,
|
||||
// 会被上面两条「含 el-link 的单元格」规则一并命中,导致:
|
||||
// ① 上面第 950 行的 display: flex 让链接成为 flex item,flex item 默认 min-width: auto 无法收缩,
|
||||
// 超长文本(如未规范命名的附件名)会直接把列撑破;
|
||||
// ② 豁免规则的 overflow: visible 让溢出内容不再被单元格裁切,直接压到相邻列上。
|
||||
// 因此对「内容列」做显式兜底:单元格自身恢复块级 + 省略号截断,链接参与收缩并显示省略号。
|
||||
// 用法:在 <el-table-column> 上配置 class-name="is-ellipsis-link"(动态列可 :class-name 绑定)。
|
||||
// 操作列不要加此类,否则会失去自动换行能力。
|
||||
.el-table .el-table__body .cell.is-ellipsis-link {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
.el-link {
|
||||
display: inline-block;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
|
||||
// Element Plus 的 el-link__inner 是 inline-flex,flex 容器上的 text-overflow 不生效,
|
||||
// 必须降级为块级容器,省略号才会真正渲染出来。
|
||||
.el-link__inner {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-file-name {
|
||||
display: inline-block;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
// 附件上传表格:文件名统一用 span,视觉与可点击行为对齐原 el-link primary
|
||||
.attachment-file-name {
|
||||
color: var(--el-color-primary, #409eff);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
color: var(--el-text-color-placeholder, #c0c4cc);
|
||||
cursor: not-allowed;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 全站统一:Avue 内置行操作按钮(查看 / 编辑 / 删除 / 复制等)视觉对齐为 el-link 链接样式。
|
||||
// 选择器仅命中表格行操作列 .avue-crud__menu 内的 is-text 按钮,不误伤顶部工具栏(.avue-crud__header)
|
||||
// 与弹窗按钮(.avue-crud__dialog__menu)。事件(rowView/rowEdit/rowDel)与权限(permission 指令)
|
||||
@@ -1073,7 +1163,12 @@ td.el-table-fixed-column--right {
|
||||
align-items: normal;
|
||||
}
|
||||
.el-form-item--default {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
// 独立表单页(非弹窗)同样避免 el-col 折叠吃掉行距
|
||||
.el-form .el-col {
|
||||
display: flow-root;
|
||||
}
|
||||
|
||||
// 配套:星号必须保持「高度 auto + 继承行高」,stretch 才会按首行排布;
|
||||
@@ -1248,3 +1343,6 @@ td.el-table-fixed-column--right {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
.is-ellipsis-link {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
/**
|
||||
* 认证审核通用工具
|
||||
*
|
||||
* 认证对象(司机 / 车辆)每次提交产生一条不可变快照记录,「审核」针对的是**一条记录**。
|
||||
* 记录分两侧:本次**待审快照**与该对象**当前生效快照**(最后一次审核通过的那条)。
|
||||
* 待审内容在审核通过前不影响生效值,所以审核页必须逐字段对比两侧,而不是只看一条当前值。
|
||||
*
|
||||
* 本文件提供三件事:
|
||||
* 1. 审核状态文案与标签(与后端 statusText 同一套词表);
|
||||
* 2. 快照字段的展示元数据(中文名、控件类型、分组);
|
||||
* 3. 两侧快照的字段级 diff 计算,含「从未认证通过、无对比基准」的处理。
|
||||
*
|
||||
* 两侧快照字段名完全一致(同一套结构),故可逐键比较。
|
||||
*/
|
||||
|
||||
const EMPTY_TEXT = '—';
|
||||
|
||||
const TYPE_IMAGE = 'image';
|
||||
const TYPE_DATE = 'date';
|
||||
const TYPE_LONG_TERM = 'longTerm';
|
||||
|
||||
/** 审核状态取值 */
|
||||
export const CERT_RECORD_STATUS = {
|
||||
PENDING: 0,
|
||||
APPROVED: 1,
|
||||
REJECTED: 2,
|
||||
VOIDED: 3,
|
||||
};
|
||||
|
||||
/** 状态文案统一为:审核中 / 已通过 / 已驳回 / 已作废 */
|
||||
const CERT_STATUS_TAG = {
|
||||
0: { label: '审核中', type: 'warning' },
|
||||
1: { label: '已通过', type: 'success' },
|
||||
2: { label: '已驳回', type: 'danger' },
|
||||
3: { label: '已作废', type: 'info' },
|
||||
};
|
||||
|
||||
/** 变更类型 → 差异标签 */
|
||||
export const CERT_CHANGE_TAG = {
|
||||
added: { label: '新增', type: 'success' },
|
||||
modified: { label: '已修改', type: 'warning' },
|
||||
cleared: { label: '已清空', type: 'danger' },
|
||||
};
|
||||
|
||||
/**
|
||||
* 「最新提交状态」展示缓存 → 标签
|
||||
*
|
||||
* 缓存为空表示该对象从未提交过认证(即没有生效快照)。
|
||||
* 注意它只是最近一次提交的状态,不等于「是否已认证」——后者由生效快照指针决定。
|
||||
*/
|
||||
export const certStatusTag = status => {
|
||||
if (status === null || status === undefined || status === '') {
|
||||
return { label: '未认证', type: 'info' };
|
||||
}
|
||||
return CERT_STATUS_TAG[status] || { label: EMPTY_TEXT, type: 'info' };
|
||||
};
|
||||
|
||||
const pad = value => String(value).padStart(2, '0');
|
||||
|
||||
/** 后端若改用时间戳毫秒序列化日期,也能落成日期文案 */
|
||||
const EPOCH_PATTERN = /^\d{13}$/;
|
||||
|
||||
const isEpochText = text => EPOCH_PATTERN.test(text);
|
||||
|
||||
const toDate = text => {
|
||||
const date = new Date(Number(text));
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
};
|
||||
|
||||
/** 提交时间等日期时间展示,兼容 `yyyy-MM-dd HH:mm:ss`、ISO 串与时间戳毫秒 */
|
||||
export const formatCertDateTime = value => {
|
||||
if (value === null || value === undefined || value === '') return EMPTY_TEXT;
|
||||
const text = String(value).trim();
|
||||
const matched = text.match(
|
||||
/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})[T ](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?/
|
||||
);
|
||||
if (matched) {
|
||||
return (
|
||||
`${matched[1]}-${pad(matched[2])}-${pad(matched[3])} ` +
|
||||
`${pad(matched[4])}:${pad(matched[5])}:${pad(matched[6] || 0)}`
|
||||
);
|
||||
}
|
||||
const date = isEpochText(text) ? toDate(text) : null;
|
||||
if (!date) return text;
|
||||
return (
|
||||
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
|
||||
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 快照字段元数据
|
||||
*
|
||||
* 字段名与后端提交 DTO 的 JSON 键一致(司机 PersonCertSubmitRequest / 车辆 VehicleCertSubmitRequest),
|
||||
* 两侧快照共用同一套结构,故同时用于标签与比较规则。
|
||||
*
|
||||
* `longTermKey` 只挂在**被长期有效作废掉的那个字段**上,也就是各组的「有效期止」与强制报废日期:
|
||||
* 勾了长期有效,它们显示「长期有效」、比较时按同一个值算(避免每次审核都报「已变更」)。
|
||||
* 「有效期起」不挂:起期是一个独立的日期,长期有效并不会让它失效,
|
||||
* 挂上去等于把它自己的值藏起来、还让审核员看不到起期的改动。
|
||||
*/
|
||||
const CERT_SNAPSHOT_GROUPS = {
|
||||
driver: [
|
||||
{
|
||||
title: '身份信息',
|
||||
fields: [
|
||||
{ key: 'realName', label: '真实姓名' },
|
||||
{ key: 'idCardNo', label: '身份证号' },
|
||||
{ key: 'idCardFrontUrl', label: '身份证正面照', type: TYPE_IMAGE },
|
||||
{ key: 'idCardBackUrl', label: '身份证反面照', type: TYPE_IMAGE },
|
||||
{ key: 'idCardValidStart', label: '身份证有效期起', type: TYPE_DATE },
|
||||
{
|
||||
key: 'idCardValidEnd',
|
||||
label: '身份证有效期止',
|
||||
type: TYPE_DATE,
|
||||
longTermKey: 'idCardLongTerm',
|
||||
},
|
||||
{ key: 'idCardLongTerm', label: '身份证长期有效', type: TYPE_LONG_TERM },
|
||||
{ key: 'headPhoto', label: '大头照', type: TYPE_IMAGE },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '驾驶证信息',
|
||||
fields: [
|
||||
{ key: 'driverFileNo', label: '驾驶证档案编号' },
|
||||
{ key: 'driverClass', label: '准驾车型' },
|
||||
{ key: 'driverLicenseFrontUrl', label: '驾驶证主页照片', type: TYPE_IMAGE },
|
||||
{ key: 'driverLicenseBackUrl', label: '驾驶证副页照片', type: TYPE_IMAGE },
|
||||
{ key: 'driverValidStart', label: '驾驶证有效期起', type: TYPE_DATE },
|
||||
{
|
||||
key: 'driverValidEnd',
|
||||
label: '驾驶证有效期止',
|
||||
type: TYPE_DATE,
|
||||
longTermKey: 'driverLongTerm',
|
||||
},
|
||||
{ key: 'driverLongTerm', label: '驾驶证长期有效', type: TYPE_LONG_TERM },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '从业资格证信息',
|
||||
fields: [
|
||||
{ key: 'qualificationType', label: '从业资格认证类型' },
|
||||
{ key: 'qualificationNo', label: '从业资格证号' },
|
||||
{
|
||||
key: 'qualificationFront',
|
||||
label: '从业资格证封面页',
|
||||
type: TYPE_IMAGE,
|
||||
legacyKeys: ['qualificationUrl'],
|
||||
},
|
||||
{ key: 'qualificationBack', label: '从业资格证内容页', type: TYPE_IMAGE },
|
||||
{ key: 'qualificationValidStart', label: '从业资格证有效期起', type: TYPE_DATE },
|
||||
{
|
||||
key: 'qualificationValidEnd',
|
||||
label: '从业资格证有效期止',
|
||||
type: TYPE_DATE,
|
||||
longTermKey: 'qualificationLongTerm',
|
||||
},
|
||||
{ key: 'qualificationLongTerm', label: '从业资格证长期有效', type: TYPE_LONG_TERM },
|
||||
],
|
||||
},
|
||||
],
|
||||
/*
|
||||
* 车辆字段名以 VehicleCertSubmitRequest 为准,与 PersonCertSubmitRequest 不同。
|
||||
* 键名对不上不报错——每个字段都算「没有值」,两组全显示「—」且 diff 不出变更。
|
||||
*/
|
||||
vehicle: [
|
||||
{
|
||||
title: '车辆基本信息',
|
||||
fields: [
|
||||
{ key: 'plateNo', label: '车牌号' },
|
||||
{ key: 'vehicleType', label: '车辆类型' },
|
||||
{ key: 'plateColor', label: '车牌颜色' },
|
||||
{ key: 'ownerName', label: '所有人' },
|
||||
{ key: 'vin', label: '车辆识别代号(车架号)' },
|
||||
{ key: 'outerLength', label: '外廓长度(mm)' },
|
||||
{ key: 'outerWidth', label: '外廓宽度(mm)' },
|
||||
{ key: 'outerHeight', label: '外廓高度(mm)' },
|
||||
{ key: 'approvedLoadKg', label: '核定载质量(KG)' },
|
||||
{ key: 'tractionMassKg', label: '准牵引总质量(KG)' },
|
||||
{ key: 'energyType', label: '能源类型' },
|
||||
{ key: 'businessRelation', label: '业务关系' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '行驶证信息',
|
||||
fields: [
|
||||
{ key: 'compulsoryScrapDate', label: '强制报废日期', type: TYPE_DATE, longTermKey: 'compulsoryScrapLongTerm' },
|
||||
{ key: 'compulsoryScrapLongTerm', label: '强制报废长期有效', type: TYPE_LONG_TERM },
|
||||
{ key: 'drivingLicenseNo', label: '行驶证档案编号' },
|
||||
{ key: 'drivingLicenseImage', label: '行驶证主页正面', type: TYPE_IMAGE },
|
||||
{ key: 'drivingLicenseMainBack', label: '行驶证主页反面', type: TYPE_IMAGE },
|
||||
{ key: 'drivingLicenseViceFront', label: '行驶证副页正面', type: TYPE_IMAGE },
|
||||
{ key: 'drivingLicenseViceBack', label: '行驶证副页反面', type: TYPE_IMAGE },
|
||||
{ key: 'drivingLicenseStartDate', label: '行驶证有效期起', type: TYPE_DATE },
|
||||
{
|
||||
key: 'drivingLicenseEndDate',
|
||||
label: '行驶证有效期止',
|
||||
type: TYPE_DATE,
|
||||
longTermKey: 'drivingLicenseLongTerm',
|
||||
},
|
||||
{ key: 'drivingLicenseLongTerm', label: '行驶证长期有效', type: TYPE_LONG_TERM },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '道路运输证信息',
|
||||
fields: [
|
||||
{ key: 'roadTransportCertNo', label: '道路运输证号' },
|
||||
{ key: 'roadTransportCertImage', label: '道路运输证照片', type: TYPE_IMAGE },
|
||||
{ key: 'roadTransportCertStartDate', label: '道路运输证有效期起', type: TYPE_DATE },
|
||||
{
|
||||
key: 'roadTransportCertEndDate',
|
||||
label: '道路运输证有效期止',
|
||||
type: TYPE_DATE,
|
||||
longTermKey: 'roadTransportCertLongTerm',
|
||||
},
|
||||
{ key: 'roadTransportCertLongTerm', label: '道路运输证长期有效', type: TYPE_LONG_TERM },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '机动车登记本信息',
|
||||
fields: [
|
||||
{ key: 'registrationNo', label: '机动车登记编号' },
|
||||
{ key: 'registrationImage', label: '机动车登记本照片', type: TYPE_IMAGE },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/** 提交元信息与主键不参与对比(两侧必相同,展示出来只是噪音) */
|
||||
const IGNORED_KEYS = [
|
||||
'id',
|
||||
'bizType',
|
||||
'bizId',
|
||||
'bizKey',
|
||||
'certificationStatus',
|
||||
'snapshotJson',
|
||||
'pendingSnapshotJson',
|
||||
'effectiveSnapshotJson',
|
||||
];
|
||||
|
||||
/**
|
||||
* 解析快照 JSON
|
||||
*
|
||||
* 后端把 null 序列化为空串,故空串 / 'null' 都按「没有快照」处理,不算解析失败。
|
||||
*/
|
||||
const readSnapshot = json => {
|
||||
if (json && typeof json === 'object') {
|
||||
return { value: Object.keys(json).length ? json : null, invalid: false };
|
||||
}
|
||||
const text = String(json === null || json === undefined ? '' : json).trim();
|
||||
if (!text || text === 'null') {
|
||||
return { value: null, invalid: false };
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
return { value: parsed, invalid: false };
|
||||
}
|
||||
return { value: null, invalid: true };
|
||||
} catch (error) {
|
||||
return { value: null, invalid: true };
|
||||
}
|
||||
};
|
||||
|
||||
/** 解析快照 JSON,无快照或解析失败时返回 null */
|
||||
export const parseSnapshot = json => readSnapshot(json).value;
|
||||
|
||||
const isEmptyValue = value => value === null || value === undefined || value === '';
|
||||
|
||||
const isLongTermOn = value => value === 1 || value === '1' || value === true;
|
||||
|
||||
/** 日期归一化到 yyyy-MM-dd,便于 `2026-01-01` 与 `2026-01-01T00:00:00` 判等 */
|
||||
const normalizeDate = value => {
|
||||
if (isEmptyValue(value)) return '';
|
||||
const text = String(value).trim();
|
||||
const matched = text.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/);
|
||||
if (matched) return `${matched[1]}-${pad(matched[2])}-${pad(matched[3])}`;
|
||||
const date = isEpochText(text) ? toDate(text) : null;
|
||||
if (date) {
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
/**
|
||||
* 取字段值,兼容字段改名前的旧键
|
||||
*
|
||||
* 后端把某字段改名后,改名之前提交的记录里存的仍是旧键(例:从业资格证封面页
|
||||
* 由 `qualificationUrl` 改为 `qualificationFront`)。只读新键会得到空,
|
||||
* 审核页就把已填的封面页显示成「—」,误导审核员以为对方没传。
|
||||
* 新旧键都存在时以新键为准。
|
||||
*/
|
||||
const readFieldValue = (snapshot, field) => {
|
||||
if (!snapshot) return undefined;
|
||||
const value = snapshot[field.key];
|
||||
if (!isEmptyValue(value)) return value;
|
||||
const legacyKeys = field.legacyKeys || [];
|
||||
for (let i = 0; i < legacyKeys.length; i += 1) {
|
||||
if (!isEmptyValue(snapshot[legacyKeys[i]])) return snapshot[legacyKeys[i]];
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
/** 比较用的归一化取值:展示相同即视为未变更 */
|
||||
const canonicalOf = (snapshot, field) => {
|
||||
const value = readFieldValue(snapshot, field);
|
||||
if (field.type === TYPE_LONG_TERM) {
|
||||
if (isEmptyValue(value)) return '';
|
||||
return isLongTermOn(value) ? '1' : '0';
|
||||
}
|
||||
if (field.type === TYPE_DATE) {
|
||||
if (field.longTermKey && isLongTermOn(snapshot && snapshot[field.longTermKey])) {
|
||||
return '长期有效';
|
||||
}
|
||||
return normalizeDate(value);
|
||||
}
|
||||
return isEmptyValue(value) ? '' : String(value).trim();
|
||||
};
|
||||
|
||||
/** 展示文案:日期型受「长期有效」影响,图片型返回 URL 由组件渲染缩略图 */
|
||||
const textOf = (snapshot, field) => {
|
||||
const value = readFieldValue(snapshot, field);
|
||||
if (field.type === TYPE_IMAGE) {
|
||||
return isEmptyValue(value) ? '' : String(value);
|
||||
}
|
||||
if (field.type === TYPE_LONG_TERM) {
|
||||
if (isEmptyValue(value)) return EMPTY_TEXT;
|
||||
return isLongTermOn(value) ? '是' : '否';
|
||||
}
|
||||
if (field.type === TYPE_DATE) {
|
||||
if (field.longTermKey && isLongTermOn(snapshot && snapshot[field.longTermKey])) {
|
||||
return '长期有效';
|
||||
}
|
||||
return normalizeDate(value) || EMPTY_TEXT;
|
||||
}
|
||||
return isEmptyValue(value) ? EMPTY_TEXT : String(value);
|
||||
};
|
||||
|
||||
const buildRow = (field, pendingSnapshot, effectiveSnapshot, hasBaseline) => {
|
||||
const type = field.type || 'text';
|
||||
const target = { ...field, type };
|
||||
const pendingCanonical = canonicalOf(pendingSnapshot, target);
|
||||
const effectiveCanonical = canonicalOf(effectiveSnapshot, target);
|
||||
let changeType = '';
|
||||
if (hasBaseline && pendingCanonical !== effectiveCanonical) {
|
||||
if (!effectiveCanonical) {
|
||||
changeType = 'added';
|
||||
} else if (!pendingCanonical) {
|
||||
changeType = 'cleared';
|
||||
} else {
|
||||
changeType = 'modified';
|
||||
}
|
||||
}
|
||||
return {
|
||||
key: field.key,
|
||||
label: field.label,
|
||||
type,
|
||||
effective: {
|
||||
raw: readFieldValue(effectiveSnapshot, target),
|
||||
text: textOf(effectiveSnapshot, target),
|
||||
},
|
||||
pending: {
|
||||
raw: readFieldValue(pendingSnapshot, target),
|
||||
text: textOf(pendingSnapshot, target),
|
||||
},
|
||||
changed: Boolean(changeType),
|
||||
changeType,
|
||||
};
|
||||
};
|
||||
|
||||
const unionKeys = (pendingSnapshot, effectiveSnapshot) => {
|
||||
const keys = [];
|
||||
[pendingSnapshot, effectiveSnapshot].forEach(snapshot => {
|
||||
Object.keys(snapshot || {}).forEach(key => {
|
||||
if (!keys.includes(key)) keys.push(key);
|
||||
});
|
||||
});
|
||||
return keys;
|
||||
};
|
||||
|
||||
/**
|
||||
* 构造「待审快照 vs 当前生效值」的字段级 diff
|
||||
*
|
||||
* @param {string} bizType 认证对象类型:driver / vehicle
|
||||
* @param {string} pendingJson 本次待审快照 JSON
|
||||
* @param {string} effectiveJson 该对象当前生效快照 JSON,从未认证通过时为空
|
||||
* @returns {Object} { hasBaseline, groups, totalCount, changedCount, ... }
|
||||
* hasBaseline 为 false 表示无对比基准(从未认证通过),此时不产出任何变更标记
|
||||
*/
|
||||
export const buildSnapshotDiff = (bizType, pendingJson, effectiveJson) => {
|
||||
const pending = readSnapshot(pendingJson);
|
||||
const effective = readSnapshot(effectiveJson);
|
||||
const hasBaseline = effective.value !== null;
|
||||
const groupsConfig = CERT_SNAPSHOT_GROUPS[bizType] || [];
|
||||
const knownKeys = [];
|
||||
|
||||
const groups = groupsConfig.map(group => {
|
||||
const rows = group.fields.map(field => {
|
||||
// 旧键已由所属字段并入展示,不能再作为独立字段出现,否则同一张封面页会出现两行
|
||||
knownKeys.push(field.key, ...(field.legacyKeys || []));
|
||||
return buildRow(field, pending.value, effective.value, hasBaseline);
|
||||
});
|
||||
return {
|
||||
title: group.title,
|
||||
rows,
|
||||
changedCount: rows.filter(row => row.changed).length,
|
||||
};
|
||||
});
|
||||
|
||||
// 元数据未登记的字段照样展示,避免后端新增字段时审核员看不到变化
|
||||
const extraKeys = unionKeys(pending.value, effective.value).filter(
|
||||
key => !knownKeys.includes(key) && !IGNORED_KEYS.includes(key)
|
||||
);
|
||||
if (extraKeys.length) {
|
||||
const rows = extraKeys.map(key =>
|
||||
buildRow({ key, label: key }, pending.value, effective.value, hasBaseline)
|
||||
);
|
||||
groups.push({
|
||||
title: '其他字段',
|
||||
rows,
|
||||
changedCount: rows.filter(row => row.changed).length,
|
||||
});
|
||||
}
|
||||
|
||||
const rows = groups.reduce((all, group) => all.concat(group.rows), []);
|
||||
return {
|
||||
hasBaseline,
|
||||
pendingInvalid: pending.invalid,
|
||||
effectiveInvalid: effective.invalid,
|
||||
groups,
|
||||
totalCount: rows.length,
|
||||
changedCount: rows.filter(row => row.changed).length,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 合同下拉/选择展示:合同编号/合同名称
|
||||
*
|
||||
* @param {Object} contract 合同对象
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatContractOptionLabel(contract = {}) {
|
||||
const contractNo = String(contract?.contractNo ?? '').trim();
|
||||
const contractName = String(contract?.contractName ?? '').trim();
|
||||
if (contractNo && contractName) {
|
||||
return `${contractNo}/${contractName}`;
|
||||
}
|
||||
return contractNo || contractName || '';
|
||||
}
|
||||
@@ -13,6 +13,92 @@ const coordinateRanges = {
|
||||
},
|
||||
};
|
||||
|
||||
/** WGS-84 → GCJ-02(高德)偏移算法常量 */
|
||||
const WGS84_TO_GCJ02_PI = Math.PI;
|
||||
const WGS84_TO_GCJ02_A = 6378245.0;
|
||||
const WGS84_TO_GCJ02_EE = 0.00669342162296594323;
|
||||
|
||||
const isOutOfChina = (longitude, latitude) =>
|
||||
longitude < 72.004 || longitude > 137.8347 || latitude < 0.8293 || latitude > 55.8271;
|
||||
|
||||
const transformLatitude = (longitude, latitude) => {
|
||||
let result =
|
||||
-100.0 +
|
||||
2.0 * longitude +
|
||||
3.0 * latitude +
|
||||
0.2 * latitude * latitude +
|
||||
0.1 * longitude * latitude +
|
||||
0.2 * Math.sqrt(Math.abs(longitude));
|
||||
result +=
|
||||
((20.0 * Math.sin(6.0 * longitude * WGS84_TO_GCJ02_PI) +
|
||||
20.0 * Math.sin(2.0 * longitude * WGS84_TO_GCJ02_PI)) *
|
||||
2.0) /
|
||||
3.0;
|
||||
result +=
|
||||
((20.0 * Math.sin(latitude * WGS84_TO_GCJ02_PI) +
|
||||
40.0 * Math.sin((latitude / 3.0) * WGS84_TO_GCJ02_PI)) *
|
||||
2.0) /
|
||||
3.0;
|
||||
result +=
|
||||
((160.0 * Math.sin((latitude / 12.0) * WGS84_TO_GCJ02_PI) +
|
||||
320 * Math.sin((latitude * WGS84_TO_GCJ02_PI) / 30.0)) *
|
||||
2.0) /
|
||||
3.0;
|
||||
return result;
|
||||
};
|
||||
|
||||
const transformLongitude = (longitude, latitude) => {
|
||||
let result =
|
||||
300.0 +
|
||||
longitude +
|
||||
2.0 * latitude +
|
||||
0.1 * longitude * longitude +
|
||||
0.1 * longitude * latitude +
|
||||
0.1 * Math.sqrt(Math.abs(longitude));
|
||||
result +=
|
||||
((20.0 * Math.sin(6.0 * longitude * WGS84_TO_GCJ02_PI) +
|
||||
20.0 * Math.sin(2.0 * longitude * WGS84_TO_GCJ02_PI)) *
|
||||
2.0) /
|
||||
3.0;
|
||||
result +=
|
||||
((20.0 * Math.sin(longitude * WGS84_TO_GCJ02_PI) +
|
||||
40.0 * Math.sin((longitude / 3.0) * WGS84_TO_GCJ02_PI)) *
|
||||
2.0) /
|
||||
3.0;
|
||||
result +=
|
||||
((150.0 * Math.sin((longitude / 12.0) * WGS84_TO_GCJ02_PI) +
|
||||
300.0 * Math.sin((longitude / 30.0) * WGS84_TO_GCJ02_PI)) *
|
||||
2.0) /
|
||||
3.0;
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* WGS-84(GPS / LBS)转 GCJ-02(高德/国测局),供高德地图绘制使用。
|
||||
* 中国境外坐标不做偏移,原样返回。
|
||||
* @param {number} longitude
|
||||
* @param {number} latitude
|
||||
* @returns {[number, number]} [经度, 纬度]
|
||||
*/
|
||||
export const wgs84ToGcj02 = (longitude, latitude) => {
|
||||
const lng = Number(longitude);
|
||||
const lat = Number(latitude);
|
||||
if (!Number.isFinite(lng) || !Number.isFinite(lat) || isOutOfChina(lng, lat)) {
|
||||
return [lng, lat];
|
||||
}
|
||||
let dLat = transformLatitude(lng - 105.0, lat - 35.0);
|
||||
let dLng = transformLongitude(lng - 105.0, lat - 35.0);
|
||||
const radLat = (lat / 180.0) * WGS84_TO_GCJ02_PI;
|
||||
let magic = Math.sin(radLat);
|
||||
magic = 1 - WGS84_TO_GCJ02_EE * magic * magic;
|
||||
const sqrtMagic = Math.sqrt(magic);
|
||||
dLat =
|
||||
(dLat * 180.0) /
|
||||
(((WGS84_TO_GCJ02_A * (1 - WGS84_TO_GCJ02_EE)) / (magic * sqrtMagic)) * WGS84_TO_GCJ02_PI);
|
||||
dLng = (dLng * 180.0) / ((WGS84_TO_GCJ02_A / sqrtMagic) * Math.cos(radLat) * WGS84_TO_GCJ02_PI);
|
||||
return [lng + dLng, lat + dLat];
|
||||
};
|
||||
|
||||
export const normalizeCoordinateInput = value => {
|
||||
const rawValue = String(value ?? '').replace(/[^\d.-]/g, '');
|
||||
const isNegative = rawValue.startsWith('-');
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
const CURRENCY_NAME_MAP = {
|
||||
CNY: '人民币',
|
||||
RMB: '人民币',
|
||||
USD: '美元',
|
||||
EUR: '欧元',
|
||||
JPY: '日元',
|
||||
GBP: '英镑',
|
||||
};
|
||||
|
||||
const CURRENCY_UNIT_MAP = {
|
||||
CNY: '元',
|
||||
RMB: '元',
|
||||
USD: '美元',
|
||||
EUR: '欧元',
|
||||
JPY: '日元',
|
||||
GBP: '英镑',
|
||||
};
|
||||
|
||||
const CURRENCY_NAME_TO_UNIT = {
|
||||
人民币: '元',
|
||||
美元: '美元',
|
||||
欧元: '欧元',
|
||||
日元: '日元',
|
||||
英镑: '英镑',
|
||||
};
|
||||
|
||||
/**
|
||||
* 归一化币种代码(CNY → RMB)
|
||||
*/
|
||||
export function normalizeCurrencyCode(value, fallback = 'RMB') {
|
||||
const raw =
|
||||
typeof value === 'object'
|
||||
? value?.dictKey || value?.value || value?.code || value?.dictValue || ''
|
||||
: value;
|
||||
const normalized = String(raw || fallback)
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
const code = normalized.includes(' - ') ? normalized.split(' - ')[0].trim() : normalized;
|
||||
if (!code) return fallback;
|
||||
return code === 'CNY' ? 'RMB' : code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 币种中文名称
|
||||
*/
|
||||
export function currencyName(value) {
|
||||
const text = String(value || '');
|
||||
if (text.includes(' - ')) return text.slice(text.indexOf(' - ') + 3);
|
||||
return CURRENCY_NAME_MAP[text.toUpperCase()] || text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金额单位文案:RMB/CNY → 元,USD → 美元
|
||||
*/
|
||||
export function currencyUnit(value) {
|
||||
const text = String(value || '');
|
||||
let code = text;
|
||||
if (text.includes(' - ')) {
|
||||
code = text.split(' - ')[0].trim();
|
||||
}
|
||||
const upperCode = code.toUpperCase();
|
||||
if (CURRENCY_UNIT_MAP[upperCode]) {
|
||||
return CURRENCY_UNIT_MAP[upperCode];
|
||||
}
|
||||
if (CURRENCY_NAME_TO_UNIT[code]) {
|
||||
return CURRENCY_NAME_TO_UNIT[code];
|
||||
}
|
||||
return text || '元';
|
||||
}
|
||||
|
||||
/**
|
||||
* 将计价单位前缀替换为当前币种单位,如 元/吨 → 美元/吨
|
||||
*/
|
||||
export function remapPriceUnitCurrency(priceUnit, currency) {
|
||||
const text = String(priceUnit || '').trim();
|
||||
if (!text || !text.includes('/')) return text;
|
||||
const suffix = text.slice(text.indexOf('/'));
|
||||
return `${currencyUnit(currency)}${suffix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按数量单位生成默认计价单位
|
||||
*/
|
||||
export function resolvePriceUnitByQuantityUnit(quantityUnit, currency = 'RMB') {
|
||||
const unit = currencyUnit(currency);
|
||||
return (
|
||||
{
|
||||
吨: `${unit}/吨`,
|
||||
方: `${unit}/立方`,
|
||||
件: `${unit}/件`,
|
||||
}[String(quantityUnit || '').trim()] || ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字典中的计价单位选项映射到当前币种
|
||||
*/
|
||||
export function mapFeeUnitOptions(options = [], currency = 'RMB') {
|
||||
const unit = currencyUnit(currency);
|
||||
return (options || []).map(item => {
|
||||
const label = String(item.label || item.value || '');
|
||||
const value = String(item.value || item.label || '');
|
||||
return {
|
||||
...item,
|
||||
label: label.replace(/^元/, unit).replace(/^人民币/, unit),
|
||||
value: value.replace(/^元/, unit).replace(/^人民币/, unit),
|
||||
};
|
||||
});
|
||||
}
|
||||
+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,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);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 行政区划展示格式化(仅展示层,不改动后端存储值)。
|
||||
*
|
||||
* 需求:省 / 市 / 区县之间要有空格隔开,避免「上海市浦东新区」「广西壮族自治区南宁市邕宁区」挤成一坨。
|
||||
* 与全站既有规则保持一致(公路地址按「市 区县」展示,省/自治区不重复展示):
|
||||
* 上海市浦东新区 -> 上海市 浦东新区
|
||||
* 广西壮族自治区南宁市邕宁区 -> 南宁市 邕宁区
|
||||
* 梅州市梅县区 -> 梅州市 梅县区
|
||||
*
|
||||
* 非纯行政区划串(站点名「南宁站」「广州南站」、含街道门牌的详细地址等)原样返回,不做任何改动。
|
||||
*/
|
||||
|
||||
// 行政区划后缀:长后缀在前,避免「自治区」被截成「区」、「自治州」被截成「州」
|
||||
const REGION_SUFFIXES = [
|
||||
'特别行政区',
|
||||
'自治区',
|
||||
'自治州',
|
||||
'自治县',
|
||||
'自治旗',
|
||||
'开发区',
|
||||
'新区',
|
||||
'林区',
|
||||
'矿区',
|
||||
'地区',
|
||||
'省',
|
||||
'市',
|
||||
'州',
|
||||
'盟',
|
||||
'区',
|
||||
'县',
|
||||
'旗',
|
||||
];
|
||||
|
||||
// 省级区划:展示时不在区划链首重复(「广西壮族自治区南宁市邕宁区」-> 「南宁市 邕宁区」)
|
||||
const PROVINCE_SUFFIX = /(?:省|自治区|特别行政区)$/;
|
||||
|
||||
/**
|
||||
* 从文本头部匹配第一个行政区划片段。
|
||||
* 特例:「梅州市」州后紧跟市,并入同一片段;「梅县区」县/旗后紧跟区,并入同一片段。
|
||||
* @param {string} text 已去除空白的文本
|
||||
* @returns {string} 片段,未匹配到返回 ''
|
||||
*/
|
||||
function matchRegionToken(text) {
|
||||
for (let end = 0; end < text.length; end += 1) {
|
||||
const prefix = text.slice(0, end + 1);
|
||||
const suffix = REGION_SUFFIXES.find(item => prefix.endsWith(item));
|
||||
if (!suffix) continue;
|
||||
const next = text[end + 1] || '';
|
||||
if ((suffix === '州' || suffix === '盟') && next === '市') continue;
|
||||
if ((suffix === '县' || suffix === '旗') && next === '区') return text.slice(0, end + 2);
|
||||
return prefix;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 按行政区划后缀把文本切成区划片段。
|
||||
* @param {string} value 原值
|
||||
* @returns {string[]|null} 整串都是行政区划时返回片段数组,否则返回 null
|
||||
*/
|
||||
export function splitRegionTokens(value) {
|
||||
const text = String(value ?? '').replace(/\s+/g, '');
|
||||
if (!text) return null;
|
||||
const tokens = [];
|
||||
let rest = text;
|
||||
while (rest) {
|
||||
const token = matchRegionToken(rest);
|
||||
if (!token) return null;
|
||||
tokens.push(token);
|
||||
rest = rest.slice(token.length);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行政区划串按「市 区县」展示,省/自治区不重复展示。
|
||||
* @param {string} value 原值
|
||||
* @returns {string} 含空格的展示值;非纯行政区划串原样返回
|
||||
*/
|
||||
export function formatRegionAddress(value) {
|
||||
const text = String(value ?? '').trim();
|
||||
if (!text) return '';
|
||||
const tokens = splitRegionTokens(text);
|
||||
if (!tokens || !tokens.length) return text;
|
||||
const display = PROVINCE_SUFFIX.test(tokens[0]) ? tokens.slice(1) : tokens;
|
||||
return (display.length ? display : tokens).join(' ');
|
||||
}
|
||||
|
||||
export default formatRegionAddress;
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 路线变更记录内容格式化
|
||||
*
|
||||
* 存储格式(单行):
|
||||
* YD-20260904-0022:发货地 A → B;到货地 C → D;YD-20260904-0025:发货地 E → F;到货地 G → H
|
||||
*
|
||||
* 展示格式(变更前 / 变更后分行):
|
||||
* YD-20260904-0022:发货地 A →
|
||||
* B
|
||||
* 到货地 C →
|
||||
* D
|
||||
*
|
||||
* 说明:仅用于展示层,不改变已存储的 content 文本(历史数据同样适用)。
|
||||
*/
|
||||
export const formatRouteChangeContent = (content, emptyText = '-') => {
|
||||
if (content === undefined || content === null) return emptyText;
|
||||
const text = String(content).trim();
|
||||
if (!text) return emptyText;
|
||||
|
||||
return text
|
||||
.replace(/\r\n?/g, '\n')
|
||||
// 多条记录之间换行
|
||||
.replace(/[ \t]*[;;][ \t]*/g, '\n')
|
||||
// 变更前地址 / 变更后地址分行(箭头保留在上一行行尾)
|
||||
.replace(/[ \t]*[-—]+>[ \t]*|[ \t]*→[ \t]*/g, ' →\n')
|
||||
.replace(/\n{2,}/g, '\n')
|
||||
.replace(/[ \t]+\n/g, '\n')
|
||||
.trim();
|
||||
};
|
||||
@@ -144,7 +144,7 @@
|
||||
|
||||
<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';
|
||||
@@ -322,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',
|
||||
@@ -388,20 +402,6 @@ export default {
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '详细地址',
|
||||
prop: 'detailAddress',
|
||||
formslot: true,
|
||||
span: 24,
|
||||
minWidth: 220,
|
||||
overHidden: false,
|
||||
maxlength: 255,
|
||||
showWordLimit: true,
|
||||
rules: [
|
||||
{ required: true, message: '请选择详细地址', trigger: 'change' },
|
||||
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '经度',
|
||||
prop: 'longitude',
|
||||
@@ -578,33 +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;
|
||||
@@ -631,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);
|
||||
@@ -642,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 || '')
|
||||
@@ -698,25 +735,265 @@ export default {
|
||||
openAddressMapPicker() {
|
||||
this.addressMapPickerVisible = true;
|
||||
},
|
||||
formatMapCoordinate(value) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return '';
|
||||
}
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number)) {
|
||||
return '';
|
||||
}
|
||||
return normalizeCoordinateInput(number.toFixed(6));
|
||||
},
|
||||
patchDialogForm(patch = {}) {
|
||||
Object.assign(this.form, patch);
|
||||
const tableForm = this.$refs.crud?.tableForm;
|
||||
if (tableForm && tableForm !== this.form) {
|
||||
Object.assign(tableForm, patch);
|
||||
}
|
||||
},
|
||||
handleAddressMapConfirm(payload) {
|
||||
const selection = typeof payload === 'string' ? { address: payload } : payload || {};
|
||||
const longitude = this.formatMapCoordinate(selection.longitude);
|
||||
const latitude = this.formatMapCoordinate(selection.latitude);
|
||||
const patch = {};
|
||||
if (selection.address) {
|
||||
this.form.detailAddress = selection.address;
|
||||
patch.detailAddress = selection.address;
|
||||
}
|
||||
if (longitude !== '' && latitude !== '') {
|
||||
patch.longitude = longitude;
|
||||
patch.latitude = latitude;
|
||||
}
|
||||
if (Object.keys(patch).length) {
|
||||
this.patchDialogForm(patch);
|
||||
}
|
||||
this.applyMapRegionSelection(selection);
|
||||
},
|
||||
normalizeAmapCityName(city, province) {
|
||||
if (Array.isArray(city)) {
|
||||
return String(province || '').trim();
|
||||
}
|
||||
const cityName = String(city || '').trim();
|
||||
return cityName || String(province || '').trim();
|
||||
},
|
||||
buildRegionLookupCodes(adcode) {
|
||||
const code = String(adcode || '').trim();
|
||||
if (!code) {
|
||||
return [];
|
||||
}
|
||||
const codes = [code];
|
||||
if (/^\d{6}$/.test(code)) {
|
||||
if (code.endsWith('00')) {
|
||||
const cityCode = code.slice(0, 4);
|
||||
if (cityCode && !codes.includes(cityCode)) {
|
||||
codes.push(cityCode);
|
||||
}
|
||||
}
|
||||
if (code.endsWith('0000')) {
|
||||
const provinceCode = code.slice(0, 2);
|
||||
if (provinceCode && !codes.includes(provinceCode)) {
|
||||
codes.push(provinceCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
return codes;
|
||||
},
|
||||
fetchRegionDetailByCodes(codes = []) {
|
||||
const tryNext = index => {
|
||||
if (index >= codes.length) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return getRegionDetail(codes[index])
|
||||
.then(res => {
|
||||
const detail = res.data && res.data.data;
|
||||
if (detail && detail.code) {
|
||||
return detail;
|
||||
}
|
||||
return tryNext(index + 1);
|
||||
})
|
||||
.catch(() => tryNext(index + 1));
|
||||
};
|
||||
return tryNext(0);
|
||||
},
|
||||
resolveProvinceCode(provinceName) {
|
||||
const name = String(provinceName || '').trim();
|
||||
if (!name) {
|
||||
return '';
|
||||
}
|
||||
const province = this.provinceOptions.find(
|
||||
item => item.title === name || item.id === name || String(item.title).includes(name)
|
||||
);
|
||||
return province ? province.id : '';
|
||||
},
|
||||
resolveCityCode(cityName, regionCode) {
|
||||
const name = String(cityName || '').trim();
|
||||
const code = String(regionCode || '').trim();
|
||||
const city = this.cityOptions.find(
|
||||
item => item.title === name || item.id === code || item.id === name
|
||||
);
|
||||
return city ? city.id : '';
|
||||
},
|
||||
resolveDistrictCode(districtName, regionCode) {
|
||||
const name = String(districtName || '').trim();
|
||||
const code = String(regionCode || '').trim();
|
||||
const district = this.districtOptions.find(
|
||||
item => item.title === name || item.id === code || item.id === name
|
||||
);
|
||||
return district ? district.id : code || '';
|
||||
},
|
||||
applyRegionDetail(detail = {}) {
|
||||
const provinceName = detail.provinceName || '';
|
||||
const cityName = detail.cityName || '';
|
||||
const districtName = detail.districtName || detail.name || '';
|
||||
return this.loadProvinceOptions()
|
||||
.then(() => {
|
||||
const provinceCode =
|
||||
String(detail.provinceCode || '').trim() || this.resolveProvinceCode(provinceName);
|
||||
const province = this.provinceOptions.find(item => item.id === provinceCode);
|
||||
this.patchDialogForm({
|
||||
provinceCode: provinceCode || undefined,
|
||||
provinceName: province ? province.title : provinceName,
|
||||
});
|
||||
if (!provinceCode) {
|
||||
return null;
|
||||
}
|
||||
return this.loadCityOptions(provinceCode);
|
||||
})
|
||||
.then(cities => {
|
||||
if (!cities) {
|
||||
return null;
|
||||
}
|
||||
const cityCode =
|
||||
String(detail.cityCode || '').trim() ||
|
||||
this.resolveCityCode(cityName, detail.districtCode || detail.code);
|
||||
const city = this.cityOptions.find(item => item.id === cityCode);
|
||||
this.patchDialogForm({
|
||||
cityCode: cityCode || undefined,
|
||||
cityName: city ? city.title : cityName,
|
||||
});
|
||||
this.ensureCityOption({
|
||||
cityCode,
|
||||
cityName: city ? city.title : cityName,
|
||||
});
|
||||
if (!cityCode) {
|
||||
return null;
|
||||
}
|
||||
return this.loadDistrictOptions(cityCode);
|
||||
})
|
||||
.then(districts => {
|
||||
if (!districts) {
|
||||
return;
|
||||
}
|
||||
const districtCode = this.resolveDistrictCode(
|
||||
districtName,
|
||||
detail.districtCode || detail.code
|
||||
);
|
||||
this.patchDialogForm({
|
||||
districtCode: districtCode || undefined,
|
||||
});
|
||||
this.handleDistrictChange(districtCode);
|
||||
this.ensureDistrictOption({
|
||||
districtCode,
|
||||
districtName: this.form.districtName || districtName,
|
||||
});
|
||||
});
|
||||
},
|
||||
applyMapRegionByNames(selection = {}) {
|
||||
const provinceName = String(selection.province || selection.provinceName || '').trim();
|
||||
const cityName = this.normalizeAmapCityName(
|
||||
selection.city || selection.cityName,
|
||||
provinceName
|
||||
);
|
||||
const districtName = String(selection.district || selection.districtName || '').trim();
|
||||
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||
// 本页无国家字段:国内 adcode 走中国省份树;无匹配则不强制回填
|
||||
if (
|
||||
selection.longitude !== undefined &&
|
||||
selection.longitude !== null &&
|
||||
selection.longitude !== ''
|
||||
!provinceName &&
|
||||
!cityName &&
|
||||
!districtName &&
|
||||
!(/^\d{6}$/.test(adcode) || String(selection.country || selection.countryName || '').includes('中国'))
|
||||
) {
|
||||
this.form.longitude = Number(selection.longitude).toFixed(6);
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (
|
||||
selection.latitude !== undefined &&
|
||||
selection.latitude !== null &&
|
||||
selection.latitude !== ''
|
||||
) {
|
||||
this.form.latitude = Number(selection.latitude).toFixed(6);
|
||||
return this.loadProvinceOptions()
|
||||
.then(() => {
|
||||
const provinceCode = this.resolveProvinceCode(provinceName);
|
||||
const province = this.provinceOptions.find(item => item.id === provinceCode);
|
||||
this.patchDialogForm({
|
||||
provinceCode: provinceCode || undefined,
|
||||
provinceName: province ? province.title : provinceName,
|
||||
});
|
||||
if (!provinceCode) {
|
||||
return null;
|
||||
}
|
||||
return this.loadCityOptions(provinceCode);
|
||||
})
|
||||
.then(cities => {
|
||||
if (!cities) {
|
||||
return null;
|
||||
}
|
||||
const cityCode = this.resolveCityCode(cityName, adcode);
|
||||
const city = this.cityOptions.find(item => item.id === cityCode);
|
||||
this.patchDialogForm({
|
||||
cityCode: cityCode || undefined,
|
||||
cityName: city ? city.title : cityName,
|
||||
});
|
||||
this.ensureCityOption({
|
||||
cityCode,
|
||||
cityName: city ? city.title : cityName,
|
||||
});
|
||||
if (!cityCode) {
|
||||
return null;
|
||||
}
|
||||
return this.loadDistrictOptions(cityCode);
|
||||
})
|
||||
.then(districts => {
|
||||
if (!districts) {
|
||||
return;
|
||||
}
|
||||
const districtCode = this.resolveDistrictCode(districtName, adcode);
|
||||
this.patchDialogForm({
|
||||
districtCode: districtCode || undefined,
|
||||
});
|
||||
this.handleDistrictChange(districtCode);
|
||||
this.ensureDistrictOption({
|
||||
districtCode,
|
||||
districtName: this.form.districtName || districtName,
|
||||
});
|
||||
});
|
||||
},
|
||||
applyMapRegionSelection(selection = {}) {
|
||||
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||
const hasNameHint = Boolean(
|
||||
selection.country ||
|
||||
selection.countryName ||
|
||||
selection.province ||
|
||||
selection.provinceName ||
|
||||
selection.city ||
|
||||
selection.cityName ||
|
||||
selection.district ||
|
||||
selection.districtName
|
||||
);
|
||||
if (!adcode && !hasNameHint) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.regionInitializing = true;
|
||||
const lookupCodes = this.buildRegionLookupCodes(adcode);
|
||||
return this.fetchRegionDetailByCodes(lookupCodes)
|
||||
.then(detail => {
|
||||
if (detail) {
|
||||
return this.applyRegionDetail(detail);
|
||||
}
|
||||
return this.applyMapRegionByNames(selection);
|
||||
})
|
||||
.catch(() => this.applyMapRegionByNames(selection))
|
||||
.finally(() => {
|
||||
this.$nextTick(() => {
|
||||
window.setTimeout(() => {
|
||||
this.regionInitializing = false;
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
},
|
||||
normalizeRow(row) {
|
||||
row.code = row.iataCode ? `JC-${row.iataCode}` : row.code;
|
||||
|
||||
@@ -344,15 +344,21 @@ export default {
|
||||
getDeptTree(this.userInfo.tenantId).then(res => {
|
||||
this.deptOptions = this.flattenDept(res.data.data || []);
|
||||
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
|
||||
deptColumn.dicData = this.deptOptions;
|
||||
if (deptColumn) {
|
||||
deptColumn.dicData = this.deptOptions;
|
||||
}
|
||||
});
|
||||
},
|
||||
flattenDept(tree, level = 0) {
|
||||
const result = [];
|
||||
tree.forEach(item => {
|
||||
// 雪花 ID 必须保持字符串,避免 JS Number 精度丢失导致筛错组织。
|
||||
const id =
|
||||
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
|
||||
if (!id) return;
|
||||
result.push({
|
||||
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
||||
value: item.id,
|
||||
value: id,
|
||||
rawLabel: item.title || item.deptName || item.name,
|
||||
});
|
||||
if (item.children && item.children.length) {
|
||||
@@ -361,6 +367,19 @@ export default {
|
||||
});
|
||||
return result;
|
||||
},
|
||||
normalizeListQuery(params = {}) {
|
||||
// 搜索条件优先于缓存的 query,避免旧组织 ID 覆盖用户新选择的组织。
|
||||
const query = { ...this.query, ...params };
|
||||
const rawDeptId = query.deptId ?? query.searchDeptId;
|
||||
delete query.searchDeptId;
|
||||
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
|
||||
query.deptId = String(rawDeptId).split(',')[0].trim();
|
||||
} else {
|
||||
delete query.deptId;
|
||||
}
|
||||
query.allDept = this.isAdmin ? 1 : 0;
|
||||
return query;
|
||||
},
|
||||
getRecords(res) {
|
||||
const data = res && res.data && res.data.data;
|
||||
if (Array.isArray(data)) return data;
|
||||
@@ -967,13 +986,15 @@ export default {
|
||||
},
|
||||
searchReset() {
|
||||
this.query = {};
|
||||
this.allDept = this.isAdmin ? 1 : 0;
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
searchChange(params, done) {
|
||||
this.query = params;
|
||||
this.query = this.normalizeListQuery(params);
|
||||
this.allDept = this.query.allDept;
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page, params);
|
||||
this.onLoad(this.page, this.query);
|
||||
done();
|
||||
},
|
||||
selectionChange(list) {
|
||||
@@ -994,7 +1015,10 @@ export default {
|
||||
},
|
||||
onLoad(page, params = {}) {
|
||||
this.loading = true;
|
||||
getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: this.allDept })
|
||||
const query = this.normalizeListQuery(params);
|
||||
this.query = query;
|
||||
this.allDept = query.allDept;
|
||||
getList(page.currentPage, page.pageSize, query)
|
||||
.then(res => {
|
||||
const data = res.data.data;
|
||||
this.page.total = data.total;
|
||||
@@ -1027,8 +1051,7 @@ export default {
|
||||
},
|
||||
buildExportParams() {
|
||||
return {
|
||||
...this.query,
|
||||
allDept: this.allDept,
|
||||
...this.normalizeListQuery(this.query),
|
||||
ids: this.ids,
|
||||
[this.website.tokenHeader]: getToken(),
|
||||
};
|
||||
|
||||
@@ -264,7 +264,7 @@ 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';
|
||||
@@ -584,7 +584,7 @@ export default {
|
||||
span: 12,
|
||||
minWidth: 220,
|
||||
overHidden: false,
|
||||
order: 85,
|
||||
order: 90,
|
||||
placeholder: '点击选择地图地址',
|
||||
maxlength: 255,
|
||||
showWordLimit: true,
|
||||
@@ -841,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);
|
||||
@@ -925,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;
|
||||
})
|
||||
@@ -1047,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) {
|
||||
@@ -1060,17 +1115,224 @@ export default {
|
||||
openAddressMapPicker() {
|
||||
this.addressMapPickerVisible = true;
|
||||
},
|
||||
formatMapCoordinate(value) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return '';
|
||||
}
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number)) {
|
||||
return '';
|
||||
}
|
||||
return normalizeCoordinateInput(number.toFixed(6));
|
||||
},
|
||||
patchDialogForm(patch = {}) {
|
||||
// 同步页面 form 与 Avue 弹窗内部 tableForm,避免只改了一边导致经纬度不刷新
|
||||
Object.assign(this.form, patch);
|
||||
const tableForm = this.$refs.crud?.tableForm;
|
||||
if (tableForm && tableForm !== this.form) {
|
||||
Object.assign(tableForm, patch);
|
||||
}
|
||||
},
|
||||
handleAddressMapConfirm(payload) {
|
||||
const selection = typeof payload === 'string' ? { address: payload } : payload || {};
|
||||
const longitude = this.formatMapCoordinate(selection.longitude);
|
||||
const latitude = this.formatMapCoordinate(selection.latitude);
|
||||
const patch = {};
|
||||
if (selection.address) {
|
||||
this.form.detailAddress = selection.address;
|
||||
patch.detailAddress = selection.address;
|
||||
}
|
||||
if (selection.longitude !== undefined && selection.longitude !== null && selection.longitude !== '') {
|
||||
this.form.longitude = Number(selection.longitude).toFixed(6);
|
||||
// 换地址时必须同步覆盖经纬度,不能保留上一次选点
|
||||
if (longitude !== '' && latitude !== '') {
|
||||
patch.longitude = longitude;
|
||||
patch.latitude = latitude;
|
||||
}
|
||||
if (selection.latitude !== undefined && selection.latitude !== null && selection.latitude !== '') {
|
||||
this.form.latitude = Number(selection.latitude).toFixed(6);
|
||||
if (Object.keys(patch).length) {
|
||||
this.patchDialogForm(patch);
|
||||
}
|
||||
this.applyMapRegionSelection(selection);
|
||||
},
|
||||
normalizeAmapCityName(city, province) {
|
||||
if (Array.isArray(city)) {
|
||||
return String(province || '').trim();
|
||||
}
|
||||
const cityName = String(city || '').trim();
|
||||
return cityName || String(province || '').trim();
|
||||
},
|
||||
buildRegionLookupCodes(adcode) {
|
||||
const code = String(adcode || '').trim();
|
||||
if (!code) {
|
||||
return [];
|
||||
}
|
||||
const codes = [code];
|
||||
if (/^\d{6}$/.test(code)) {
|
||||
if (code.endsWith('00')) {
|
||||
const cityCode = code.slice(0, 4);
|
||||
if (cityCode && !codes.includes(cityCode)) {
|
||||
codes.push(cityCode);
|
||||
}
|
||||
}
|
||||
if (code.endsWith('0000')) {
|
||||
const provinceCode = code.slice(0, 2);
|
||||
if (provinceCode && !codes.includes(provinceCode)) {
|
||||
codes.push(provinceCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
return codes;
|
||||
},
|
||||
fetchRegionDetailByCodes(codes = []) {
|
||||
const tryNext = index => {
|
||||
if (index >= codes.length) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return getRegionDetail(codes[index])
|
||||
.then(res => {
|
||||
const detail = res.data && res.data.data;
|
||||
if (detail && detail.code) {
|
||||
return detail;
|
||||
}
|
||||
return tryNext(index + 1);
|
||||
})
|
||||
.catch(() => tryNext(index + 1));
|
||||
};
|
||||
return tryNext(0);
|
||||
},
|
||||
resolveCountryFromRegionDetail(detail = {}) {
|
||||
const ancestors = String(detail.ancestors || '')
|
||||
.split(',')
|
||||
.map(item => String(item || '').trim())
|
||||
.filter(Boolean);
|
||||
for (let index = 0; index < ancestors.length; index += 1) {
|
||||
const ancestorCode = ancestors[index];
|
||||
if (this.countryOptions.some(item => item.id === ancestorCode)) {
|
||||
return ancestorCode;
|
||||
}
|
||||
}
|
||||
if (
|
||||
detail.parentCode &&
|
||||
Number(detail.regionLevel) === 1 &&
|
||||
this.countryOptions.some(item => item.id === String(detail.parentCode))
|
||||
) {
|
||||
return String(detail.parentCode);
|
||||
}
|
||||
if (/^\d{6}$/.test(String(detail.code || ''))) {
|
||||
return DEFAULT_COUNTRY_CODE;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
applyRegionDetail(detail = {}) {
|
||||
const countryHint = this.resolveCountryFromRegionDetail(detail);
|
||||
return this.resolveCountryCode(countryHint || DEFAULT_COUNTRY_CODE)
|
||||
.then(countryCode => {
|
||||
this.form.countryCode = countryCode;
|
||||
this.syncCountryName(countryCode);
|
||||
return this.loadProvinceOptions(countryCode);
|
||||
})
|
||||
.then(() => {
|
||||
this.form.provinceCode =
|
||||
String(detail.provinceCode || '') || this.resolveProvinceCode(detail.provinceName);
|
||||
const province = this.provinceOptions.find(item => item.id === this.form.provinceCode);
|
||||
this.form.provinceName = province ? province.title : detail.provinceName || '';
|
||||
return this.loadCityOptions(this.form.provinceCode);
|
||||
})
|
||||
.then(() => {
|
||||
this.form.cityCode =
|
||||
String(detail.cityCode || '') ||
|
||||
this.resolveCityCode(detail.cityName, detail.districtCode || detail.code);
|
||||
const city = this.cityOptions.find(item => item.id === this.form.cityCode);
|
||||
this.form.city = city ? city.title : detail.cityName || '';
|
||||
return this.loadDistrictOptions(this.form.cityCode);
|
||||
})
|
||||
.then(() => {
|
||||
this.form.districtCode = this.resolveDistrictCode(
|
||||
detail.districtName || detail.name,
|
||||
detail.districtCode || detail.code
|
||||
);
|
||||
this.handleDistrictChange(this.form.districtCode);
|
||||
});
|
||||
},
|
||||
applyMapRegionByNames(selection = {}) {
|
||||
const provinceName = String(selection.province || selection.provinceName || '').trim();
|
||||
const cityName = this.normalizeAmapCityName(
|
||||
selection.city || selection.cityName,
|
||||
provinceName
|
||||
);
|
||||
const districtName = String(selection.district || selection.districtName || '').trim();
|
||||
const countryName = String(selection.country || selection.countryName || '').trim();
|
||||
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||
const countryPromise = countryName
|
||||
? this.resolveCountryCode(countryName)
|
||||
: Promise.resolve(/^\d{6}$/.test(adcode) ? DEFAULT_COUNTRY_CODE : '');
|
||||
return countryPromise
|
||||
.then(countryCode => {
|
||||
if (!countryCode) {
|
||||
return null;
|
||||
}
|
||||
this.form.countryCode = countryCode;
|
||||
this.syncCountryName(countryCode);
|
||||
return this.loadProvinceOptions(countryCode);
|
||||
})
|
||||
.then(provinces => {
|
||||
if (!provinces) {
|
||||
return null;
|
||||
}
|
||||
this.form.provinceCode = this.resolveProvinceCode(provinceName);
|
||||
const province = this.provinceOptions.find(item => item.id === this.form.provinceCode);
|
||||
this.form.provinceName = province ? province.title : provinceName;
|
||||
if (!this.form.provinceCode) {
|
||||
this.clearCityValue();
|
||||
return null;
|
||||
}
|
||||
return this.loadCityOptions(this.form.provinceCode);
|
||||
})
|
||||
.then(cities => {
|
||||
if (!cities) {
|
||||
return null;
|
||||
}
|
||||
this.form.cityCode = this.resolveCityCode(cityName, adcode);
|
||||
const city = this.cityOptions.find(item => item.id === this.form.cityCode);
|
||||
this.form.city = city ? city.title : cityName;
|
||||
if (!this.form.cityCode) {
|
||||
this.clearDistrictValue();
|
||||
return null;
|
||||
}
|
||||
return this.loadDistrictOptions(this.form.cityCode);
|
||||
})
|
||||
.then(districts => {
|
||||
if (!districts) {
|
||||
return;
|
||||
}
|
||||
this.form.districtCode = this.resolveDistrictCode(districtName, adcode);
|
||||
this.handleDistrictChange(this.form.districtCode);
|
||||
});
|
||||
},
|
||||
applyMapRegionSelection(selection = {}) {
|
||||
if (this.isParentRegionLocked) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||
const hasNameHint = Boolean(
|
||||
selection.country ||
|
||||
selection.countryName ||
|
||||
selection.province ||
|
||||
selection.provinceName ||
|
||||
selection.city ||
|
||||
selection.cityName ||
|
||||
selection.district ||
|
||||
selection.districtName
|
||||
);
|
||||
if (!adcode && !hasNameHint) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const lookupCodes = this.buildRegionLookupCodes(adcode);
|
||||
return this.fetchRegionDetailByCodes(lookupCodes)
|
||||
.then(detail => {
|
||||
if (detail) {
|
||||
return this.applyRegionDetail(detail);
|
||||
}
|
||||
return this.applyMapRegionByNames(selection);
|
||||
})
|
||||
.catch(() => this.applyMapRegionByNames(selection));
|
||||
},
|
||||
resolveCountryCode(country) {
|
||||
if (!country) {
|
||||
@@ -1251,13 +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;
|
||||
@@ -1292,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(() => {
|
||||
@@ -1443,42 +1714,14 @@ export default {
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* 港口码头编辑/新增弹窗:对齐 railway-station 统一风格(灰底 + 白卡表单 + 浮动底栏,padding 24px 与全局弹窗标准一致) */
|
||||
//.port-terminal-edit-dialog {
|
||||
// /* 灰底内容区 */
|
||||
// .el-dialog__body {
|
||||
// background-color: #f2f2f2;
|
||||
// padding: 8px;
|
||||
// }
|
||||
// /* 表单白卡(与全局 .el-dialog .avue-form 一致,padding 24px 对齐 customer-archive / railway-station) */
|
||||
// .avue-form {
|
||||
// background: #fff;
|
||||
// border-radius: 6px;
|
||||
// box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
// padding: 24px !important;
|
||||
// margin-bottom: 0;
|
||||
// }
|
||||
// /* 底部按钮区白卡(Avue 弹窗 footer 在 body 内,接续白卡) */
|
||||
// .avue-dialog__footer {
|
||||
// background: #fff;
|
||||
// border-radius: 0 0 6px 6px;
|
||||
// box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
// padding: 16px 24px !important;
|
||||
// display: flex;
|
||||
// justify-content: flex-end;
|
||||
// }
|
||||
// /* label 宽度统一 108px(对齐 railway-station,标签对齐更整齐) */
|
||||
// .el-form-item__label {
|
||||
// width: 108px !important;
|
||||
// }
|
||||
// /* 所有 input 文本框、select 下拉控件统一宽度 250px */
|
||||
// .el-input,
|
||||
// .el-select {
|
||||
// width: 250px !important;
|
||||
// }
|
||||
// /* 备注宽度 85% */
|
||||
// .remark-input {
|
||||
// width: 85% !important;
|
||||
// }
|
||||
//}
|
||||
/* 港口码头编辑/新增弹窗:与全局表单项行距保持一致 */
|
||||
.port-terminal-edit-dialog {
|
||||
.el-form-item,
|
||||
.el-form-item--default.el-form-item,
|
||||
.el-form-item--small.el-form-item,
|
||||
.el-form-item--large.el-form-item,
|
||||
.el-form-item--medium.el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
|
||||
<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';
|
||||
@@ -171,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();
|
||||
@@ -273,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),
|
||||
},
|
||||
@@ -293,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',
|
||||
@@ -360,20 +376,6 @@ export default {
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '详细地址',
|
||||
prop: 'detailAddress',
|
||||
formslot: true,
|
||||
span: 24,
|
||||
minWidth: 220,
|
||||
overHidden: false,
|
||||
maxlength: 255,
|
||||
showWordLimit: true,
|
||||
rules: [
|
||||
{ required: true, message: '请选择详细地址', trigger: 'change' },
|
||||
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '经度',
|
||||
prop: 'longitude',
|
||||
@@ -549,33 +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;
|
||||
@@ -681,28 +701,269 @@ export default {
|
||||
openAddressMapPicker() {
|
||||
this.addressMapPickerVisible = true;
|
||||
},
|
||||
formatMapCoordinate(value) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return '';
|
||||
}
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number)) {
|
||||
return '';
|
||||
}
|
||||
return normalizeCoordinateInput(number.toFixed(6));
|
||||
},
|
||||
patchDialogForm(patch = {}) {
|
||||
Object.assign(this.form, patch);
|
||||
const tableForm = this.$refs.crud?.tableForm;
|
||||
if (tableForm && tableForm !== this.form) {
|
||||
Object.assign(tableForm, patch);
|
||||
}
|
||||
},
|
||||
handleAddressMapConfirm(payload) {
|
||||
const selection = typeof payload === 'string' ? { address: payload } : payload || {};
|
||||
const longitude = this.formatMapCoordinate(selection.longitude);
|
||||
const latitude = this.formatMapCoordinate(selection.latitude);
|
||||
const patch = {};
|
||||
if (selection.address) {
|
||||
this.form.detailAddress = selection.address;
|
||||
patch.detailAddress = selection.address;
|
||||
}
|
||||
if (longitude !== '' && latitude !== '') {
|
||||
patch.longitude = longitude;
|
||||
patch.latitude = latitude;
|
||||
}
|
||||
if (Object.keys(patch).length) {
|
||||
this.patchDialogForm(patch);
|
||||
}
|
||||
this.applyMapRegionSelection(selection);
|
||||
},
|
||||
normalizeAmapCityName(city, province) {
|
||||
if (Array.isArray(city)) {
|
||||
return String(province || '').trim();
|
||||
}
|
||||
const cityName = String(city || '').trim();
|
||||
return cityName || String(province || '').trim();
|
||||
},
|
||||
buildRegionLookupCodes(adcode) {
|
||||
const code = String(adcode || '').trim();
|
||||
if (!code) {
|
||||
return [];
|
||||
}
|
||||
const codes = [code];
|
||||
if (/^\d{6}$/.test(code)) {
|
||||
if (code.endsWith('00')) {
|
||||
const cityCode = code.slice(0, 4);
|
||||
if (cityCode && !codes.includes(cityCode)) {
|
||||
codes.push(cityCode);
|
||||
}
|
||||
}
|
||||
if (code.endsWith('0000')) {
|
||||
const provinceCode = code.slice(0, 2);
|
||||
if (provinceCode && !codes.includes(provinceCode)) {
|
||||
codes.push(provinceCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
return codes;
|
||||
},
|
||||
fetchRegionDetailByCodes(codes = []) {
|
||||
const tryNext = index => {
|
||||
if (index >= codes.length) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return getRegionDetail(codes[index])
|
||||
.then(res => {
|
||||
const detail = res.data && res.data.data;
|
||||
if (detail && detail.code) {
|
||||
return detail;
|
||||
}
|
||||
return tryNext(index + 1);
|
||||
})
|
||||
.catch(() => tryNext(index + 1));
|
||||
};
|
||||
return tryNext(0);
|
||||
},
|
||||
resolveProvinceCode(provinceName) {
|
||||
const name = String(provinceName || '').trim();
|
||||
if (!name) {
|
||||
return '';
|
||||
}
|
||||
const province = this.provinceOptions.find(
|
||||
item => item.title === name || item.id === name || String(item.title).includes(name)
|
||||
);
|
||||
return province ? province.id : '';
|
||||
},
|
||||
resolveCityCode(cityName, regionCode) {
|
||||
const name = String(cityName || '').trim();
|
||||
const code = String(regionCode || '').trim();
|
||||
const city = this.cityOptions.find(
|
||||
item => item.title === name || item.id === code || item.id === name
|
||||
);
|
||||
return city ? city.id : '';
|
||||
},
|
||||
resolveDistrictCode(districtName, regionCode) {
|
||||
const name = String(districtName || '').trim();
|
||||
const code = String(regionCode || '').trim();
|
||||
const district = this.districtOptions.find(
|
||||
item => item.title === name || item.id === code || item.id === name
|
||||
);
|
||||
return district ? district.id : code || '';
|
||||
},
|
||||
applyRegionDetail(detail = {}) {
|
||||
const provinceName = detail.provinceName || '';
|
||||
const cityName = detail.cityName || '';
|
||||
const districtName = detail.districtName || detail.name || '';
|
||||
return this.loadProvinceOptions()
|
||||
.then(() => {
|
||||
const provinceCode =
|
||||
String(detail.provinceCode || '').trim() || this.resolveProvinceCode(provinceName);
|
||||
const province = this.provinceOptions.find(item => item.id === provinceCode);
|
||||
this.patchDialogForm({
|
||||
provinceCode: provinceCode || undefined,
|
||||
provinceName: province ? province.title : provinceName,
|
||||
});
|
||||
if (!provinceCode) {
|
||||
return null;
|
||||
}
|
||||
return this.loadCityOptions(provinceCode);
|
||||
})
|
||||
.then(cities => {
|
||||
if (!cities) {
|
||||
return null;
|
||||
}
|
||||
const cityCode =
|
||||
String(detail.cityCode || '').trim() ||
|
||||
this.resolveCityCode(cityName, detail.districtCode || detail.code);
|
||||
const city = this.cityOptions.find(item => item.id === cityCode);
|
||||
this.patchDialogForm({
|
||||
cityCode: cityCode || undefined,
|
||||
cityName: city ? city.title : cityName,
|
||||
});
|
||||
this.ensureCityOption({
|
||||
cityCode,
|
||||
cityName: city ? city.title : cityName,
|
||||
});
|
||||
if (!cityCode) {
|
||||
return null;
|
||||
}
|
||||
return this.loadDistrictOptions(cityCode);
|
||||
})
|
||||
.then(districts => {
|
||||
if (!districts) {
|
||||
return;
|
||||
}
|
||||
const districtCode = this.resolveDistrictCode(
|
||||
districtName,
|
||||
detail.districtCode || detail.code
|
||||
);
|
||||
this.patchDialogForm({
|
||||
districtCode: districtCode || undefined,
|
||||
});
|
||||
this.handleDistrictChange(districtCode);
|
||||
this.ensureDistrictOption({
|
||||
districtCode,
|
||||
districtName: this.form.districtName || districtName,
|
||||
});
|
||||
});
|
||||
},
|
||||
applyMapRegionByNames(selection = {}) {
|
||||
const provinceName = String(selection.province || selection.provinceName || '').trim();
|
||||
const cityName = this.normalizeAmapCityName(
|
||||
selection.city || selection.cityName,
|
||||
provinceName
|
||||
);
|
||||
const districtName = String(selection.district || selection.districtName || '').trim();
|
||||
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||
// 本页无国家字段:国内 adcode 走中国省份树;无匹配则不强制回填
|
||||
if (
|
||||
selection.longitude !== undefined &&
|
||||
selection.longitude !== null &&
|
||||
selection.longitude !== ''
|
||||
!provinceName &&
|
||||
!cityName &&
|
||||
!districtName &&
|
||||
!(/^\d{6}$/.test(adcode) || String(selection.country || selection.countryName || '').includes('中国'))
|
||||
) {
|
||||
this.form.longitude = Number(selection.longitude).toFixed(6);
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (
|
||||
selection.latitude !== undefined &&
|
||||
selection.latitude !== null &&
|
||||
selection.latitude !== ''
|
||||
) {
|
||||
this.form.latitude = Number(selection.latitude).toFixed(6);
|
||||
return this.loadProvinceOptions()
|
||||
.then(() => {
|
||||
const provinceCode = this.resolveProvinceCode(provinceName);
|
||||
const province = this.provinceOptions.find(item => item.id === provinceCode);
|
||||
this.patchDialogForm({
|
||||
provinceCode: provinceCode || undefined,
|
||||
provinceName: province ? province.title : provinceName,
|
||||
});
|
||||
if (!provinceCode) {
|
||||
return null;
|
||||
}
|
||||
return this.loadCityOptions(provinceCode);
|
||||
})
|
||||
.then(cities => {
|
||||
if (!cities) {
|
||||
return null;
|
||||
}
|
||||
const cityCode = this.resolveCityCode(cityName, adcode);
|
||||
const city = this.cityOptions.find(item => item.id === cityCode);
|
||||
this.patchDialogForm({
|
||||
cityCode: cityCode || undefined,
|
||||
cityName: city ? city.title : cityName,
|
||||
});
|
||||
this.ensureCityOption({
|
||||
cityCode,
|
||||
cityName: city ? city.title : cityName,
|
||||
});
|
||||
if (!cityCode) {
|
||||
return null;
|
||||
}
|
||||
return this.loadDistrictOptions(cityCode);
|
||||
})
|
||||
.then(districts => {
|
||||
if (!districts) {
|
||||
return;
|
||||
}
|
||||
const districtCode = this.resolveDistrictCode(districtName, adcode);
|
||||
this.patchDialogForm({
|
||||
districtCode: districtCode || undefined,
|
||||
});
|
||||
this.handleDistrictChange(districtCode);
|
||||
this.ensureDistrictOption({
|
||||
districtCode,
|
||||
districtName: this.form.districtName || districtName,
|
||||
});
|
||||
});
|
||||
},
|
||||
applyMapRegionSelection(selection = {}) {
|
||||
const adcode = String(selection.adcode || selection.regionCode || '').trim();
|
||||
const hasNameHint = Boolean(
|
||||
selection.country ||
|
||||
selection.countryName ||
|
||||
selection.province ||
|
||||
selection.provinceName ||
|
||||
selection.city ||
|
||||
selection.cityName ||
|
||||
selection.district ||
|
||||
selection.districtName
|
||||
);
|
||||
if (!adcode && !hasNameHint) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.regionInitializing = true;
|
||||
const lookupCodes = this.buildRegionLookupCodes(adcode);
|
||||
return this.fetchRegionDetailByCodes(lookupCodes)
|
||||
.then(detail => {
|
||||
if (detail) {
|
||||
return this.applyRegionDetail(detail);
|
||||
}
|
||||
return this.applyMapRegionByNames(selection);
|
||||
})
|
||||
.catch(() => this.applyMapRegionByNames(selection))
|
||||
.finally(() => {
|
||||
this.$nextTick(() => {
|
||||
window.setTimeout(() => {
|
||||
this.regionInitializing = false;
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
},
|
||||
normalizeRow(row) {
|
||||
row.code = row.tmisCode ? `TL${row.tmisCode}` : row.code;
|
||||
row.telegraphCode = String(row.telegraphCode || '').trim() || undefined;
|
||||
row.regionCode = String(row.regionCode || '').trim();
|
||||
row.regionName = String(row.regionName || '').trim();
|
||||
row.districtCode = String(row.districtCode || '').trim();
|
||||
@@ -735,14 +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();
|
||||
|
||||
@@ -787,6 +787,6 @@ export default {
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
.el-form-item{
|
||||
margin-bottom: 9px !important;
|
||||
margin-bottom: 22px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -352,7 +352,8 @@ export default {
|
||||
return result;
|
||||
},
|
||||
normalizeListQuery(params = {}) {
|
||||
const query = { ...params, ...this.query };
|
||||
// 搜索条件优先于缓存的 query,避免默认组织 ID 覆盖用户新选择的组织。
|
||||
const query = { ...this.query, ...params };
|
||||
const rawDeptId = query.deptId ?? query.searchDeptId;
|
||||
delete query.searchDeptId;
|
||||
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
|
||||
|
||||
@@ -415,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));
|
||||
@@ -436,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();
|
||||
@@ -999,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) {
|
||||
@@ -1024,12 +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: this.isAdmin ? 1 : 0,
|
||||
})
|
||||
.getList(page.currentPage, page.pageSize, query)
|
||||
.then(res => {
|
||||
const result = res.data.data;
|
||||
this.page.total = result.total;
|
||||
@@ -1042,8 +1067,7 @@ export default {
|
||||
},
|
||||
buildExportParams() {
|
||||
return {
|
||||
...this.query,
|
||||
allDept: this.isAdmin ? 1 : 0,
|
||||
...this.normalizeListQuery(this.query),
|
||||
ids: this.ids,
|
||||
[this.website.tokenHeader]: getToken(),
|
||||
};
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
:label="item.label"
|
||||
:value="item.value" /></el-select></template
|
||||
></el-table-column>
|
||||
<el-table-column label="单价(元)" width="180"
|
||||
<el-table-column :label="unitPriceColumnLabel" width="180"
|
||||
><template #default="{ row }"
|
||||
><div v-if="readonly && usesRangeUnitPrice(row)" class="limit-list">
|
||||
<span v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex">{{
|
||||
@@ -379,6 +379,7 @@ import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import SectionCard from '@/components/section-card/main.vue';
|
||||
import { currencyUnit } from '@/utils/currency';
|
||||
|
||||
const clone = value => JSON.parse(JSON.stringify(value));
|
||||
/** 计费要素与计量单位维度的对应关系(这些要素可选计费单位) */
|
||||
@@ -386,12 +387,15 @@ const BILLING_ELEMENT_DIMENSION_MAP = {
|
||||
按重量: '重量',
|
||||
按体积: '体积',
|
||||
按数量: '数量',
|
||||
按里程: '里程',
|
||||
'按吨·公里': '吨·公里',
|
||||
'固定金额(整单一口价)': '整车',
|
||||
};
|
||||
const BILLING_UNIT_SELECTABLE_ELEMENTS = Object.keys(BILLING_ELEMENT_DIMENSION_MAP);
|
||||
/** 固定计费单位(不可编辑) */
|
||||
const FIXED_BILLING_UNIT_MAP = {
|
||||
/** 对应维度下无计量单位时的默认计费单位 */
|
||||
const DEFAULT_BILLING_UNIT_MAP = {
|
||||
按里程: '公里',
|
||||
'按吨·公里': '吨公里',
|
||||
'按吨·公里': '吨·公里',
|
||||
'固定金额(整单一口价)': '单',
|
||||
};
|
||||
const defaultRule = () => ({
|
||||
@@ -428,6 +432,7 @@ export default {
|
||||
value: { type: Object, default: () => ({}) },
|
||||
index: { type: Number, default: -1 },
|
||||
readonly: Boolean,
|
||||
settlementCurrency: { type: String, default: 'RMB' },
|
||||
},
|
||||
emits: ['update:modelValue', 'save'],
|
||||
data() {
|
||||
@@ -497,6 +502,9 @@ export default {
|
||||
emitPath: true,
|
||||
};
|
||||
},
|
||||
unitPriceColumnLabel() {
|
||||
return `单价(${currencyUnit(this.settlementCurrency)})`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadDictionaries();
|
||||
@@ -652,58 +660,61 @@ export default {
|
||||
}),
|
||||
]).finally(() => {
|
||||
this.unitLoading = false;
|
||||
(this.draft.rules || []).forEach(row => this.syncBillingUnit(row));
|
||||
});
|
||||
},
|
||||
measurementDimension(billingElement) {
|
||||
return BILLING_ELEMENT_DIMENSION_MAP[billingElement] || '';
|
||||
},
|
||||
fixedBillingUnit(billingElement) {
|
||||
return FIXED_BILLING_UNIT_MAP[String(billingElement || '').trim()] || '';
|
||||
defaultBillingUnit(billingElement) {
|
||||
return DEFAULT_BILLING_UNIT_MAP[String(billingElement || '').trim()] || '';
|
||||
},
|
||||
canSelectBillingUnit(row) {
|
||||
return BILLING_UNIT_SELECTABLE_ELEMENTS.includes(String(row?.billingElement || '').trim());
|
||||
},
|
||||
billingUnitPlaceholder(row) {
|
||||
if (!row?.billingElement) return '请先选择计费要素';
|
||||
if (this.fixedBillingUnit(row.billingElement)) return this.fixedBillingUnit(row.billingElement);
|
||||
if (!this.canSelectBillingUnit(row)) return '当前计费要素无需选择';
|
||||
return '请选择';
|
||||
},
|
||||
unitOptionsFor(row) {
|
||||
const fixedUnit = this.fixedBillingUnit(row?.billingElement);
|
||||
if (fixedUnit) {
|
||||
return [{ id: fixedUnit, label: fixedUnit, value: fixedUnit }];
|
||||
}
|
||||
if (!this.canSelectBillingUnit(row)) {
|
||||
return [];
|
||||
}
|
||||
const dimension = this.measurementDimension(row?.billingElement);
|
||||
if (dimension) {
|
||||
return this.measurementUnits
|
||||
.filter(item => String(item.dimension || '').trim() === dimension)
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
label: item.unitName,
|
||||
value: item.unitName,
|
||||
}))
|
||||
.filter(item => item.value);
|
||||
const defaultUnit = this.defaultBillingUnit(row?.billingElement);
|
||||
const options = dimension
|
||||
? this.measurementUnits
|
||||
.filter(item => String(item.dimension || '').trim() === dimension)
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
label: item.unitName,
|
||||
value: item.unitName,
|
||||
}))
|
||||
.filter(item => item.value)
|
||||
: [];
|
||||
// 对应维度未配置计量单位时,下拉仅展示默认值
|
||||
if (!options.length && defaultUnit) {
|
||||
return [{ id: `default-${defaultUnit}`, label: defaultUnit, value: defaultUnit }];
|
||||
}
|
||||
return [];
|
||||
return options;
|
||||
},
|
||||
syncBillingUnit(row) {
|
||||
const fixedUnit = this.fixedBillingUnit(row?.billingElement);
|
||||
if (fixedUnit) {
|
||||
row.billingUnit = fixedUnit;
|
||||
return;
|
||||
}
|
||||
if (!this.canSelectBillingUnit(row)) {
|
||||
row.billingUnit = '';
|
||||
return;
|
||||
}
|
||||
const options = this.unitOptionsFor(row);
|
||||
if (!options.some(item => String(item.value) === String(row.billingUnit || ''))) {
|
||||
row.billingUnit = '';
|
||||
const defaultUnit = this.defaultBillingUnit(row?.billingElement);
|
||||
const current = String(row.billingUnit || '').trim();
|
||||
if (options.some(item => String(item.value) === current)) {
|
||||
return;
|
||||
}
|
||||
if (defaultUnit && options.some(item => String(item.value) === defaultUnit)) {
|
||||
row.billingUnit = defaultUnit;
|
||||
return;
|
||||
}
|
||||
row.billingUnit = options[0]?.value || defaultUnit || '';
|
||||
},
|
||||
feeTypeKey(row) {
|
||||
const option = this.feeCategories.find(
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
<el-option
|
||||
v-for="item in contractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -2065,11 +2065,22 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="200" align="left" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="200"
|
||||
align="left"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ row.originalName || row.name }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ row.originalName || row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220" align="center">
|
||||
@@ -2338,14 +2349,16 @@
|
||||
<div class="business-crud-page__transport-plan-detail-field is-attachments">
|
||||
<span>附件</span>
|
||||
<div class="business-crud-page__transport-plan-detail-attachments">
|
||||
<el-link
|
||||
<span
|
||||
v-for="file in attachmentRows"
|
||||
:key="file.url || file.name || file.originalName"
|
||||
type="primary"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(file)"
|
||||
>
|
||||
{{ attachmentName(file) }}
|
||||
</el-link>
|
||||
@keydown.enter="previewAttachment(file)"
|
||||
@keydown.space.prevent="previewAttachment(file)"
|
||||
>{{ attachmentName(file) }}</span>
|
||||
<span v-if="!attachmentRows.length">-</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2531,14 +2544,16 @@
|
||||
<div class="business-crud-page__waybill-attachments">
|
||||
<span class="business-crud-page__waybill-label">附件</span>
|
||||
<div>
|
||||
<el-link
|
||||
<span
|
||||
v-for="file in attachmentRows"
|
||||
:key="file.url || file.name || file.originalName"
|
||||
type="primary"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(file)"
|
||||
>
|
||||
{{ file.originalName || file.name || '-' }}
|
||||
</el-link>
|
||||
@keydown.enter="previewAttachment(file)"
|
||||
@keydown.space.prevent="previewAttachment(file)"
|
||||
>{{ file.originalName || file.name || '-' }}</span>
|
||||
<span v-if="!attachmentRows.length">-</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2820,11 +2835,21 @@
|
||||
</div>
|
||||
<el-table :data="attachmentRows" empty-text="暂无附件">
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="260">
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="260"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -2968,9 +2993,10 @@
|
||||
<el-table-column prop="changeUser" label="变更人" min-width="120" />
|
||||
<el-table-column prop="content" label="变更内容" min-width="360">
|
||||
<template #default="{ row }">
|
||||
<div class="business-crud-page__route-change-record-content">
|
||||
{{ row.content || '-' }}
|
||||
</div>
|
||||
<div
|
||||
class="business-crud-page__route-change-record-content"
|
||||
v-text="formatRouteChangeContent(row.content)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
|
||||
@@ -3056,7 +3082,7 @@
|
||||
<el-option
|
||||
v-for="item in transportPlanImportContractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -4225,11 +4251,22 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="200" align="center" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="200"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row, dispatchItemAttachmentRows)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
@keydown.enter="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
@keydown.space.prevent="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220" align="center">
|
||||
@@ -5242,8 +5279,11 @@ import { addressTypeOptions } from '@/option/base/common-address';
|
||||
import { packageOptions } from '@/option/business/common';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||
import { formatRouteChangeContent } from '@/utils/route-change';
|
||||
import { applyTableMenuWidth } from '@/utils/table-menu';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
@@ -5822,7 +5862,7 @@ export default {
|
||||
billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'],
|
||||
reconciliationModeOptions: ['明细逐行核对', '按账单汇总核对', '跳过自动核对'],
|
||||
packageOptions,
|
||||
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
|
||||
quantityUnitOptions: [],
|
||||
attachmentFileTypes: [
|
||||
'pdf',
|
||||
'bmp',
|
||||
@@ -5865,7 +5905,6 @@ export default {
|
||||
this.ensureBillingUnitOptions();
|
||||
}
|
||||
if (this.config.enableTaskInfoForm) {
|
||||
this.loadTaskQuantityUnitOptions();
|
||||
this.loadTaskFeeUnitOptions();
|
||||
this.ensureBillingCargoTypeOptions();
|
||||
}
|
||||
@@ -5886,6 +5925,7 @@ export default {
|
||||
} else {
|
||||
this.consumeTemplateCreatePayload();
|
||||
}
|
||||
this.loadTaskQuantityUnitOptions();
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
@@ -6589,6 +6629,8 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
formatRouteChangeContent,
|
||||
initStandaloneFormPage() {
|
||||
if (!this.isStandaloneFormPage) return;
|
||||
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
||||
@@ -7789,8 +7831,6 @@ export default {
|
||||
['contractName', '客户合同'],
|
||||
['planName', '计划名称'],
|
||||
['transportType', '运输方式'],
|
||||
['planStartDate', '计划开始日期'],
|
||||
['planEndDate', '计划结束日期'],
|
||||
['departureAddress', '发货地址'],
|
||||
['arrivalAddress', '收货地址'],
|
||||
];
|
||||
@@ -9270,9 +9310,10 @@ export default {
|
||||
return Promise.resolve(this.taskQuantityUnitOptions);
|
||||
}
|
||||
this.taskQuantityUnitLoading = true;
|
||||
return getDictionary({ code: 'quantity_unit' })
|
||||
.then(res => {
|
||||
this.taskQuantityUnitOptions = this.normalizeDictOptions(res.data.data || []);
|
||||
return loadMeasurementUnitOptions()
|
||||
.then(options => {
|
||||
this.taskQuantityUnitOptions = options;
|
||||
this.quantityUnitOptions = options.map(item => item.value);
|
||||
return this.taskQuantityUnitOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -14111,7 +14152,7 @@ export default {
|
||||
|
||||
&__dispatch-shipping-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14634,7 +14675,7 @@ export default {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-select),
|
||||
@@ -15860,6 +15901,9 @@ export default {
|
||||
|
||||
&__route-change-record-content {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
@@ -16251,7 +16295,7 @@ export default {
|
||||
}
|
||||
|
||||
:global(.business-crud-page__mileage-dialog .business-crud-page__mileage-form .el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:global(.business-crud-page__mileage-dialog .business-crud-page__mileage-form .el-input),
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
class="contract-manage-form__attachment-table"
|
||||
@selection-change="selected = $event"
|
||||
>
|
||||
<el-table-column v-if="!readonly" type="selection" width="55" align="center" />
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column v-if="attachmentType" label="附件类型" min-width="160" align="center">
|
||||
<template #default="{ row, $index }">
|
||||
@@ -45,11 +45,17 @@
|
||||
align="left"
|
||||
header-align="left"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="preview?.(row, rows)">{{
|
||||
attachmentName(row)
|
||||
}}</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="preview?.(row, rows)"
|
||||
@keydown.enter="preview?.(row, rows)"
|
||||
@keydown.space.prevent="preview?.(row, rows)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="description" label="附件描述" min-width="220">
|
||||
@@ -173,13 +179,41 @@ import { downloadFileByUrl } from '@/utils/util';
|
||||
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||||
|
||||
const CONTRACT_ATTACHMENT_TYPE_OTHER = '其他文件';
|
||||
/** 用印材料历史取值兼容别名 */
|
||||
export const CONTRACT_SEAL_MATERIAL_LOCATION = '用印材料';
|
||||
export const CONTRACT_SEAL_MATERIAL_TYPE_ALIASES = {
|
||||
合同文件: CONTRACT_SEAL_MATERIAL_LOCATION,
|
||||
};
|
||||
/** 佐证材料附件位置(历史:其它附件) */
|
||||
export const CONTRACT_SUPPORTING_ATTACHMENT_LOCATION = '附件(佐证材料等)';
|
||||
export const CONTRACT_ATTACHMENT_TYPE_DICT = 'contract_attachment_type';
|
||||
export const CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT = 'contract_supporting_attachment_type';
|
||||
export const isContractSupportingAttachmentLocation = location => {
|
||||
const text = String(location || '');
|
||||
return (
|
||||
text === CONTRACT_SUPPORTING_ATTACHMENT_LOCATION ||
|
||||
text === '其它附件' ||
|
||||
text === '其他附件'
|
||||
);
|
||||
};
|
||||
export const resolveAttachmentTypeDictCode = (location, fallback = CONTRACT_ATTACHMENT_TYPE_DICT) =>
|
||||
isContractSupportingAttachmentLocation(location)
|
||||
? CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT
|
||||
: fallback || CONTRACT_ATTACHMENT_TYPE_DICT;
|
||||
export const defaultContractAttachmentTypeOptions = [
|
||||
{ label: '合同文件', value: '合同文件' },
|
||||
{ label: '用印材料', value: '用印材料' },
|
||||
{ label: '双章归档文件', value: '双章归档文件' },
|
||||
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
|
||||
];
|
||||
/** @deprecated 兼容旧引用,实际选项由业务字典 contract_attachment_type 动态加载 */
|
||||
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',
|
||||
@@ -234,8 +268,11 @@ const isOtherAttachmentTypeOption = item => {
|
||||
const attachmentTypeKeywords = fileType => {
|
||||
const text = String(fileType || '');
|
||||
if (text.includes('双章')) return ['双章归档文件', '双章归档', '双章'];
|
||||
if (text.includes('合同') && !text.includes('其他') && !text.includes('其它')) {
|
||||
return ['合同文件', '合同'];
|
||||
if (
|
||||
text.includes('用印') ||
|
||||
(text.includes('合同') && !text.includes('其他') && !text.includes('其它'))
|
||||
) {
|
||||
return ['用印材料', '合同文件', '用印', '合同'];
|
||||
}
|
||||
if (isOtherAttachmentTypeOption({ label: text, value: text })) return [];
|
||||
return text ? [text] : [];
|
||||
@@ -254,8 +291,13 @@ export const resolveContractAttachmentType = (
|
||||
const list = options.length ? options : defaultContractAttachmentTypeOptions;
|
||||
const otherType = resolveOtherAttachmentType(list);
|
||||
const values = list.map(item => item.value);
|
||||
if (values.includes(row.fileType)) return row.fileType;
|
||||
const matchedByLabel = list.find(item => String(item.label) === String(row.fileType));
|
||||
const rawFileType = String(row.fileType || '');
|
||||
const aliasedFileType = CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawFileType] || rawFileType;
|
||||
if (values.includes(aliasedFileType)) return aliasedFileType;
|
||||
if (values.includes(rawFileType)) return rawFileType;
|
||||
const matchedByLabel = list.find(
|
||||
item => String(item.label) === aliasedFileType || String(item.label) === rawFileType
|
||||
);
|
||||
if (matchedByLabel) return matchedByLabel.value;
|
||||
const fileName = normalizeAttachmentTypeText(attachmentName(row));
|
||||
if (!fileName) return otherType;
|
||||
@@ -283,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 },
|
||||
@@ -307,8 +364,13 @@ export default {
|
||||
markApprovedOnUpload: Boolean,
|
||||
useUploadDialog: Boolean,
|
||||
dialogOnly: Boolean,
|
||||
attachmentLocation: { type: String, default: '合同文件' },
|
||||
attachmentLocation: { type: String, default: CONTRACT_SEAL_MATERIAL_LOCATION },
|
||||
attachmentLocationOptions: { type: Array, default: null },
|
||||
/** 附件类型业务字典 code,默认用印材料字典;佐证材料传 contract_supporting_attachment_type */
|
||||
attachmentTypeDictCode: {
|
||||
type: String,
|
||||
default: CONTRACT_ATTACHMENT_TYPE_DICT,
|
||||
},
|
||||
preview: { type: Function, default: null },
|
||||
},
|
||||
emits: ['update:rows', 'upload-to-location', 'upload-confirmed'],
|
||||
@@ -317,8 +379,9 @@ export default {
|
||||
selected: [],
|
||||
attachmentFileTypes,
|
||||
contractAttachmentTypeOptions: [...defaultContractAttachmentTypeOptions],
|
||||
activeAttachmentTypeDictCode: CONTRACT_ATTACHMENT_TYPE_DICT,
|
||||
uploadDialogVisible: false,
|
||||
uploadDialogLocation: '合同文件',
|
||||
uploadDialogLocation: CONTRACT_SEAL_MATERIAL_LOCATION,
|
||||
uploadDialogType: resolveOtherAttachmentType(defaultContractAttachmentTypeOptions),
|
||||
uploadDialogFiles: [],
|
||||
uploadDialogFileList: [],
|
||||
@@ -337,48 +400,91 @@ export default {
|
||||
defaultAttachmentType() {
|
||||
return resolveOtherAttachmentType(this.contractAttachmentTypeOptions);
|
||||
},
|
||||
resolvedAttachmentTypeDictCode() {
|
||||
return this.attachmentTypeDictCode || CONTRACT_ATTACHMENT_TYPE_DICT;
|
||||
},
|
||||
resolvedLocationOptions() {
|
||||
const options = (this.attachmentLocationOptions || []).filter(Boolean);
|
||||
if (options.length) return options;
|
||||
return ['合同文件', '其它附件'];
|
||||
if (options.length) {
|
||||
return options.map(item => {
|
||||
if (item === '合同文件') return CONTRACT_SEAL_MATERIAL_LOCATION;
|
||||
if (item === '其它附件' || item === '其他附件') {
|
||||
return CONTRACT_SUPPORTING_ATTACHMENT_LOCATION;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return [CONTRACT_SEAL_MATERIAL_LOCATION, CONTRACT_SUPPORTING_ATTACHMENT_LOCATION];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
attachmentTypeDictCode: {
|
||||
immediate: false,
|
||||
handler() {
|
||||
this.loadAttachmentTypeOptions(this.resolvedAttachmentTypeDictCode, true);
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadAttachmentTypeOptions();
|
||||
this.loadAttachmentTypeOptions(this.resolvedAttachmentTypeDictCode, true);
|
||||
},
|
||||
methods: {
|
||||
attachmentName,
|
||||
loadAttachmentTypeOptions() {
|
||||
return getBizDictionary({ code: 'contract_attachment_type' })
|
||||
currentUploadUserName() {
|
||||
const raw = this.$store.getters.userInfo;
|
||||
const userInfo = raw && !Array.isArray(raw) ? raw : {};
|
||||
// 与顶部栏一致优先 real_name;用印材料 / 佐证材料共用
|
||||
return (
|
||||
userInfo.real_name ||
|
||||
userInfo.realName ||
|
||||
userInfo.nick_name ||
|
||||
userInfo.nickName ||
|
||||
userInfo.user_name ||
|
||||
userInfo.userName ||
|
||||
userInfo.account ||
|
||||
''
|
||||
);
|
||||
},
|
||||
loadAttachmentTypeOptions(dictCode = this.resolvedAttachmentTypeDictCode, alignRows = false) {
|
||||
const code = dictCode || CONTRACT_ATTACHMENT_TYPE_DICT;
|
||||
this.activeAttachmentTypeDictCode = code;
|
||||
const fallbackOptions = defaultOptionsByDictCode(code);
|
||||
return getBizDictionary({ code })
|
||||
.then(res => {
|
||||
const options = mapDictOptions(res);
|
||||
// 切换字典期间若已发起更新请求,忽略过期响应
|
||||
if (this.activeAttachmentTypeDictCode !== code) return;
|
||||
const options = mapDictOptions(res, code);
|
||||
if (options.length) {
|
||||
// 字典缺少「其他文件」时补上,保证未匹配文件名可默认选中
|
||||
this.contractAttachmentTypeOptions = options.some(isOtherAttachmentTypeOption)
|
||||
? options
|
||||
: [...options, { label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER }];
|
||||
if (
|
||||
!this.contractAttachmentTypeOptions.some(
|
||||
item => String(item.value) === String(this.uploadDialogType)
|
||||
)
|
||||
) {
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
}
|
||||
// 字典加载后,把已有行的中文类型名对齐到 dictKey,保证下拉可选中
|
||||
if (this.attachmentType && (this.rows || []).length) {
|
||||
const aligned = (this.rows || []).map(row => ({
|
||||
...row,
|
||||
fileType: resolveContractAttachmentType(row, this.contractAttachmentTypeOptions),
|
||||
}));
|
||||
const changed = aligned.some(
|
||||
(row, index) => row.fileType !== this.rows[index]?.fileType
|
||||
);
|
||||
if (changed) this.update(aligned);
|
||||
}
|
||||
} else {
|
||||
this.contractAttachmentTypeOptions = fallbackOptions;
|
||||
}
|
||||
if (
|
||||
!this.contractAttachmentTypeOptions.some(
|
||||
item => String(item.value) === String(this.uploadDialogType)
|
||||
)
|
||||
) {
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
}
|
||||
// 字典加载后,把已有行的中文类型名对齐到 dictKey,保证下拉可选中
|
||||
if (alignRows && this.attachmentType && (this.rows || []).length) {
|
||||
const aligned = (this.rows || []).map(row => ({
|
||||
...row,
|
||||
fileType: resolveContractAttachmentType(row, this.contractAttachmentTypeOptions),
|
||||
}));
|
||||
const changed = aligned.some(
|
||||
(row, index) => row.fileType !== this.rows[index]?.fileType
|
||||
);
|
||||
if (changed) this.update(aligned);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 字典加载失败时保留默认选项
|
||||
if (this.activeAttachmentTypeDictCode !== code) return;
|
||||
this.contractAttachmentTypeOptions = fallbackOptions;
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
});
|
||||
},
|
||||
isRowLocked(row) {
|
||||
@@ -388,13 +494,14 @@ export default {
|
||||
this.$emit('update:rows', Array.isArray(rows) ? rows.map(item => ({ ...item })) : []);
|
||||
},
|
||||
enrichUploadedRow(row, existing) {
|
||||
const uploadUserName = this.$store.getters.userInfo?.realName || '';
|
||||
const uploadUserName = this.currentUploadUserName();
|
||||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
const next = {
|
||||
...existing,
|
||||
...row,
|
||||
description: row.description || existing?.description || '',
|
||||
uploadUserName: existing?.uploadUserName || row.uploadUserName || uploadUserName,
|
||||
// 新上传行始终写入当前用户;已有行保留原上传人
|
||||
uploadUserName: existing?.uploadUserName || uploadUserName || row.uploadUserName || '',
|
||||
uploadTime: existing?.uploadTime || row.uploadTime || uploadTime,
|
||||
approved: existing?.approved || row.approved || false,
|
||||
};
|
||||
@@ -434,20 +541,38 @@ export default {
|
||||
resolveTypeByLocation(location) {
|
||||
const options = this.contractAttachmentTypeOptions || [];
|
||||
const locationText = String(location || '');
|
||||
if (locationText.includes('其它') || locationText.includes('其他')) {
|
||||
if (
|
||||
isContractSupportingAttachmentLocation(locationText) ||
|
||||
locationText.includes('佐证') ||
|
||||
locationText.includes('其它') ||
|
||||
locationText.includes('其他')
|
||||
) {
|
||||
return this.defaultAttachmentType;
|
||||
}
|
||||
const matched = options.find(
|
||||
item =>
|
||||
String(item.value) === locationText ||
|
||||
String(item.label) === locationText ||
|
||||
String(item.value).includes('用印') ||
|
||||
String(item.label).includes('用印') ||
|
||||
String(item.value).includes('合同') ||
|
||||
String(item.label).includes('合同')
|
||||
);
|
||||
return matched?.value || options[0]?.value || this.defaultAttachmentType;
|
||||
},
|
||||
syncUploadDialogTypeByLocation() {
|
||||
this.uploadDialogType = this.resolveTypeByLocation(this.uploadDialogLocation);
|
||||
const dictCode = resolveAttachmentTypeDictCode(
|
||||
this.uploadDialogLocation,
|
||||
this.resolvedAttachmentTypeDictCode
|
||||
);
|
||||
const applyType = () => {
|
||||
this.uploadDialogType = this.resolveTypeByLocation(this.uploadDialogLocation);
|
||||
};
|
||||
if (dictCode !== this.activeAttachmentTypeDictCode) {
|
||||
this.loadAttachmentTypeOptions(dictCode, false).finally(applyType);
|
||||
return;
|
||||
}
|
||||
applyType();
|
||||
},
|
||||
openUploadDialog() {
|
||||
const options = this.resolvedLocationOptions;
|
||||
@@ -544,13 +669,13 @@ export default {
|
||||
this.$message.warning('请选择附件类型');
|
||||
return;
|
||||
}
|
||||
const uploadUserName = this.$store.getters.userInfo?.realName || '';
|
||||
const uploadUserName = this.currentUploadUserName();
|
||||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
const appended = this.uploadDialogFiles.map(row => ({
|
||||
...row,
|
||||
fileType: this.uploadDialogType,
|
||||
description: row.description || '',
|
||||
uploadUserName: row.uploadUserName || uploadUserName,
|
||||
uploadUserName: uploadUserName || row.uploadUserName || '',
|
||||
uploadTime: row.uploadTime || uploadTime,
|
||||
...(this.markApprovedOnUpload ? { approved: true } : {}),
|
||||
}));
|
||||
@@ -607,7 +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);
|
||||
|
||||
@@ -44,19 +44,22 @@
|
||||
<dt>附件</dt>
|
||||
<dd>
|
||||
<template v-if="attachments.length">
|
||||
<el-link
|
||||
<span
|
||||
v-for="file in attachments"
|
||||
:key="file.url || file.link || file.name || file.originalName"
|
||||
type="primary"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(file)"
|
||||
>{{ attachmentName(file) }}</el-link
|
||||
>
|
||||
@keydown.enter="previewAttachment(file)"
|
||||
@keydown.space.prevent="previewAttachment(file)"
|
||||
>{{ attachmentName(file) }}</span>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="route-map"><template v-for="(route, index) in segments" :key="route.segmentNo"><div class="route-map__node"><span :class="['route-badge', index ? 'middle' : 'start']">{{ index ? '经' : '起' }}</span><strong>{{ segmentLocationName(route, 'departure') }}</strong><small>{{ route.departureAddress || '-' }}</small><small class="route-map__progress">{{ index ? `已到达 ${quantity(segments[index - 1].arrivedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(segments[index - 1])}` : `已调度 ${quantity(route.dispatchedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(route)}` }}</small><small v-if="index" class="route-map__progress">已调度 {{ quantity(route.dispatchedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div><div class="route-map__line"></div><div v-if="index === segments.length - 1" class="route-map__node"><span class="route-badge end">终</span><strong>{{ segmentLocationName(route, 'arrival') }}</strong><small>{{ route.arrivalAddress || '-' }}</small><small class="route-map__progress">已到达 {{ quantity(route.arrivedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div></template></div>
|
||||
<div class="route-map"><template v-for="(route, index) in segments" :key="route.segmentNo"><div class="route-map__node"><span :class="['route-badge', index ? 'middle' : 'start']">{{ index ? '经' : '起' }}</span><strong>{{ segmentLocationName(route, 'departure') }}</strong><small>{{ formatRegionAddress(route.departureAddress) || '-' }}</small><small class="route-map__progress">{{ index ? `已到达 ${quantity(segments[index - 1].arrivedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(segments[index - 1])}` : `已调度 ${quantity(route.dispatchedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(route)}` }}</small><small v-if="index" class="route-map__progress">已调度 {{ quantity(route.dispatchedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div><div class="route-map__line"></div><div v-if="index === segments.length - 1" class="route-map__node"><span class="route-badge end">终</span><strong>{{ segmentLocationName(route, 'arrival') }}</strong><small>{{ formatRegionAddress(route.arrivalAddress) || '-' }}</small><small class="route-map__progress">已到达 {{ quantity(route.arrivedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div></template></div>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="documentPreviewVisible" :title="previewFile.name || '附件预览'" append-to-body destroy-on-close width="90%" top="4vh">
|
||||
@@ -107,6 +110,7 @@ import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archiv
|
||||
import { CaretBottom, CaretTop } from '@element-plus/icons-vue';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
import { formatRegionAddress } from '@/utils/region-address';
|
||||
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
|
||||
import '@open-file-viewer/core/style.css';
|
||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||
@@ -161,6 +165,7 @@ export default {
|
||||
} finally { this.loading = false; }
|
||||
},
|
||||
methods: {
|
||||
formatRegionAddress,
|
||||
quantity(value) { const number = Number(value || 0); return Number.isInteger(number) ? number : number.toFixed(3).replace(/\.?0+$/, ''); },
|
||||
segmentNumber(route, index) { return String(route.segmentNo || '').replace(/^段/, '') || index + 1; },
|
||||
isSegmentExpanded(segmentNo) { return Boolean(this.expandedSegments[segmentNo]); },
|
||||
@@ -330,7 +335,7 @@ export default {
|
||||
.transport-flow-tag {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #303133; font-size: 14px; word-break: break-all; } :deep(.el-link) { font-size: 14px; } }
|
||||
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #303133; font-size: 14px; word-break: break-all; } .attachment-file-name { font-size: 14px; max-width: 100%; } }
|
||||
.route-map { display: flex; align-items: flex-start; padding: 16px 24px 20px; border-top: 1px solid #eff1f7; overflow-x: auto; }
|
||||
.route-map__node { display: grid; flex: 0 0 150px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } }
|
||||
.route-map__progress { color: #303133 !important; }
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
<div class="route-overview__node">
|
||||
<span :class="['route-badge', index === 0 ? 'start' : 'middle']">{{ index === 0 ? '起' : '经' }}</span>
|
||||
<el-tooltip :content="route.departureName || '-'" placement="top" :show-after="120">
|
||||
<strong>{{ route.departureName || '-' }}</strong>
|
||||
<strong>{{ dispatchLocationName(route, 'departure') }}</strong>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="route.departureAddress || '-'" placement="top" :show-after="120">
|
||||
<small>{{ route.departureAddress || '-' }}</small>
|
||||
<small>{{ formatRegionAddress(route.departureAddress) || '-' }}</small>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="contactText(route.departureContact, route.departurePhone)" placement="top" :show-after="120">
|
||||
<small>{{ contactText(route.departureContact, route.departurePhone) }}</small>
|
||||
@@ -34,10 +34,10 @@
|
||||
<div v-if="index === routes.length - 1" class="route-overview__node">
|
||||
<span class="route-badge end">终</span>
|
||||
<el-tooltip :content="route.arrivalName || '-'" placement="top" :show-after="120">
|
||||
<strong>{{ route.arrivalName || '-' }}</strong>
|
||||
<strong>{{ dispatchLocationName(route, 'arrival') }}</strong>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="route.arrivalAddress || '-'" placement="top" :show-after="120">
|
||||
<small>{{ route.arrivalAddress || '-' }}</small>
|
||||
<small>{{ formatRegionAddress(route.arrivalAddress) || '-' }}</small>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="contactText(route.arrivalContact, route.arrivalPhone)" placement="top" :show-after="120">
|
||||
<small>{{ contactText(route.arrivalContact, route.arrivalPhone) }}</small>
|
||||
@@ -266,7 +266,7 @@
|
||||
<el-col :span="6">
|
||||
<el-form-item label="单价">
|
||||
<el-input :model-value="freightGroupUnitPrice(group)" inputmode="decimal" placeholder="请输入" @input="value => handleFreightGroupUnitPriceInput(group, value)" @clear="() => handleFreightGroupUnitPriceInput(group, '')">
|
||||
<template #append><el-select :model-value="freightGroupPriceUnit(group)" class="freight-unit-select" @change="value => handleFreightGroupPriceUnitChange(group, value)"><el-option label="元/吨" value="元/吨" /><el-option label="元/件" value="元/件" /><el-option label="元/方" value="元/方" /></el-select></template>
|
||||
<template #append><el-select :model-value="freightGroupPriceUnit(group)" class="freight-unit-select" @change="value => handleFreightGroupPriceUnitChange(group, value)"><el-option v-for="item in priceUnitOptions(route)" :key="item.value" :label="item.label" :value="item.value" /></el-select></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -317,6 +317,14 @@
|
||||
<script>
|
||||
import * as api from '@/api/business/master-order';
|
||||
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import {
|
||||
currencyUnit,
|
||||
remapPriceUnitCurrency,
|
||||
resolvePriceUnitByQuantityUnit,
|
||||
} from '@/utils/currency';
|
||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||
import { formatRegionAddress } from '@/utils/region-address';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
|
||||
const unwrapRecords = res => {
|
||||
@@ -347,7 +355,8 @@ export default {
|
||||
carrierLoading: false,
|
||||
driverLoading: false,
|
||||
escortLoading: false,
|
||||
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
|
||||
quantityUnitOptions: [],
|
||||
quantityUnitLoading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -389,10 +398,24 @@ export default {
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadQuantityUnitOptions();
|
||||
await Promise.all([this.load(), this.loadCarrierOptions()]);
|
||||
this.applySelfOperatedCarrierDefaults();
|
||||
},
|
||||
methods: {
|
||||
loadQuantityUnitOptions() {
|
||||
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
|
||||
this.quantityUnitLoading = true;
|
||||
this.quantityUnitLoadPromise = loadMeasurementUnitOptions()
|
||||
.then(options => {
|
||||
this.quantityUnitOptions = options.map(item => item.value);
|
||||
return this.quantityUnitOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.quantityUnitLoading = false;
|
||||
});
|
||||
return this.quantityUnitLoadPromise;
|
||||
},
|
||||
async load() {
|
||||
if (!this.id) return;
|
||||
this.loading = true;
|
||||
@@ -412,8 +435,10 @@ export default {
|
||||
},
|
||||
async loadContractCurrency() {
|
||||
const directCurrency = this.master?.settlementCurrency || this.master?.settlementCurrencyCode || this.master?.currency;
|
||||
const previousCurrency = this.master?.settlementCurrency;
|
||||
if (!this.master?.projectId || !this.master?.contractId) {
|
||||
this.master.settlementCurrency = this.normalizeCurrencyValue(directCurrency || 'RMB');
|
||||
this.syncRouteCurrency(previousCurrency, this.master.settlementCurrency);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -427,6 +452,20 @@ export default {
|
||||
this.master.customerContractPartyB = '';
|
||||
this.master.settlementCurrency = 'RMB';
|
||||
}
|
||||
this.syncRouteCurrency(previousCurrency, this.master.settlementCurrency);
|
||||
},
|
||||
syncRouteCurrency(_previousCurrency, nextCurrency) {
|
||||
const currency = this.normalizeCurrencyValue(nextCurrency || 'RMB');
|
||||
(this.routes || []).forEach(route => {
|
||||
route.currency = currency;
|
||||
(route.freightItems || []).forEach(item => {
|
||||
if (item.priceUnit) {
|
||||
item.priceUnit = remapPriceUnitCurrency(item.priceUnit, currency);
|
||||
} else if (item.quantityUnit) {
|
||||
item.priceUnit = resolvePriceUnitByQuantityUnit(item.quantityUnit, currency);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
dispatchRouteNodes() {
|
||||
const routes = Array.isArray(this.master?.routes) ? this.master.routes : [];
|
||||
@@ -601,8 +640,23 @@ export default {
|
||||
const prices = (group.rows || []).map(item => String(item.unitPrice ?? '').trim()).filter(Boolean);
|
||||
return !prices.length || prices.some(price => price !== prices[0]) ? '' : prices[0];
|
||||
},
|
||||
freightGroupPriceUnit(group = {}) { return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : ''); },
|
||||
freightGroupPriceUnit(group = {}) {
|
||||
if (group.rows?.[0]?.priceUnit) return group.rows[0].priceUnit;
|
||||
if (!group.quantityUnit) return '';
|
||||
return (
|
||||
resolvePriceUnitByQuantityUnit(group.quantityUnit, this.master?.settlementCurrency || 'RMB') ||
|
||||
`${currencyUnit(this.master?.settlementCurrency || 'RMB')}/${group.quantityUnit}`
|
||||
);
|
||||
},
|
||||
freightGroupAmount(group = {}) { return (group.rows || []).reduce((sum, item) => sum + this.freightAmount(item), 0); },
|
||||
priceUnitOptions(route = {}) {
|
||||
const unit = currencyUnit(route.currency || this.master?.settlementCurrency || 'RMB');
|
||||
return [
|
||||
{ label: `${unit}/吨`, value: `${unit}/吨` },
|
||||
{ label: `${unit}/件`, value: `${unit}/件` },
|
||||
{ label: `${unit}/方`, value: `${unit}/方` },
|
||||
];
|
||||
},
|
||||
handleFreightGroupUnitPriceInput(group, value) {
|
||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||
const normalized = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||
@@ -612,6 +666,10 @@ export default {
|
||||
handleFreightGroupQuantityUnitChange(route, group, value) {
|
||||
(group.rows || []).forEach(item => {
|
||||
item.quantityUnit = value || '';
|
||||
item.priceUnit = resolvePriceUnitByQuantityUnit(
|
||||
value,
|
||||
route.currency || this.master?.settlementCurrency || 'RMB'
|
||||
) || item.priceUnit;
|
||||
const goods = (route.goods || []).find(row => String(row.sourceIndex) === String(item.sourceIndex));
|
||||
if (goods) goods.quantityUnit = value || '';
|
||||
});
|
||||
@@ -634,7 +692,7 @@ export default {
|
||||
})),
|
||||
});
|
||||
},
|
||||
currencyLabel(value) { return ({ CNY: '元', RMB: '元', USD: '美元', EUR: '欧元' })[value] || value || '元'; },
|
||||
currencyLabel(value) { return currencyUnit(value); },
|
||||
quantityNumber(value) { return Number(value || 0); },
|
||||
goodsKey(goods = {}, includeUnit = true) {
|
||||
return [
|
||||
@@ -794,7 +852,12 @@ export default {
|
||||
const mileage = Number(value);
|
||||
return Number.isFinite(mileage) && mileage >= 0 ? String(Math.trunc(mileage)) : '';
|
||||
},
|
||||
addressText(name, address) { return [name, address].filter(Boolean).join(' / ') || '-'; },
|
||||
formatRegionAddress,
|
||||
addressText(name, address) {
|
||||
// 名称与详细地址都要按「市 区县」展示;两者格式化后相同时只展示一次
|
||||
const values = [this.formatRegionAddress(name), this.formatRegionAddress(address)].filter(Boolean);
|
||||
return [...new Set(values)].join(' / ') || '-';
|
||||
},
|
||||
contactText(name, phone) { return [name, phone].filter(Boolean).join(' - ') || '-'; },
|
||||
dateStartLabel(route) { return route.documentType === '计划单' ? '计划开始日期' : '预计发货日期'; },
|
||||
dateEndLabel(route) { return route.documentType === '计划单' ? '计划结束日期' : '预计完成日期'; },
|
||||
@@ -824,6 +887,7 @@ export default {
|
||||
removeGoods(route, index) { route.goods.splice(index, 1); this.syncFreightItems(route); },
|
||||
syncFreightItems(route) {
|
||||
const oldItems = route.freightItems || [];
|
||||
const currency = route.currency || this.master?.settlementCurrency || 'RMB';
|
||||
route.freightItems = (route.goods || [])
|
||||
.filter(item => item.sourceIndex !== undefined && item.sourceIndex !== '')
|
||||
.map(item => {
|
||||
@@ -835,7 +899,10 @@ export default {
|
||||
quantity: item.dispatchQuantity || '',
|
||||
quantityUnit: item.quantityUnit || '',
|
||||
unitPrice: old.unitPrice || '',
|
||||
priceUnit: old.priceUnit || `元/${item.quantityUnit || '吨'}`,
|
||||
priceUnit:
|
||||
old.priceUnit ||
|
||||
resolvePriceUnitByQuantityUnit(item.quantityUnit || '吨', currency) ||
|
||||
`${currencyUnit(currency)}/${item.quantityUnit || '吨'}`,
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -888,9 +955,11 @@ export default {
|
||||
},
|
||||
carrierOptionLabel(item) {
|
||||
const duplicateCount = this.carrierOptions.filter(option => option.carrierName === item.carrierName).length;
|
||||
return duplicateCount > 1
|
||||
? `${item.carrierName}(${item.contractName || item.contractId})`
|
||||
: item.carrierName;
|
||||
if (duplicateCount <= 1) {
|
||||
return item.carrierName;
|
||||
}
|
||||
const contractLabel = formatContractOptionLabel(item);
|
||||
return contractLabel ? `${item.carrierName}(${contractLabel})` : item.carrierName;
|
||||
},
|
||||
driverOptionLabel(item) { return item.driverName || item.name || ''; },
|
||||
driverOptionKey(item) { return item.id || item.driverId || this.driverOptionLabel(item) || item.mobile; },
|
||||
@@ -1197,7 +1266,7 @@ export default {
|
||||
.route-overview__line { flex: 0 1 56px; min-width: 24px; height: 32px; margin-top: 0; border-bottom: 6px solid #e4e7ed; }
|
||||
.segment-header { min-height: 46px; padding: 0 20px; border-bottom: 1px solid #eff1f7; font-weight: 600; em { color: #409eff; font-style: normal; } }
|
||||
.segment-content { padding: 16px 24px; }
|
||||
.dispatch-form { :deep(.el-form-item) { margin-bottom: 14px; } :deep(.el-input), :deep(.el-select), :deep(.el-date-editor), :deep(.el-input-number) { width: 100%; } :deep(.el-input-group__append .el-select) { width: auto; } }
|
||||
.dispatch-form { :deep(.el-form-item) { margin-bottom: 24px; } :deep(.el-input), :deep(.el-select), :deep(.el-date-editor), :deep(.el-input-number) { width: 100%; } :deep(.el-input-group__append .el-select) { width: auto; } }
|
||||
/* 短标签按自身宽度,避免被「预计发货日期」等长标签把左侧撑开 */
|
||||
.dispatch-form__compact-label {
|
||||
:deep(.el-form-item__label) {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
><el-option
|
||||
v-for="contract in contracts"
|
||||
:key="contract.id"
|
||||
:label="contract.contractName"
|
||||
:label="formatContractOptionLabel(contract)"
|
||||
:value="contract.id" /></el-select></el-form-item></el-col
|
||||
><el-col :span="6"
|
||||
><el-form-item label="总单号" required
|
||||
@@ -261,11 +261,22 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="240" align="center" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="240"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220" align="center">
|
||||
@@ -589,6 +600,7 @@ import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||
import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
|
||||
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
||||
import { exportBlob, importBlob } from '@/api/common';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { OfficeBuilding, Search } from '@element-plus/icons-vue';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
@@ -597,10 +609,10 @@ import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from
|
||||
import '@open-file-viewer/core/style.css';
|
||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||
|
||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||
const quantityUnitOptions = ['吨', '件', '方'];
|
||||
const attachmentViewerPlugins = [
|
||||
imagePlugin(),
|
||||
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
|
||||
@@ -703,10 +715,11 @@ export default {
|
||||
'zip',
|
||||
],
|
||||
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
|
||||
quantityUnitOptions,
|
||||
quantityUnitOptions: [],
|
||||
quantityUnitLoading: false,
|
||||
form: {
|
||||
routes: [{ segmentNo: '段1', transportType: '' }],
|
||||
goods: [{ quantityUnit: quantityUnitOptions[0] }],
|
||||
goods: [{ quantityUnit: '吨' }],
|
||||
attachmentsJson: '[]',
|
||||
transportOrganizationType: '多式联运',
|
||||
},
|
||||
@@ -773,6 +786,7 @@ export default {
|
||||
async mounted() {
|
||||
this.loading = true;
|
||||
try {
|
||||
await this.loadQuantityUnitOptions();
|
||||
await Promise.all([this.loadProjects(), this.loadCargoTypeOptions(), this.loadRegionOptions()]);
|
||||
if (this.id) {
|
||||
await this.loadDetail(this.id);
|
||||
@@ -782,6 +796,23 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
defaultQuantityUnit() {
|
||||
return this.quantityUnitOptions[0] || '吨';
|
||||
},
|
||||
loadQuantityUnitOptions() {
|
||||
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
|
||||
this.quantityUnitLoading = true;
|
||||
this.quantityUnitLoadPromise = loadMeasurementUnitOptions()
|
||||
.then(options => {
|
||||
this.quantityUnitOptions = options.map(item => item.value);
|
||||
return this.quantityUnitOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.quantityUnitLoading = false;
|
||||
});
|
||||
return this.quantityUnitLoadPromise;
|
||||
},
|
||||
responseData(response) {
|
||||
return response?.data?.data || response?.data || response || {};
|
||||
},
|
||||
@@ -958,7 +989,7 @@ export default {
|
||||
finalTransportType: hasFinalSegment
|
||||
? finalSegment.transportType
|
||||
: detail.finalTransportType || '',
|
||||
goods: detail.goods || [{ quantityUnit: quantityUnitOptions[0] }],
|
||||
goods: detail.goods || [{ quantityUnit: this.defaultQuantityUnit() }],
|
||||
attachmentsJson: detail.attachmentsJson || '[]',
|
||||
transportOrganizationType: '多式联运',
|
||||
};
|
||||
@@ -1659,7 +1690,7 @@ export default {
|
||||
'',
|
||||
cargoTypePath,
|
||||
quantity: row.quantity || '',
|
||||
quantityUnit: row.quantityUnit || quantityUnitOptions[0],
|
||||
quantityUnit: row.quantityUnit || this.defaultQuantityUnit(),
|
||||
packageType: row.packageType || row.package || '',
|
||||
brand: row.brand || '',
|
||||
specification: row.specification || row.spec || '',
|
||||
@@ -1785,7 +1816,7 @@ export default {
|
||||
},
|
||||
addGoods(index) {
|
||||
this.form.goods.splice(index + 1, 0, {
|
||||
quantityUnit: quantityUnitOptions[0],
|
||||
quantityUnit: this.defaultQuantityUnit(),
|
||||
materialCode: '',
|
||||
deviceCode: '',
|
||||
remark: '',
|
||||
@@ -2047,7 +2078,7 @@ export default {
|
||||
font-size: 16px;
|
||||
}
|
||||
.el-form-item {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
.master-editor__quantity-field {
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<div v-if="groups.length" class="punch-record-photos">
|
||||
<section v-for="group in groups" :key="group.type" class="punch-record-photos__group">
|
||||
<div class="punch-record-photos__head">
|
||||
<span class="punch-record-photos__title">{{ group.type }}</span>
|
||||
<span v-if="editable" class="punch-record-photos__count">{{ countText(group) }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="punch-record-photos__grid"
|
||||
:class="{ 'punch-record-photos__grid--edit': editable }"
|
||||
>
|
||||
<div
|
||||
v-for="(item, groupIndex) in group.items"
|
||||
:key="`${item.photo.url}-${item.index}`"
|
||||
class="punch-record-photos__item"
|
||||
@click="handlePreview(item.index)"
|
||||
>
|
||||
<el-image
|
||||
:src="item.photo.url"
|
||||
:preview-src-list="nativePreview ? previewUrls : []"
|
||||
:initial-index="nativePreview ? item.index : 0"
|
||||
:preview-teleported="nativePreview"
|
||||
:lazy="lazy"
|
||||
fit="cover"
|
||||
/>
|
||||
<!-- 主图 = 该类型的第一张,按上传顺序取;没有独立字段,也不可切换 -->
|
||||
<div v-if="groupIndex === 0" class="punch-record-photos__badge">主图</div>
|
||||
<el-link
|
||||
v-if="editable"
|
||||
class="punch-record-photos__remove"
|
||||
type="danger"
|
||||
:underline="false"
|
||||
@click.stop="$emit('remove', item.index)"
|
||||
>删除</el-link
|
||||
>
|
||||
</div>
|
||||
<el-upload
|
||||
v-if="editable && !group.full"
|
||||
class="punch-record-photos__upload"
|
||||
:action="uploadAction"
|
||||
name="file"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:before-upload="file => handleBeforeUpload(file, group.type)"
|
||||
:on-success="res => $emit('upload-success', res, group.type)"
|
||||
:on-error="error => $emit('upload-error', error, group.type)"
|
||||
>
|
||||
<div class="punch-record-photos__add">+ 添加</div>
|
||||
</el-upload>
|
||||
<div v-else-if="editable" class="punch-record-photos__add punch-record-photos__add--full">
|
||||
已满 {{ maxPerType }} 张
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
// 打卡照片是每个打卡节点必有的一个照片类型,不由过程配置的凭证类型决定;
|
||||
// 它与各凭证类型各自最多 9 张,同一类型下第一张即界面上的「主图」。
|
||||
const PUNCH_PHOTO_TYPE = '打卡照片';
|
||||
// 照片没带类型时的兜底名称,与后端落库时的兜底口径一致
|
||||
const PHOTO_TYPE_FALLBACK = '凭证';
|
||||
const MAX_PHOTOS_PER_TYPE = 9;
|
||||
|
||||
const props = defineProps({
|
||||
// 扁平照片数组:管理端回显用 voucherType,司机端回显与旧数据用 type
|
||||
photos: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 恒定展示的照片类型:「打卡照片」+ 该节点过程配置勾选的凭证类型。
|
||||
// 留空则只按照片自身带的类型分组(只读回显用)。
|
||||
groupTypes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 编辑态:每个类型自带「添加」格与删除链接,并显示已选/上限
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 每个类型的张数上限;0 表示不限制(在途打卡只有一个照片字段,张数由「只保留第一张」约束)
|
||||
maxPerType: {
|
||||
type: Number,
|
||||
default: MAX_PHOTOS_PER_TYPE,
|
||||
},
|
||||
// 编辑态用 el-image 自带预览;只读态不发预览事件,把点击交给调用方开自己的看图器
|
||||
nativePreview: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
uploadAction: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
uploadHeaders: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 返回 false 即中止本次上传;类型、体积、张数校验都在调用方
|
||||
beforeUpload: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['preview', 'remove', 'upload-success', 'upload-error']);
|
||||
|
||||
const photoType = photo =>
|
||||
String(photo?.voucherType || photo?.type || '').trim() || PHOTO_TYPE_FALLBACK;
|
||||
|
||||
const groups = computed(() => {
|
||||
const photos = Array.isArray(props.photos) ? props.photos : [];
|
||||
const names = [];
|
||||
const buckets = new Map();
|
||||
const ensure = name => {
|
||||
if (!buckets.has(name)) {
|
||||
buckets.set(name, []);
|
||||
names.push(name);
|
||||
}
|
||||
return buckets.get(name);
|
||||
};
|
||||
// 恒定展示的类型先占位:一张照片都没有的类型也要出现,否则编辑态下没有添加入口
|
||||
props.groupTypes.forEach(type => {
|
||||
const name = String(type || '').trim();
|
||||
if (name) ensure(name);
|
||||
});
|
||||
// 按上传顺序入组,组间顺序即类型首次出现的顺序,不重排照片
|
||||
photos.forEach((photo, index) => {
|
||||
if (!photo?.url) return;
|
||||
ensure(photoType(photo)).push({ photo, index });
|
||||
});
|
||||
const list = names.map(type => {
|
||||
const items = buckets.get(type);
|
||||
return {
|
||||
type,
|
||||
items,
|
||||
full: props.editable && props.maxPerType > 0 && items.length >= props.maxPerType,
|
||||
};
|
||||
});
|
||||
// 编辑态至少要有一个分组,否则记录里还没有照片时就没有添加入口
|
||||
if (props.editable && !list.length) {
|
||||
return [{ type: props.groupTypes[0] || PUNCH_PHOTO_TYPE, items: [], full: false }];
|
||||
}
|
||||
return list;
|
||||
});
|
||||
|
||||
// 预览列表按原始下标对齐,删除/分组都不改变点击某张时打开的是哪一张
|
||||
const previewUrls = computed(() =>
|
||||
(Array.isArray(props.photos) ? props.photos : []).map(photo => photo?.url || '')
|
||||
);
|
||||
|
||||
const handlePreview = index => {
|
||||
if (props.nativePreview) return;
|
||||
emit('preview', index);
|
||||
};
|
||||
|
||||
const handleBeforeUpload = (file, type) => {
|
||||
if (typeof props.beforeUpload !== 'function') return true;
|
||||
return props.beforeUpload(file, type);
|
||||
};
|
||||
|
||||
const countText = group =>
|
||||
props.maxPerType > 0 ? `${group.items.length}/${props.maxPerType}` : String(group.items.length);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.punch-record-photos {
|
||||
// 弹窗里本组件挂在 el-form-item__content 下,那是个 flex 容器:
|
||||
// 不给宽度就会收缩成内容宽,里面的网格跟着塌成一小块
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 4px;
|
||||
|
||||
&__group {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
// 只读回显:撑满容器宽度的五列网格
|
||||
&__grid {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
// 编辑态沿用弹窗原本的固定 96px 方图 + 自动换行:尺寸不随容器宽窄变化
|
||||
&__grid--edit {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
.el-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border: 1px solid #eff1f7;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
}
|
||||
|
||||
&__grid--edit &__item {
|
||||
width: 96px;
|
||||
|
||||
.el-image {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
aspect-ratio: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0 6px;
|
||||
border-radius: 4px 0 4px 0;
|
||||
background: rgba(64, 158, 255, 0.9);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&__remove {
|
||||
align-self: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__grid--edit &__upload {
|
||||
width: 96px;
|
||||
}
|
||||
|
||||
&__add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__grid--edit &__add {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
aspect-ratio: auto;
|
||||
}
|
||||
|
||||
&__add--full {
|
||||
color: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -113,7 +113,7 @@
|
||||
<el-option
|
||||
v-for="item in contractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -153,11 +153,24 @@
|
||||
@click="handleTransportSecondaryAddressInputClick('departure')"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon @click.stop="openTransportMapDialog('departure')">
|
||||
<el-icon
|
||||
class="shipping-template-page__map-suffix"
|
||||
@click.stop="openTransportMapDialog('departure')"
|
||||
>
|
||||
<Location />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-tooltip content="选择常用地址" placement="top">
|
||||
<el-button
|
||||
class="shipping-template-page__address-pick-btn"
|
||||
type="primary"
|
||||
:icon="OfficeBuilding"
|
||||
link
|
||||
:disabled="transportAddressSelectDisabled"
|
||||
@click="openTransportAddressPicker('departure')"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<span>联系人</span>
|
||||
<el-input
|
||||
v-model="form.departureContact"
|
||||
@@ -166,16 +179,6 @@
|
||||
/>
|
||||
<span>联系方式</span>
|
||||
<el-input v-model="form.departurePhone" placeholder="请输入" :disabled="dialogReadonly" />
|
||||
<el-tooltip content="选择常用地址" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:disabled="transportAddressSelectDisabled"
|
||||
@click="openTransportAddressPicker('departure')"
|
||||
>
|
||||
选择
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -197,25 +200,28 @@
|
||||
@click="handleTransportSecondaryAddressInputClick('arrival')"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon @click.stop="openTransportMapDialog('arrival')">
|
||||
<el-icon
|
||||
class="shipping-template-page__map-suffix"
|
||||
@click.stop="openTransportMapDialog('arrival')"
|
||||
>
|
||||
<Location />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-tooltip content="选择常用地址" placement="top">
|
||||
<el-button
|
||||
class="shipping-template-page__address-pick-btn"
|
||||
type="primary"
|
||||
:icon="OfficeBuilding"
|
||||
link
|
||||
:disabled="transportAddressSelectDisabled"
|
||||
@click="openTransportAddressPicker('arrival')"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<span>联系人</span>
|
||||
<el-input v-model="form.arrivalContact" placeholder="请输入" :disabled="dialogReadonly" />
|
||||
<span>联系方式</span>
|
||||
<el-input v-model="form.arrivalPhone" placeholder="请输入" :disabled="dialogReadonly" />
|
||||
<el-tooltip content="选择常用地址" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:disabled="transportAddressSelectDisabled"
|
||||
@click="openTransportAddressPicker('arrival')"
|
||||
>
|
||||
选择
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -454,11 +460,20 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column type="index" label="序号" width="70" />
|
||||
<el-table-column label="文件名" min-width="220"
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
><template #default="{ row }"
|
||||
><el-link type="primary" @click="previewAttachment(row)">{{
|
||||
attachmentName(row)
|
||||
}}</el-link></template
|
||||
><span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column label="附件描述" min-width="220"
|
||||
@@ -721,11 +736,21 @@
|
||||
</div>
|
||||
<el-table :data="detailAttachmentRows" border empty-text="暂无附件">
|
||||
<el-table-column type="index" label="序号" width="70" />
|
||||
<el-table-column label="文件名" min-width="220">
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">{{
|
||||
attachmentName(row)
|
||||
}}</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220">
|
||||
@@ -1098,10 +1123,12 @@ import {
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { packageOptions } from '@/option/business/common';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
import { Location } from '@element-plus/icons-vue';
|
||||
import { Location, OfficeBuilding } from '@element-plus/icons-vue';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
import {
|
||||
@@ -1252,6 +1279,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
Location,
|
||||
OfficeBuilding,
|
||||
form: {},
|
||||
query: {},
|
||||
loading: true,
|
||||
@@ -1278,7 +1306,8 @@ export default {
|
||||
cargoTypeRequest: null,
|
||||
transportCargoRows: [],
|
||||
packageOptions,
|
||||
quantityUnitOptions: ['吨', '千克', '立方米', '立方', '公里', '单', '件', '车', '箱', '托盘'],
|
||||
quantityUnitOptions: [],
|
||||
quantityUnitLoading: false,
|
||||
feeUnitOptions: [],
|
||||
currencyOptions: [],
|
||||
currencyLoading: false,
|
||||
@@ -1525,11 +1554,26 @@ export default {
|
||||
created() {
|
||||
this.loadProjectOptions();
|
||||
this.ensureCargoTypeOptions();
|
||||
this.loadQuantityUnitOptions();
|
||||
this.loadFeeUnitOptions();
|
||||
this.loadCurrencyOptions();
|
||||
if (this.isStandaloneFormPage) this.$nextTick(() => this.initStandaloneFormPage());
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
loadQuantityUnitOptions() {
|
||||
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
|
||||
this.quantityUnitLoading = true;
|
||||
this.quantityUnitLoadPromise = loadMeasurementUnitOptions()
|
||||
.then(options => {
|
||||
this.quantityUnitOptions = options.map(item => item.value);
|
||||
return this.quantityUnitOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.quantityUnitLoading = false;
|
||||
});
|
||||
return this.quantityUnitLoadPromise;
|
||||
},
|
||||
buildOption(option) {
|
||||
const next = { ...option, column: (option.column || []).map(column => ({ ...column })) };
|
||||
const tableOrder = this.config.tableColumnOrder || [];
|
||||
@@ -3384,10 +3428,25 @@ export default {
|
||||
.shipping-template-page__route-row {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(160px, 1fr) minmax(220px, 2fr) auto minmax(140px, 1fr) auto minmax(160px, 1fr)
|
||||
auto;
|
||||
minmax(160px, 1fr) minmax(220px, 2fr) 40px auto minmax(140px, 1fr) auto
|
||||
minmax(160px, 1fr);
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.shipping-template-page__address-pick-btn {
|
||||
:deep(.el-icon) {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.shipping-template-page__map-suffix {
|
||||
cursor: pointer;
|
||||
color: #909399;
|
||||
font-size: 18px;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
.shipping-template-page__address-map-input {
|
||||
cursor: pointer;
|
||||
@@ -3506,7 +3565,7 @@ export default {
|
||||
}
|
||||
.shipping-template-page__detail-form :deep(.el-form-item) {
|
||||
min-width: 0;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.shipping-template-page__detail-form :deep(.el-form-item__label) {
|
||||
color: #909399;
|
||||
@@ -3695,7 +3754,7 @@ export default {
|
||||
}
|
||||
|
||||
:global(.shipping-template-form-page-dialog .avue-group .el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:global(.shipping-template-form-page-dialog .avue-form__menu) {
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
<el-option
|
||||
v-for="item in contractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -492,11 +492,22 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="200" align="left" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="200"
|
||||
align="left"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ row.originalName || row.name }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ row.originalName || row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220" align="center">
|
||||
@@ -673,14 +684,16 @@
|
||||
<div class="transport-plan-page__transport-plan-detail-field is-attachments">
|
||||
<span>附件</span>
|
||||
<div class="transport-plan-page__transport-plan-detail-attachments">
|
||||
<el-link
|
||||
<span
|
||||
v-for="file in attachmentRows"
|
||||
:key="file.url || file.name || file.originalName"
|
||||
type="primary"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(file)"
|
||||
>
|
||||
{{ attachmentName(file) }}
|
||||
</el-link>
|
||||
@keydown.enter="previewAttachment(file)"
|
||||
@keydown.space.prevent="previewAttachment(file)"
|
||||
>{{ attachmentName(file) }}</span>
|
||||
<span v-if="!attachmentRows.length">-</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -855,11 +868,21 @@
|
||||
</div>
|
||||
<el-table :data="attachmentRows" empty-text="暂无附件">
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="260">
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="260"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -980,7 +1003,7 @@
|
||||
<el-option
|
||||
v-for="item in transportPlanImportContractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -1744,11 +1767,11 @@
|
||||
><el-select
|
||||
:model-value="dispatchFreightGroupPriceUnit(group)"
|
||||
class="transport-plan-page__dispatch-unit-select"
|
||||
placeholder="元/吨"
|
||||
:placeholder="defaultPriceUnit"
|
||||
@change="value => handleDispatchFreightGroupPriceUnitChange(group, value)"
|
||||
@visible-change="visible => visible && loadTaskFeeUnitOptions()"
|
||||
><el-option
|
||||
v-for="item in taskFeeUnitOptions"
|
||||
v-for="item in taskFeeUnitSelectOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value" /></el-select
|
||||
@@ -1969,17 +1992,17 @@
|
||||
<el-select
|
||||
v-model="dispatchItemForm.priceUnit"
|
||||
class="transport-plan-page__dispatch-unit-select"
|
||||
placeholder="元/吨"
|
||||
:placeholder="defaultPriceUnit"
|
||||
:loading="taskFeeUnitLoading"
|
||||
@visible-change="visible => visible && loadTaskFeeUnitOptions()"
|
||||
>
|
||||
<el-option
|
||||
v-if="!taskFeeUnitOptions.some(item => item.value === '元/吨')"
|
||||
label="元/吨"
|
||||
value="元/吨"
|
||||
v-if="!taskFeeUnitSelectOptions.some(item => item.value === defaultPriceUnit)"
|
||||
:label="defaultPriceUnit"
|
||||
:value="defaultPriceUnit"
|
||||
/>
|
||||
<el-option
|
||||
v-for="item in taskFeeUnitOptions"
|
||||
v-for="item in taskFeeUnitSelectOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
@@ -2201,11 +2224,22 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="200" align="center" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="200"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row, dispatchItemAttachmentRows)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
@keydown.enter="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
@keydown.space.prevent="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220" align="center">
|
||||
@@ -2741,7 +2775,15 @@ import { addressTypeOptions } from '@/option/base/common-address';
|
||||
import { packageOptions } from '@/option/business/common';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import {
|
||||
currencyUnit,
|
||||
mapFeeUnitOptions,
|
||||
remapPriceUnitCurrency,
|
||||
resolvePriceUnitByQuantityUnit,
|
||||
} from '@/utils/currency';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||
import { applyTableMenuWidth } from '@/utils/table-menu';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
@@ -3206,7 +3248,7 @@ export default {
|
||||
billingCargoTypeLoading: false,
|
||||
billingCargoTypeRequest: null,
|
||||
packageOptions,
|
||||
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
|
||||
quantityUnitOptions: [],
|
||||
attachmentFileTypes: [
|
||||
'pdf',
|
||||
'bmp',
|
||||
@@ -3245,6 +3287,7 @@ export default {
|
||||
} else {
|
||||
this.consumeTemplateCreatePayload();
|
||||
}
|
||||
this.loadTaskQuantityUnitOptions();
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
@@ -3609,6 +3652,22 @@ export default {
|
||||
const value = this.dispatchItemForm.freightCurrency || 'RMB';
|
||||
return this.currencyRemark(value);
|
||||
},
|
||||
taskFeeUnitSelectOptions() {
|
||||
const currency =
|
||||
this.dispatchItemForm.freightCurrency ||
|
||||
this.dispatchContractCurrency ||
|
||||
this.form.settlementCurrency ||
|
||||
'RMB';
|
||||
return mapFeeUnitOptions(this.taskFeeUnitOptions, currency);
|
||||
},
|
||||
defaultPriceUnit() {
|
||||
return `${currencyUnit(
|
||||
this.dispatchItemForm.freightCurrency ||
|
||||
this.dispatchContractCurrency ||
|
||||
this.form.settlementCurrency ||
|
||||
'RMB'
|
||||
)}/吨`;
|
||||
},
|
||||
isAdmin() {
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
@@ -3719,6 +3778,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
initStandaloneFormPage() {
|
||||
if (!this.isStandaloneFormPage) return;
|
||||
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
||||
@@ -4307,8 +4367,6 @@ export default {
|
||||
['contractName', '客户合同'],
|
||||
['planName', '计划名称'],
|
||||
['transportType', '运输方式'],
|
||||
['planStartDate', '计划开始日期'],
|
||||
['planEndDate', '计划结束日期'],
|
||||
['departureAddress', '发货地址'],
|
||||
['arrivalAddress', '收货地址'],
|
||||
];
|
||||
@@ -4804,19 +4862,49 @@ export default {
|
||||
contract.customerNames ||
|
||||
this.form.customerName ||
|
||||
'';
|
||||
const nextCurrency = this.getContractCurrency(contract);
|
||||
this.remapTransportPlanPriceUnits(nextCurrency);
|
||||
this.$refs.crud?.validateField?.('contractName');
|
||||
},
|
||||
remapTransportPlanPriceUnits(currency) {
|
||||
if (this.form.priceUnit) {
|
||||
this.form.priceUnit = remapPriceUnitCurrency(this.form.priceUnit, currency);
|
||||
}
|
||||
if (this.dispatchItemForm.priceUnit) {
|
||||
this.dispatchItemForm.priceUnit = remapPriceUnitCurrency(
|
||||
this.dispatchItemForm.priceUnit,
|
||||
currency
|
||||
);
|
||||
}
|
||||
this.dispatchItemForm.freightCurrency = currency;
|
||||
(this.dispatchItemCargoRows || []).forEach(cargo => {
|
||||
if (cargo.priceUnit) {
|
||||
cargo.priceUnit = remapPriceUnitCurrency(cargo.priceUnit, currency);
|
||||
}
|
||||
});
|
||||
(this.transportCargoRows || []).forEach(cargo => {
|
||||
if (cargo.priceUnit) {
|
||||
cargo.priceUnit = remapPriceUnitCurrency(cargo.priceUnit, currency);
|
||||
}
|
||||
});
|
||||
},
|
||||
loadDispatchContractCurrency(plan = {}) {
|
||||
const findContract = () =>
|
||||
this.contractOptions.find(item => String(item.id) === String(plan.contractId)) ||
|
||||
this.contractOptions.find(item => item.contractName === plan.contractName);
|
||||
const applyCurrency = contract => {
|
||||
const previousCurrency = this.dispatchContractCurrency;
|
||||
this.dispatchContractCurrency = this.getContractCurrency(
|
||||
contract &&
|
||||
(contract.settlementCurrency || contract.settlementCurrencyCode || contract.currency)
|
||||
? contract
|
||||
: { settlementCurrency: plan.settlementCurrency }
|
||||
);
|
||||
if (previousCurrency && previousCurrency !== this.dispatchContractCurrency) {
|
||||
this.remapTransportPlanPriceUnits(this.dispatchContractCurrency);
|
||||
} else {
|
||||
this.dispatchItemForm.freightCurrency = this.dispatchContractCurrency;
|
||||
}
|
||||
return this.dispatchContractCurrency;
|
||||
};
|
||||
const current = findContract();
|
||||
@@ -5076,15 +5164,40 @@ export default {
|
||||
return Promise.resolve(this.taskQuantityUnitOptions);
|
||||
}
|
||||
this.taskQuantityUnitLoading = true;
|
||||
return getDictionary({ code: 'quantity_unit' })
|
||||
.then(res => {
|
||||
this.taskQuantityUnitOptions = this.normalizeDictOptions(res.data.data || []);
|
||||
return loadMeasurementUnitOptions()
|
||||
.then(options => {
|
||||
this.taskQuantityUnitOptions = options;
|
||||
this.quantityUnitOptions = options.map(item => item.value);
|
||||
this.ensureQuantityUnitOptions([
|
||||
this.dispatchItemForm?.quantityUnit,
|
||||
...(this.dispatchItemCargoRows || []).map(row => row.quantityUnit),
|
||||
...(this.transportCargoRows || []).map(row => row.quantityUnit),
|
||||
]);
|
||||
return this.taskQuantityUnitOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.taskQuantityUnitLoading = false;
|
||||
});
|
||||
},
|
||||
ensureQuantityUnitOptions(units = []) {
|
||||
const pending = [...new Set(
|
||||
(Array.isArray(units) ? units : [units])
|
||||
.map(item => String(item || '').trim())
|
||||
.filter(Boolean)
|
||||
)];
|
||||
if (!pending.length) return;
|
||||
pending.forEach(unit => {
|
||||
if (!this.quantityUnitOptions.includes(unit)) {
|
||||
this.quantityUnitOptions = [...this.quantityUnitOptions, unit];
|
||||
}
|
||||
if (!this.taskQuantityUnitOptions.some(item => item.value === unit)) {
|
||||
this.taskQuantityUnitOptions = [
|
||||
...this.taskQuantityUnitOptions,
|
||||
{ label: unit, value: unit },
|
||||
];
|
||||
}
|
||||
});
|
||||
},
|
||||
loadTaskFeeUnitOptions() {
|
||||
if (this.taskFeeUnitOptions.length || this.taskFeeUnitLoading) {
|
||||
return Promise.resolve(this.taskFeeUnitOptions);
|
||||
@@ -5146,6 +5259,9 @@ export default {
|
||||
});
|
||||
},
|
||||
normalizeTransportCargoRow(row = {}) {
|
||||
const quantityUnit = String(
|
||||
row.quantityUnit || row.goodsQuantityUnit || row.unit || ''
|
||||
).trim();
|
||||
const cargo = {
|
||||
...defaultTransportCargo(),
|
||||
...row,
|
||||
@@ -5162,10 +5278,14 @@ export default {
|
||||
packageType: row.packageType || row.package || '',
|
||||
specification: row.specification || row.spec || '',
|
||||
freightAmount: row.freightAmount ?? row.amount ?? row.totalAmount ?? '',
|
||||
quantityUnit,
|
||||
};
|
||||
['quantity', 'mileage', 'unitPrice', 'freightAmount'].forEach(prop => {
|
||||
if (Number(cargo[prop]) === -1) cargo[prop] = '';
|
||||
});
|
||||
if (quantityUnit) {
|
||||
this.ensureQuantityUnitOptions(quantityUnit);
|
||||
}
|
||||
return cargo;
|
||||
},
|
||||
hasMultipleConfiguredGoods(source = {}, rows = []) {
|
||||
@@ -5183,7 +5303,7 @@ export default {
|
||||
},
|
||||
addTransportCargoRow(index = -1, row = {}) {
|
||||
const nextRow = this.normalizeTransportCargoRow({
|
||||
priceUnit: this.form.priceUnit || '元/吨',
|
||||
priceUnit: this.form.priceUnit || this.defaultPriceUnit,
|
||||
...row,
|
||||
});
|
||||
if (
|
||||
@@ -7594,6 +7714,9 @@ export default {
|
||||
...baseRow,
|
||||
...(index >= 0 ? row : {}),
|
||||
};
|
||||
this.dispatchItemForm.quantityUnit =
|
||||
this.dispatchItemForm.quantityUnit || defaultUnit || TRANSPORT_PLAN_QUANTITY_UNIT;
|
||||
this.ensureQuantityUnitOptions(this.dispatchItemForm.quantityUnit);
|
||||
this.dispatchItemForm.otherFeeTotal = this.normalizeDispatchFeeValue(
|
||||
this.dispatchItemForm.otherFeeTotal
|
||||
);
|
||||
@@ -7603,7 +7726,11 @@ export default {
|
||||
});
|
||||
const goodsRows = this.parseJsonArray(this.dispatchItemForm.goodsJson);
|
||||
this.dispatchItemCargoRows = (goodsRows.length ? goodsRows : [this.dispatchItemForm]).map(
|
||||
item => this.normalizeTransportCargoRow(item)
|
||||
item =>
|
||||
this.normalizeTransportCargoRow({
|
||||
...item,
|
||||
quantityUnit: item.quantityUnit || this.dispatchItemForm.quantityUnit || defaultUnit,
|
||||
})
|
||||
);
|
||||
const hasMultiplePlanGoods = this.dispatchPlanGoodsRows.length > 1;
|
||||
const shouldAutoFillFullGoods =
|
||||
@@ -7613,11 +7740,18 @@ export default {
|
||||
const unscheduledGoodsRows = this.getDispatchUnscheduledGoodsRows();
|
||||
this.dispatchItemForm.taskEntryMode = 'full';
|
||||
this.dispatchItemForm.quantity = '';
|
||||
this.dispatchItemCargoRows = unscheduledGoodsRows.length
|
||||
? unscheduledGoodsRows
|
||||
: this.dispatchPlanGoodsRows.map(goods =>
|
||||
this.normalizeTransportCargoRow({ ...goods, quantity: '' })
|
||||
);
|
||||
this.dispatchItemCargoRows = (
|
||||
unscheduledGoodsRows.length
|
||||
? unscheduledGoodsRows
|
||||
: this.dispatchPlanGoodsRows.map(goods =>
|
||||
this.normalizeTransportCargoRow({ ...goods, quantity: '' })
|
||||
)
|
||||
).map(goods =>
|
||||
this.normalizeTransportCargoRow({
|
||||
...goods,
|
||||
quantityUnit: goods.quantityUnit || defaultUnit || TRANSPORT_PLAN_QUANTITY_UNIT,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!this.dispatchItemCargoRows.length) {
|
||||
this.dispatchItemCargoRows = [this.normalizeTransportCargoRow()];
|
||||
@@ -7632,7 +7766,10 @@ export default {
|
||||
this.dispatchItemCargoRows = this.dispatchItemCargoRows.map((cargo, itemIndex) => ({
|
||||
...cargo,
|
||||
unitPrice: cargo.unitPrice || freightInfo.freightItems[itemIndex]?.unitPrice || '',
|
||||
priceUnit: cargo.priceUnit || freightInfo.freightItems[itemIndex]?.priceUnit || '元/吨',
|
||||
priceUnit:
|
||||
cargo.priceUnit ||
|
||||
freightInfo.freightItems[itemIndex]?.priceUnit ||
|
||||
this.defaultPriceUnit,
|
||||
freightAmount:
|
||||
cargo.freightAmount || freightInfo.freightItems[itemIndex]?.freightAmount || '',
|
||||
}));
|
||||
@@ -7881,7 +8018,7 @@ export default {
|
||||
return value;
|
||||
},
|
||||
addDispatchCargoRow(index = -1) {
|
||||
const nextRow = this.normalizeTransportCargoRow({ priceUnit: '元/吨' });
|
||||
const nextRow = this.normalizeTransportCargoRow({ priceUnit: this.defaultPriceUnit });
|
||||
if (index > -1) this.dispatchItemCargoRows.splice(index + 1, 0, nextRow);
|
||||
else this.dispatchItemCargoRows.push(nextRow);
|
||||
},
|
||||
@@ -7925,7 +8062,14 @@ export default {
|
||||
return prices[0];
|
||||
},
|
||||
dispatchFreightGroupPriceUnit(group = {}) {
|
||||
return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : '');
|
||||
if (group.rows?.[0]?.priceUnit) return group.rows[0].priceUnit;
|
||||
if (!group.quantityUnit) return '';
|
||||
return (
|
||||
resolvePriceUnitByQuantityUnit(
|
||||
group.quantityUnit,
|
||||
this.dispatchItemForm.freightCurrency || this.dispatchContractCurrency || 'RMB'
|
||||
) || `${currencyUnit(this.dispatchContractCurrency || 'RMB')}/${group.quantityUnit}`
|
||||
);
|
||||
},
|
||||
dispatchFreightGroupAmount(group = {}) {
|
||||
const amount = (group.rows || []).reduce(
|
||||
@@ -8309,7 +8453,10 @@ export default {
|
||||
this.dispatchItemForm.cargoName ||
|
||||
'',
|
||||
quantity: quantityText,
|
||||
quantityUnit: firstGoods.quantityUnit || this.dispatchItemForm.quantityUnit || '',
|
||||
quantityUnit:
|
||||
firstGoods.quantityUnit ||
|
||||
this.dispatchItemForm.quantityUnit ||
|
||||
TRANSPORT_PLAN_QUANTITY_UNIT,
|
||||
unitPrice: firstGoods.unitPrice || '',
|
||||
priceUnit: firstGoods.priceUnit || '',
|
||||
freightTotal: this.dispatchItemFreightTotal,
|
||||
@@ -8324,7 +8471,7 @@ export default {
|
||||
unitPrice: cargo.unitPrice || '',
|
||||
priceUnit: cargo.priceUnit || '',
|
||||
quantity: cargo.quantity || '',
|
||||
quantityUnit: cargo.quantityUnit || '',
|
||||
quantityUnit: cargo.quantityUnit || firstGoods.quantityUnit || TRANSPORT_PLAN_QUANTITY_UNIT,
|
||||
freightAmount: this.dispatchCargoFreightAmount(cargo),
|
||||
})),
|
||||
}),
|
||||
@@ -8413,26 +8560,40 @@ export default {
|
||||
return {
|
||||
id: this.dispatchRow.id,
|
||||
mode,
|
||||
waybills: rows.map(row => ({
|
||||
...row,
|
||||
taskEntryMode: row.taskEntryMode || 'simple',
|
||||
taskRemark: row.remark || '',
|
||||
estimatedStartTime: this.formatDispatchDate(row.estimatedStartTime),
|
||||
estimatedEndTime: this.formatDispatchDate(row.estimatedEndTime),
|
||||
goodsJson:
|
||||
row.goodsJson ||
|
||||
JSON.stringify([
|
||||
waybills: rows.map(row => {
|
||||
const quantityUnit = this.getDispatchQuantityUnit(row);
|
||||
let goodsJson = row.goodsJson;
|
||||
const goodsRows = this.parseJsonArray(goodsJson);
|
||||
if (goodsRows.length) {
|
||||
goodsJson = JSON.stringify(
|
||||
goodsRows.map(goods => ({
|
||||
...goods,
|
||||
quantityUnit: goods.quantityUnit || quantityUnit,
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
goodsJson = JSON.stringify([
|
||||
{
|
||||
cargoName: row.cargoName,
|
||||
cargoType: row.cargoType,
|
||||
quantity: row.quantity,
|
||||
quantityUnit: row.quantityUnit,
|
||||
quantityUnit,
|
||||
specification: row.specification,
|
||||
model: row.model,
|
||||
packageType: row.packageType,
|
||||
},
|
||||
]),
|
||||
})),
|
||||
]);
|
||||
}
|
||||
return {
|
||||
...row,
|
||||
quantityUnit,
|
||||
taskEntryMode: row.taskEntryMode || 'simple',
|
||||
taskRemark: row.remark || '',
|
||||
estimatedStartTime: this.formatDispatchDate(row.estimatedStartTime),
|
||||
estimatedEndTime: this.formatDispatchDate(row.estimatedEndTime),
|
||||
goodsJson,
|
||||
};
|
||||
}),
|
||||
};
|
||||
},
|
||||
formatDispatchDate(value) {
|
||||
@@ -8680,7 +8841,7 @@ export default {
|
||||
|
||||
&__dispatch-shipping-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9871,7 +10032,7 @@ export default {
|
||||
}
|
||||
|
||||
:global(.transport-plan-form-page-dialog .avue-group .el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:global(.transport-plan-form-page-dialog .avue-group .dialog-section-title) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="voucher-folder-page voucher-folder-page--embedded">
|
||||
<section class="voucher-folder-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="160px">
|
||||
<el-form :model="query" label-position="right">
|
||||
<div class="voucher-folder-page__search-grid">
|
||||
<el-form-item label="车牌号">
|
||||
<el-input v-model="query.plateNo" clearable placeholder="请输入" />
|
||||
@@ -196,7 +196,7 @@ watch(
|
||||
}
|
||||
&__search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 360px 1fr;
|
||||
grid-template-columns: 220px 220px 1fr;
|
||||
gap: 8px 24px;
|
||||
}
|
||||
&__search-actions {
|
||||
|
||||
@@ -270,15 +270,22 @@
|
||||
prop="mileage"
|
||||
label="里程(km)"
|
||||
width="100"
|
||||
/><el-table-column prop="unitPrice" label="单价" width="90" /><el-table-column
|
||||
prop="freight"
|
||||
label="运费"
|
||||
width="100"
|
||||
/><el-table-column prop="otherFeeTotal" label="其他费用合计" width="120" /><el-table-column
|
||||
prop="freightTotal"
|
||||
label="运费合计"
|
||||
width="100"
|
||||
/><el-table-column
|
||||
>
|
||||
<template #default="{ row }">{{ formatOptionalNumber(row.mileage) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="unitPrice" label="单价" width="90">
|
||||
<template #default="{ row }">{{ formatOptionalNumber(row.unitPrice) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="freight" label="运费" width="100">
|
||||
<template #default="{ row }">{{ formatOptionalNumber(row.freight) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="otherFeeTotal" label="其他费用合计" width="120">
|
||||
<template #default="{ row }">{{ formatOptionalNumber(row.otherFeeTotal) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="freightTotal" label="运费合计" width="100">
|
||||
<template #default="{ row }">{{ formatOptionalNumber(row.freightTotal) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="actualStartDate"
|
||||
label="实际发货时间"
|
||||
width="160"
|
||||
@@ -374,7 +381,7 @@
|
||||
<el-option
|
||||
v-for="item in contracts"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -610,6 +617,7 @@ import { getList as getProjectList } from '@/api/business/project-apply';
|
||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import * as api from '@/api/business/waybill-manage';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import SectionCard from '@/components/section-card/main.vue';
|
||||
|
||||
@@ -879,11 +887,14 @@ const syncCarrierOptions = () => {
|
||||
carrierOptions.value = [];
|
||||
return;
|
||||
}
|
||||
const carrierId =
|
||||
const carrierId = resolveCarrierArchiveId(
|
||||
customerContract?.partyBId ||
|
||||
customerContract?.partyBUserId ||
|
||||
customerContract?.customerId ||
|
||||
'';
|
||||
customerContract?.partyBUserId ||
|
||||
customerContract?.contractPartyBId ||
|
||||
customerContract?.customerId ||
|
||||
'',
|
||||
carrierName
|
||||
);
|
||||
carrierOptions.value = [{ id: carrierId, carrierName, label: carrierName, value: carrierName }];
|
||||
form.carrierId = carrierId;
|
||||
form.carrierIds = carrierId ? [carrierId] : [];
|
||||
@@ -1079,19 +1090,40 @@ const transportTypeLabel = value => {
|
||||
);
|
||||
return matched?.dictValue || matched?.label || named || text || '-';
|
||||
};
|
||||
const buildBatchQueryParams = () => ({
|
||||
batchNo: query.batchNo || undefined,
|
||||
carrierId: query.carrierId || undefined,
|
||||
createUser: query.createUser || undefined,
|
||||
createTimeStart: query.createTimeRange?.[0] || undefined,
|
||||
createTimeEnd: query.createTimeRange?.[1] || undefined,
|
||||
current: page.current,
|
||||
size: page.size,
|
||||
});
|
||||
const loadListOptions = async () => {
|
||||
const res = await api.getImportOptions?.();
|
||||
const data = res?.data?.data || {};
|
||||
queryCarriers.value = data.carriers || [];
|
||||
creators.value = data.creators || [];
|
||||
};
|
||||
const loadBatches = async () => {
|
||||
const res = await api.getImportBatches({ ...query, current: page.current, size: page.size });
|
||||
const res = await api.getImportBatches(buildBatchQueryParams());
|
||||
batches.value = res.data?.data?.records || [];
|
||||
page.total = res.data?.data?.total || 0;
|
||||
};
|
||||
onMounted(() => {
|
||||
if (props.standalone) loadBatches();
|
||||
if (props.standalone) {
|
||||
loadListOptions();
|
||||
loadBatches();
|
||||
}
|
||||
if (props.createPage) openCreatePage();
|
||||
});
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
value => {
|
||||
if (value) loadBatches();
|
||||
if (value) {
|
||||
loadListOptions();
|
||||
loadBatches();
|
||||
}
|
||||
}
|
||||
);
|
||||
const resetQuery = () => {
|
||||
@@ -1106,7 +1138,7 @@ const loadDetails = async () => {
|
||||
current: detailPage.current,
|
||||
size: detailPage.size,
|
||||
});
|
||||
details.value = res.data?.data?.records || [];
|
||||
details.value = (res.data?.data?.records || []).map(normalizeDetailRow);
|
||||
detailPage.total = res.data?.data?.total || 0;
|
||||
};
|
||||
const openCreate = () => prepareCreateForm(true);
|
||||
@@ -1155,7 +1187,7 @@ const loadBatchRows = async batchId => {
|
||||
if (!batchId) return;
|
||||
const res = await api.getImportDetails({ batchId, current: 1, size: 9999 });
|
||||
rows.value = extractRecords(res).map(({ id, ...item }, index) => ({
|
||||
...item,
|
||||
...normalizeDetailRow(item),
|
||||
_key: `${id || 'row'}-${index}`,
|
||||
batchNo: item.batchNo || form.batchNo,
|
||||
}));
|
||||
@@ -1288,15 +1320,60 @@ const contractChange = id => {
|
||||
const carrierTypeChange = () => {
|
||||
syncCarrierOptions();
|
||||
};
|
||||
const resolveCarrierArchiveId = (carrierId, carrierName) => {
|
||||
if (carrierId) return carrierId;
|
||||
const name = String(carrierName || '').trim();
|
||||
if (!name) return '';
|
||||
const matched = queryCarriers.value.find(item => String(item.name || '').trim() === name);
|
||||
return matched?.id || '';
|
||||
};
|
||||
const carrierChange = value => {
|
||||
const item = carrierOptions.value.find(option => String(option.value) === String(value || ''));
|
||||
clearCarrier();
|
||||
if (!item) return;
|
||||
form.carrierId = item.id || '';
|
||||
form.carrierIds = item.id ? [item.id] : [];
|
||||
const carrierId = resolveCarrierArchiveId(item.id, item.carrierName);
|
||||
form.carrierId = carrierId;
|
||||
form.carrierIds = carrierId ? [carrierId] : [];
|
||||
form.carrierName = item.carrierName || '';
|
||||
if (!isSelfOperated.value) form.carrierContractId = item.carrierContractId || '';
|
||||
};
|
||||
const firstNotEmpty = (...values) => values.find(value => String(value ?? '').trim()) ?? '';
|
||||
const OPTIONAL_NUMBER_EMPTY_SENTINELS = [-1];
|
||||
const OPTIONAL_NUMBER_PROPS = ['mileage', 'unitPrice', 'freight', 'otherFeeTotal', 'freightTotal'];
|
||||
const normalizeOptionalImportNumber = (value, emptySentinels = OPTIONAL_NUMBER_EMPTY_SENTINELS) => {
|
||||
if (!String(value ?? '').trim()) return null;
|
||||
return emptySentinels.some(sentinel => Number(value) === sentinel) ? null : value;
|
||||
};
|
||||
const formatOptionalNumber = value => {
|
||||
const normalized = normalizeOptionalImportNumber(value);
|
||||
return normalized === null || normalized === undefined ? '' : normalized;
|
||||
};
|
||||
const normalizeDetailRow = row => {
|
||||
const next = { ...(row || {}) };
|
||||
OPTIONAL_NUMBER_PROPS.forEach(prop => {
|
||||
next[prop] = formatOptionalNumber(next[prop]);
|
||||
});
|
||||
return next;
|
||||
};
|
||||
const normalizeImportRow = row => ({
|
||||
...row,
|
||||
mileage: normalizeOptionalImportNumber(
|
||||
firstNotEmpty(row.mileage, row['里程'], row['里程(km)'], row['里程(公里)'])
|
||||
),
|
||||
otherFeeTotal: normalizeOptionalImportNumber(
|
||||
firstNotEmpty(row.otherFeeTotal, row['其他费用合计'])
|
||||
),
|
||||
unitPrice: normalizeOptionalImportNumber(firstNotEmpty(row.unitPrice, row['单价'])),
|
||||
freight: normalizeOptionalImportNumber(firstNotEmpty(row.freight, row['运费'])),
|
||||
freightTotal: normalizeOptionalImportNumber(firstNotEmpty(row.freightTotal, row['运费合计'])),
|
||||
transportType: firstNotEmpty(
|
||||
row.transportType,
|
||||
row['运输类型'],
|
||||
row['*运输类型'],
|
||||
row['运输方式'],
|
||||
row['*运输方式']
|
||||
),
|
||||
});
|
||||
const fileChange = async (file, list) => {
|
||||
if (!projectInfoReady.value) {
|
||||
ElMessage.warning('请先选择项目、客户合同和承运信息,再导入运单明细');
|
||||
@@ -1370,6 +1447,9 @@ const fileChange = async (file, list) => {
|
||||
item[key] ??
|
||||
'';
|
||||
});
|
||||
['mileage', 'unitPrice', 'freight', 'otherFeeTotal', 'freightTotal'].forEach(prop => {
|
||||
row[prop] = normalizeOptionalImportNumber(row[prop]);
|
||||
});
|
||||
row.actualStartDate = normalizeImportDate(row.actualStartDate);
|
||||
row.actualEndDate = normalizeImportDate(row.actualEndDate);
|
||||
row.planStartDate = normalizeImportDate(row.planStartDate);
|
||||
@@ -1618,6 +1698,7 @@ const downloadTemplate = async () => {
|
||||
ElMessage.error('模板下载失败');
|
||||
}
|
||||
};
|
||||
const isValidMobile = value => /^\d{11}$/.test(String(value || '').trim());
|
||||
const validateImportRows = () => {
|
||||
if (!rows.value.length) {
|
||||
ElMessage.warning('请上传至少一条运单明细');
|
||||
@@ -1629,6 +1710,11 @@ const validateImportRows = () => {
|
||||
ElMessage.warning(`第${index + 1}行${missingField.label}不能为空`);
|
||||
return false;
|
||||
}
|
||||
const driverPhone = String(row.driverPhone ?? '').trim();
|
||||
if (driverPhone && !isValidMobile(driverPhone)) {
|
||||
ElMessage.warning(`第${index + 1}行司机/船长手机号格式不正确,应为11位数字`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@@ -1656,31 +1742,6 @@ const removeSelectedRows = () => {
|
||||
rowSelection.value = [];
|
||||
refreshDuplicateFlags();
|
||||
};
|
||||
const firstNotEmpty = (...values) => values.find(value => String(value ?? '').trim()) ?? '';
|
||||
const normalizeOptionalImportNumber = (value, emptySentinels = []) => {
|
||||
if (!String(value ?? '').trim()) return null;
|
||||
return emptySentinels.some(sentinel => Number(value) === sentinel) ? null : value;
|
||||
};
|
||||
const normalizeImportRow = row => ({
|
||||
...row,
|
||||
mileage: normalizeOptionalImportNumber(
|
||||
firstNotEmpty(row.mileage, row['里程'], row['里程(km)'], row['里程(公里)']),
|
||||
[-1]
|
||||
),
|
||||
otherFeeTotal: normalizeOptionalImportNumber(
|
||||
firstNotEmpty(row.otherFeeTotal, row['其他费用合计'])
|
||||
),
|
||||
unitPrice: normalizeOptionalImportNumber(firstNotEmpty(row.unitPrice, row['单价'])),
|
||||
freight: normalizeOptionalImportNumber(firstNotEmpty(row.freight, row['运费'])),
|
||||
freightTotal: normalizeOptionalImportNumber(firstNotEmpty(row.freightTotal, row['运费合计'])),
|
||||
transportType: firstNotEmpty(
|
||||
row.transportType,
|
||||
row['运输类型'],
|
||||
row['*运输类型'],
|
||||
row['运输方式'],
|
||||
row['*运输方式']
|
||||
),
|
||||
});
|
||||
const normalizeOptionalPlanId = value => {
|
||||
if (value === undefined || value === null || value === '') return null;
|
||||
return Number(value) === -1 || Number(value) <= 0 ? null : value;
|
||||
@@ -1688,11 +1749,23 @@ const normalizeOptionalPlanId = value => {
|
||||
const buildImportPayload = (status = form.status, rowList = rows.value, extra = {}) => {
|
||||
const { file, ...batch } = form;
|
||||
const planId = normalizeOptionalPlanId(batch.planId);
|
||||
const carrierId = resolveCarrierArchiveId(
|
||||
batch.carrierId || batch.carrierIds?.[0] || '',
|
||||
batch.carrierName
|
||||
);
|
||||
const carrierIds =
|
||||
Array.isArray(batch.carrierIds) && batch.carrierIds.length
|
||||
? batch.carrierIds
|
||||
: carrierId
|
||||
? [carrierId]
|
||||
: [];
|
||||
return {
|
||||
...batch,
|
||||
planId,
|
||||
// 未选计划时不带占位名称,避免落库/回显 -1
|
||||
planName: planId ? batch.planName || undefined : null,
|
||||
carrierId: carrierId || undefined,
|
||||
carrierIds,
|
||||
status,
|
||||
rows: rowList.map(normalizeImportRow).map(stripInternalRowFields),
|
||||
...extra,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,7 @@
|
||||
<el-form-item label="项目">
|
||||
<el-input v-model="form.projectName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属组织">
|
||||
<el-form-item label="合同负责部门">
|
||||
<el-input v-model="form.organizationName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="甲方">
|
||||
@@ -63,6 +63,15 @@
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期">
|
||||
<el-date-picker
|
||||
v-model="form.applyDate"
|
||||
type="date"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同期限">
|
||||
<div class="contract-basic-section__date-range">
|
||||
<el-date-picker
|
||||
@@ -91,12 +100,13 @@
|
||||
<template #suffix>天</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同金额">
|
||||
<el-form-item label="合同金额" prop="contractAmount">
|
||||
<el-input
|
||||
:model-value="contractAmountDisplay"
|
||||
placeholder="请输入"
|
||||
clearable
|
||||
@input="handleContractAmountInput"
|
||||
@blur="formatContractAmountField"
|
||||
@clear="form.contractAmount = null"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -154,7 +164,7 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否需要加盖法人章">
|
||||
<el-select v-model="form.legalSealFlag" placeholder="请选择">
|
||||
<el-select v-model="form.legalSealFlag" placeholder="请选择" clearable>
|
||||
<el-option label="否" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
@@ -173,20 +183,30 @@
|
||||
<el-input v-model="form.handlerUserName" disabled />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="备注" class="contract-basic-section__remark">
|
||||
<el-form-item label="合同主要内容" class="contract-basic-section__remark">
|
||||
<el-input
|
||||
v-model="form.mainContent"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入合同主要内容"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="其他事项说明" class="contract-basic-section__remark">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入备注"
|
||||
placeholder="请输入其他事项说明"
|
||||
/>
|
||||
</el-form-item>
|
||||
</section>
|
||||
|
||||
<contract-attachment-section
|
||||
title="合同文件"
|
||||
title="用印材料"
|
||||
description
|
||||
attachment-type
|
||||
:attachment-location-options="changeAttachmentLocationOptions"
|
||||
@@ -251,13 +271,20 @@
|
||||
<el-button type="primary" plain @click="addPaymentRatioRow">添加</el-button>
|
||||
</section>
|
||||
|
||||
<billing-plan-editor v-model="planDialogVisible" :value="planEditor" :index="planEditorIndex" @save="savePlan" />
|
||||
<billing-plan-editor
|
||||
v-model="planDialogVisible"
|
||||
:value="planEditor"
|
||||
:index="planEditorIndex"
|
||||
:settlement-currency="form.settlementCurrency"
|
||||
@save="savePlan"
|
||||
/>
|
||||
|
||||
<contract-attachment-section
|
||||
title="其它附件"
|
||||
title="附件(佐证材料等)"
|
||||
description
|
||||
attachment-type
|
||||
attachment-location="其它附件"
|
||||
attachment-location="附件(佐证材料等)"
|
||||
attachment-type-dict-code="contract_supporting_attachment_type"
|
||||
:attachment-location-options="changeAttachmentLocationOptions"
|
||||
:rows="attachments"
|
||||
:preview="previewAttachment"
|
||||
@@ -308,6 +335,8 @@ import * as api from '@/api/business/contract-manage';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import BillingPlanEditor from './components/billing-plan-editor.vue';
|
||||
import ContractAttachmentSection, {
|
||||
isContractSealMaterialLocation,
|
||||
isContractSupportingAttachmentLocation,
|
||||
normalizeContractFileRows,
|
||||
} from './components/contract-attachment-section.vue';
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
@@ -358,6 +387,19 @@ const normalizeOptionalAmount = value => {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : null;
|
||||
};
|
||||
const formatContractAmountInput = (value, emptyValue = null) => {
|
||||
if (
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
Number(value) === -1 ||
|
||||
String(value) === '-1'
|
||||
) {
|
||||
return emptyValue;
|
||||
}
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number >= 0 ? number.toFixed(2) : emptyValue;
|
||||
};
|
||||
|
||||
|
||||
const normalizeCustomPeriods = (periods = []) =>
|
||||
@@ -424,6 +466,21 @@ export default {
|
||||
],
|
||||
rules: {
|
||||
contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
|
||||
contractAmount: [
|
||||
{ required: true, message: '请输入合同金额', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return callback(new Error('请输入合同金额'));
|
||||
}
|
||||
if (Number(value) < 0) {
|
||||
return callback(new Error('合同金额不能小于0'));
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
changeReason: [{ max: 500, message: '变更原因不能超过500个字符', trigger: 'blur' }],
|
||||
},
|
||||
};
|
||||
@@ -433,7 +490,7 @@ export default {
|
||||
return this.$route.query.name || '合同变更';
|
||||
},
|
||||
changeAttachmentLocationOptions() {
|
||||
return ['合同文件', '其它附件', '变更材料'];
|
||||
return ['用印材料', '附件(佐证材料等)', '变更材料'];
|
||||
},
|
||||
contractAmountDisplay() {
|
||||
const value = this.form.contractAmount;
|
||||
@@ -498,6 +555,9 @@ export default {
|
||||
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
|
||||
this.form.contractAmount = normalized === '' ? null : normalized;
|
||||
},
|
||||
formatContractAmountField() {
|
||||
this.form.contractAmount = formatContractAmountInput(this.form.contractAmount);
|
||||
},
|
||||
loadDictionaries() {
|
||||
Promise.all([
|
||||
getSystemDictionary({ code: 'currency_type' }),
|
||||
@@ -540,7 +600,7 @@ export default {
|
||||
copyCount: normalizeOptionalPositiveInteger(data.copyCount),
|
||||
invoiceCycle: normalizeOptionalPositiveInteger(data.invoiceCycle),
|
||||
paymentDays: normalizeOptionalPositiveInteger(data.paymentDays),
|
||||
contractAmount: normalizeOptionalAmount(data.contractAmount),
|
||||
contractAmount: formatContractAmountInput(data.contractAmount),
|
||||
templateFlag: data.templateFlag ?? 0,
|
||||
electronicSealFlag: data.electronicSealFlag ?? 0,
|
||||
archiveStatus: data.archiveStatus || '未归档',
|
||||
@@ -601,7 +661,7 @@ export default {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
// 审核通过后上一次的变更材料归集到「其它附件」,按文件地址/文件名去重,避免重复展示
|
||||
// 审核通过后上一次的变更材料归集到「附件(佐证材料等)」,按文件地址/文件名去重,避免重复展示
|
||||
mergeChangeAttachments(attachments = [], changeAttachments = []) {
|
||||
const key = item => this.attachmentUrl(item) || this.attachmentName(item);
|
||||
const exists = new Set(attachments.map(key));
|
||||
@@ -655,11 +715,11 @@ export default {
|
||||
previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; },
|
||||
handleAttachmentUploadToLocation({ location, rows = [] }) {
|
||||
if (!rows.length) return;
|
||||
if (location === '合同文件') {
|
||||
if (isContractSealMaterialLocation(location)) {
|
||||
this.contractFileRows = [...(this.contractFileRows || []), ...rows];
|
||||
return;
|
||||
}
|
||||
if (location === '其它附件') {
|
||||
if (isContractSupportingAttachmentLocation(location)) {
|
||||
this.attachments = [...(this.attachments || []), ...rows];
|
||||
return;
|
||||
}
|
||||
@@ -692,11 +752,18 @@ export default {
|
||||
.contract-basic-section > .dialog-section-title { margin-bottom: 20px; color: #303133; }
|
||||
.contract-basic-section__grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: 48px; }
|
||||
.contract-basic-section__change-type { grid-column: 1 / -1; }
|
||||
.contract-basic-section__remark { width: 100%; margin-bottom: 0 !important; }
|
||||
.contract-basic-section__remark {
|
||||
width: 100%;
|
||||
margin-bottom: 23px !important;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
.contract-basic-section__date-range { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 8px; width: 360px; max-width: 100%; }
|
||||
.contract-basic-section__date-range span { color: #606266; }
|
||||
.contract-basic-section__date-range :deep(.el-date-editor) { width: auto; min-width: 0; }
|
||||
.contract-basic-section :deep(.el-form-item) { align-items: center; margin-bottom: 16px; }
|
||||
.contract-basic-section :deep(.el-form-item) { align-items: center; }
|
||||
.contract-basic-section :deep(.el-form-item__label) { flex: 0 0 96px !important; width: 96px !important; height: auto; padding-right: 12px; white-space: normal; word-break: break-all; line-height: 18px; }
|
||||
.contract-basic-section :deep(.el-form-item__content) { min-width: 0; }
|
||||
.contract-basic-section :deep(.el-input),
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
<el-descriptions-item label="甲方">{{ detailValue('partyA') }}</el-descriptions-item>
|
||||
<el-descriptions-item label="乙方">{{ detailValue('partyB') }}</el-descriptions-item>
|
||||
<el-descriptions-item label="合同期限">{{ detailContractPeriod }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属组织">{{
|
||||
<el-descriptions-item label="合同负责部门">{{
|
||||
detailValue('organizationName')
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="经办人">{{
|
||||
@@ -140,6 +140,9 @@
|
||||
<el-descriptions-item label="签订日期">{{
|
||||
detailValue('signDate')
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="申请日期">{{
|
||||
detailValue('applyDate')
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="合同格式">{{
|
||||
detailValue('contractFormat')
|
||||
}}</el-descriptions-item>
|
||||
@@ -171,14 +174,17 @@
|
||||
<el-descriptions-item label="是否电子章">
|
||||
{{ detailElectronicSealText }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="3">{{
|
||||
<el-descriptions-item label="合同主要内容" :span="3">{{
|
||||
detailValue('mainContent')
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="其他事项说明" :span="3">{{
|
||||
detailValue('remark')
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</section>
|
||||
|
||||
<contract-attachment-section
|
||||
title="合同文件"
|
||||
title="用印材料"
|
||||
description
|
||||
attachment-type
|
||||
readonly
|
||||
@@ -294,7 +300,7 @@
|
||||
</section>
|
||||
|
||||
<contract-attachment-section
|
||||
title="其它附件"
|
||||
title="附件(佐证材料等)"
|
||||
description
|
||||
readonly
|
||||
:rows="detailAttachmentRows"
|
||||
@@ -377,8 +383,14 @@
|
||||
<el-form-item label="合同编号">
|
||||
<el-input
|
||||
v-model="form.contractNo"
|
||||
:disabled="formMode === 'edit'"
|
||||
placeholder="不填写,则系统自动生成"
|
||||
disabled
|
||||
:placeholder="
|
||||
formMode === 'add'
|
||||
? form.organizationId
|
||||
? '系统按部门规则自动生成'
|
||||
: '请先选择合同负责部门'
|
||||
: ''
|
||||
"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同名称" prop="contractName">
|
||||
@@ -417,7 +429,7 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属组织" prop="organizationName">
|
||||
<el-form-item label="合同负责部门" prop="organizationName">
|
||||
<el-cascader
|
||||
v-model="organizationCascaderValue"
|
||||
:options="organizationOptions"
|
||||
@@ -471,6 +483,16 @@
|
||||
placeholder="请选择签订日期"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期">
|
||||
<el-date-picker
|
||||
v-model="form.applyDate"
|
||||
type="date"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="提交正式合同时自动填充"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同期限" prop="startDate">
|
||||
<div class="contract-manage-form__date-range">
|
||||
<el-date-picker
|
||||
@@ -497,12 +519,13 @@
|
||||
@input="value => integerInput('paymentDays', value)"
|
||||
><template #suffix>天</template></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同金额">
|
||||
<el-form-item label="合同金额" prop="contractAmount">
|
||||
<el-input
|
||||
:model-value="contractAmountDisplay"
|
||||
placeholder="请输入"
|
||||
clearable
|
||||
@input="handleContractAmountInput"
|
||||
@blur="formatContractAmountField"
|
||||
@clear="form.contractAmount = null"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -557,7 +580,7 @@
|
||||
>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否需要加盖法人章" prop="legalSealFlag">
|
||||
<el-select v-model="form.legalSealFlag" placeholder="请选择">
|
||||
<el-select v-model="form.legalSealFlag" placeholder="请选择" clearable>
|
||||
<el-option label="否" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
@@ -576,20 +599,30 @@
|
||||
<el-input v-model="form.handlerUserName" disabled />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="备注" prop="remark" class="contract-manage-form__remark">
|
||||
<el-form-item label="合同主要内容" prop="mainContent" class="contract-manage-form__remark">
|
||||
<el-input
|
||||
v-model="form.mainContent"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入合同主要内容"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="其他事项说明" prop="remark" class="contract-manage-form__remark">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="200"
|
||||
:rows="4"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入备注"
|
||||
placeholder="请输入其他事项说明"
|
||||
/>
|
||||
</el-form-item>
|
||||
</section>
|
||||
|
||||
<contract-attachment-section
|
||||
title="合同文件"
|
||||
title="用印材料"
|
||||
description
|
||||
attachment-type
|
||||
:attachment-location-options="contractAttachmentLocationOptions"
|
||||
@@ -809,10 +842,11 @@
|
||||
</section>
|
||||
|
||||
<contract-attachment-section
|
||||
title="其它附件"
|
||||
title="附件(佐证材料等)"
|
||||
description
|
||||
attachment-type
|
||||
attachment-location="其它附件"
|
||||
attachment-location="附件(佐证材料等)"
|
||||
attachment-type-dict-code="contract_supporting_attachment_type"
|
||||
:attachment-location-options="contractAttachmentLocationOptions"
|
||||
:readonly="isAttachmentUploadMode"
|
||||
:rows="attachmentRows"
|
||||
@@ -920,18 +954,19 @@
|
||||
v-model="billingPlanBox"
|
||||
:value="billingPlanForm"
|
||||
:index="billingPlanIndex"
|
||||
:settlement-currency="form.settlementCurrency"
|
||||
@save="saveBillingPlan"
|
||||
/>
|
||||
|
||||
<contract-attachment-section
|
||||
ref="listAttachmentUploader"
|
||||
dialog-only
|
||||
title="合同文件"
|
||||
title="用印材料"
|
||||
description
|
||||
attachment-type
|
||||
use-upload-dialog
|
||||
mark-approved-on-upload
|
||||
:attachment-location-options="['合同文件', '其它附件']"
|
||||
:attachment-location-options="['用印材料', '附件(佐证材料等)']"
|
||||
:rows="listUploadContractFileRows"
|
||||
@update:rows="listUploadContractFileRows = $event"
|
||||
@upload-to-location="handleListAttachmentUploadToLocation"
|
||||
@@ -988,6 +1023,7 @@
|
||||
v-model="detailBillingPlanBox"
|
||||
:value="detailBillingPlanForm"
|
||||
:index="detailBillingPlanIndex"
|
||||
:settlement-currency="detailRow.settlementCurrency || form.settlementCurrency"
|
||||
readonly
|
||||
/>
|
||||
|
||||
@@ -1019,12 +1055,15 @@ import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||||
import { config, option } from '@/option/business/contract-manage';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import BillingPlanEditor from './components/billing-plan-editor.vue';
|
||||
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
|
||||
import ContractAttachmentSection, {
|
||||
attachmentName as sharedAttachmentName,
|
||||
attachmentUrl as sharedAttachmentUrl,
|
||||
isContractSealMaterialLocation,
|
||||
isContractSupportingAttachmentLocation,
|
||||
normalizeContractFileRows,
|
||||
} from './components/contract-attachment-section.vue';
|
||||
import PdfPreview from '@/components/pdf-preview/main.vue';
|
||||
@@ -1060,6 +1099,19 @@ const normalizeOptionalAmount = (value, emptyValue = null) => {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : emptyValue;
|
||||
};
|
||||
const formatContractAmountInput = (value, emptyValue = null) => {
|
||||
if (
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
Number(value) === -1 ||
|
||||
String(value) === '-1'
|
||||
) {
|
||||
return emptyValue;
|
||||
}
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number >= 0 ? number.toFixed(2) : emptyValue;
|
||||
};
|
||||
const normalizeProjectValue = value => {
|
||||
if (value === undefined || value === null || value === '' || String(value) === '-1') return '';
|
||||
return value;
|
||||
@@ -1092,8 +1144,9 @@ const defaultForm = () => ({
|
||||
partyB: '',
|
||||
handlerUserName: '',
|
||||
signDate: '',
|
||||
applyDate: '',
|
||||
contractFormat: '',
|
||||
legalSealFlag: 0,
|
||||
legalSealFlag: null,
|
||||
copyCount: '',
|
||||
settlementCurrency: '',
|
||||
invoiceCycle: '',
|
||||
@@ -1109,6 +1162,7 @@ const defaultForm = () => ({
|
||||
effectiveType: '',
|
||||
feeGenerationMode: 'system',
|
||||
archiveStatus: '未归档',
|
||||
mainContent: '',
|
||||
remark: '',
|
||||
});
|
||||
// 复制新增:仅保留可编辑字段与明细,剔除主键、编号、审批与流程信息
|
||||
@@ -1120,6 +1174,7 @@ const createCopiedDetail = (detail = {}) => {
|
||||
return {
|
||||
...form,
|
||||
contractNo: '',
|
||||
applyDate: '',
|
||||
archiveStatus: '未归档',
|
||||
feeGenerationMode: detail.feeGenerationMode || '',
|
||||
billingEnabled: detail.billingEnabled,
|
||||
@@ -1214,13 +1269,12 @@ export default {
|
||||
signType: [{ required: true, message: '请选择签约类型', trigger: 'change' }],
|
||||
settlementCurrency: [{ required: true, message: '请选择结算币种', trigger: 'change' }],
|
||||
projectName: [{ required: true, message: '请选择所属项目', trigger: 'change' }],
|
||||
organizationName: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
|
||||
organizationName: [{ required: true, message: '请选择合同负责部门', trigger: 'change' }],
|
||||
partyA: [{ required: true, message: '请选择甲方', trigger: 'change' }],
|
||||
partyB: [{ required: true, message: '请选择乙方', trigger: 'change' }],
|
||||
signDate: [{ required: true, message: '请选择签订日期', trigger: 'change' }],
|
||||
settlementMode: [{ required: true, message: '请选择结算方式', trigger: 'change' }],
|
||||
contractFormat: [{ required: true, message: '请选择合同格式', trigger: 'change' }],
|
||||
legalSealFlag: [{ required: true, message: '请选择是否需要加盖法人章', trigger: 'change' }],
|
||||
invoiceCycle: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
@@ -1235,6 +1289,22 @@ export default {
|
||||
],
|
||||
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
|
||||
endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }],
|
||||
contractAmount: [
|
||||
{ required: true, message: '请输入合同金额', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return callback(new Error('请输入合同金额'));
|
||||
}
|
||||
if (Number(value) < 0) {
|
||||
return callback(new Error('合同金额不能小于0'));
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
mainContent: [{ max: 1000, message: '合同主要内容不能超过1000个字符', trigger: 'blur' }],
|
||||
},
|
||||
submitAction: '',
|
||||
projectOptions: [],
|
||||
@@ -1301,6 +1371,7 @@ export default {
|
||||
flowBox: false,
|
||||
flowUrl: '',
|
||||
processInstanceId: '',
|
||||
mkEnabled: false,
|
||||
// 页面形态在实例创建时锁定,避免 keep-alive 缓存后随全局 $route 翻转
|
||||
formPageLocked: false,
|
||||
detailPageLocked: false,
|
||||
@@ -1343,8 +1414,8 @@ export default {
|
||||
return this.$route.query.attachmentUpload === '1';
|
||||
},
|
||||
contractAttachmentLocationOptions() {
|
||||
if (this.isAttachmentUploadMode) return ['合同文件'];
|
||||
return ['合同文件', '其它附件'];
|
||||
if (this.isAttachmentUploadMode) return ['用印材料'];
|
||||
return ['用印材料', '附件(佐证材料等)'];
|
||||
},
|
||||
contractAmountDisplay() {
|
||||
const value = this.form.contractAmount;
|
||||
@@ -1411,7 +1482,7 @@ export default {
|
||||
expandTrigger: 'click',
|
||||
};
|
||||
},
|
||||
// 所属组织级联:selectedOrganizationId 存部门 id,级联控件用根到节点的 id 路径,
|
||||
// 合同负责部门级联:selectedOrganizationId 存部门 id,级联控件用根到节点的 id 路径,
|
||||
// 两者在此双向转换;依赖 organizationOptions,树加载完成后回显自动刷新
|
||||
organizationCascaderValue: {
|
||||
get() {
|
||||
@@ -1520,6 +1591,13 @@ export default {
|
||||
this.loadProjectOptions();
|
||||
this.loadSettlementDictionaries();
|
||||
this.loadOrganizationOptions();
|
||||
loadMkEnabled()
|
||||
.then(enabled => {
|
||||
this.mkEnabled = !!enabled;
|
||||
})
|
||||
.catch(() => {
|
||||
this.mkEnabled = false;
|
||||
});
|
||||
}
|
||||
if (this.isFormPage) this.initFormPage();
|
||||
if (this.isDetailPage) this.initDetailPage();
|
||||
@@ -1730,7 +1808,7 @@ export default {
|
||||
copyCount: normalizeOptionalInteger(detail.copyCount, ''),
|
||||
invoiceCycle: normalizeOptionalInteger(detail.invoiceCycle, ''),
|
||||
paymentDays: normalizeOptionalInteger(detail.paymentDays, ''),
|
||||
contractAmount: normalizeOptionalAmount(detail.contractAmount, null),
|
||||
contractAmount: formatContractAmountInput(detail.contractAmount, null),
|
||||
archiveStatus: detail.archiveStatus || '未归档',
|
||||
feeGenerationMode:
|
||||
detail.feeGenerationMode || (Number(detail.billingEnabled) === 0 ? 'manual' : 'system'),
|
||||
@@ -1766,6 +1844,7 @@ export default {
|
||||
this.applyDetail(createCopiedDetail(detail));
|
||||
this.form.handlerUserName =
|
||||
this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || '';
|
||||
this.refreshPreviewContractNo();
|
||||
this.$nextTick(() => this.$refs.contractForm?.clearValidate());
|
||||
},
|
||||
closeForm() {
|
||||
@@ -1780,9 +1859,10 @@ export default {
|
||||
['contractName', '合同名称'],
|
||||
['contractCategory', '合同类型'],
|
||||
['projectName', '所属项目'],
|
||||
['organizationName', '所属组织'],
|
||||
['organizationName', '合同负责部门'],
|
||||
['partyA', '甲方'],
|
||||
['partyB', '乙方'],
|
||||
['contractAmount', '合同金额'],
|
||||
];
|
||||
const empty = required.find(([prop]) => {
|
||||
const value = this.form[prop];
|
||||
@@ -1790,10 +1870,16 @@ export default {
|
||||
});
|
||||
if (empty) {
|
||||
this.$message.warning(
|
||||
empty[0] === 'contractName' ? `请输入${empty[1]}` : `请选择${empty[1]}`
|
||||
['contractName', 'contractAmount'].includes(empty[0])
|
||||
? `请输入${empty[1]}`
|
||||
: `请选择${empty[1]}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (Number(this.form.contractAmount) < 0) {
|
||||
this.$message.warning('合同金额不能小于0');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!this.form.contractName?.trim()) {
|
||||
@@ -1816,7 +1902,7 @@ export default {
|
||||
if (action !== 'formal' && action !== 'edit') return true;
|
||||
const required = [
|
||||
['projectName', '所属项目'],
|
||||
['organizationName', '所属组织'],
|
||||
['organizationName', '合同负责部门'],
|
||||
['partyA', '甲方'],
|
||||
['partyB', '乙方'],
|
||||
['startDate', '合同开始日期'],
|
||||
@@ -1824,10 +1910,20 @@ export default {
|
||||
['signDate', '签订日期'],
|
||||
['settlementMode', '结算方式'],
|
||||
['contractFormat', '合同格式'],
|
||||
['contractAmount', '合同金额'],
|
||||
];
|
||||
const empty = required.find(([prop]) => !this.form[prop]);
|
||||
const empty = required.find(([prop]) => {
|
||||
const value = this.form[prop];
|
||||
return value === undefined || value === null || String(value).trim() === '';
|
||||
});
|
||||
if (empty) {
|
||||
this.$message.warning(`请选择或输入${empty[1]}`);
|
||||
this.$message.warning(
|
||||
empty[0] === 'contractAmount' ? `请输入${empty[1]}` : `请选择或输入${empty[1]}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (Number(this.form.contractAmount) < 0) {
|
||||
this.$message.warning('合同金额不能小于0');
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
@@ -2017,11 +2113,11 @@ export default {
|
||||
},
|
||||
handleListAttachmentUploadToLocation({ location, rows = [] }) {
|
||||
if (!rows.length) return;
|
||||
if (location === '合同文件') {
|
||||
if (isContractSealMaterialLocation(location)) {
|
||||
this.listUploadContractFileRows = [...(this.listUploadContractFileRows || []), ...rows];
|
||||
return;
|
||||
}
|
||||
if (location === '其它附件') {
|
||||
if (isContractSupportingAttachmentLocation(location)) {
|
||||
this.listUploadAttachmentRows = [...(this.listUploadAttachmentRows || []), ...rows];
|
||||
}
|
||||
},
|
||||
@@ -2141,6 +2237,9 @@ export default {
|
||||
column.renderSearch = scope => this.renderOrganizationSearch(scope);
|
||||
}
|
||||
this.syncCurrentOrganizationOption();
|
||||
if (this.isFormPage && this.formMode === 'add' && this.form.organizationId) {
|
||||
this.refreshPreviewContractNo();
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.organizationLoading = false;
|
||||
@@ -2203,6 +2302,29 @@ export default {
|
||||
}
|
||||
: { organizationId: '', organizationName: '' }
|
||||
);
|
||||
if (this.formMode === 'add') {
|
||||
this.refreshPreviewContractNo();
|
||||
}
|
||||
},
|
||||
refreshPreviewContractNo() {
|
||||
if (this.formMode !== 'add') return;
|
||||
const organizationId = this.form.organizationId;
|
||||
if (!organizationId) {
|
||||
this.form.contractNo = '';
|
||||
return;
|
||||
}
|
||||
const requestId = (this.contractNoPreviewRequestId =
|
||||
(this.contractNoPreviewRequestId || 0) + 1);
|
||||
api
|
||||
.nextContractNo(organizationId)
|
||||
.then(res => {
|
||||
if (requestId !== this.contractNoPreviewRequestId) return;
|
||||
this.form.contractNo = res.data?.data || '';
|
||||
})
|
||||
.catch(() => {
|
||||
if (requestId !== this.contractNoPreviewRequestId) return;
|
||||
this.form.contractNo = '';
|
||||
});
|
||||
},
|
||||
syncCurrentOrganizationOption() {
|
||||
if (!this.form.organizationId || !this.form.organizationName) return;
|
||||
@@ -2216,7 +2338,7 @@ export default {
|
||||
this.organizationFlatOptions.unshift(item);
|
||||
},
|
||||
renderOrganizationSearch(scope) {
|
||||
// 搜索区所属组织:单选级联(与新增/编辑表单一致)
|
||||
// 搜索区合同负责部门:单选级联(与新增/编辑表单一致)
|
||||
// 级联返回 id 路径数组,提交时由 normalizeSearch 取末级 id 转组织名称
|
||||
return h(ElCascader, {
|
||||
modelValue: Array.isArray(scope.row?.organizationName) ? scope.row.organizationName : [],
|
||||
@@ -2235,7 +2357,7 @@ export default {
|
||||
filterable: true,
|
||||
clearable: true,
|
||||
style: 'width: 100%',
|
||||
placeholder: '请选择 所属组织',
|
||||
placeholder: '请选择 合同负责部门',
|
||||
});
|
||||
},
|
||||
integerInput(prop, value) {
|
||||
@@ -2248,6 +2370,9 @@ export default {
|
||||
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
|
||||
this.form.contractAmount = normalized === '' ? null : normalized;
|
||||
},
|
||||
formatContractAmountField() {
|
||||
this.form.contractAmount = formatContractAmountInput(this.form.contractAmount, null);
|
||||
},
|
||||
positiveIntegerInput(prop, value) {
|
||||
const normalized = String(value || '').replace(/\D/g, '').replace(/^0+/, '');
|
||||
this.form[prop] = normalized;
|
||||
@@ -2280,11 +2405,11 @@ export default {
|
||||
},
|
||||
handleAttachmentUploadToLocation({ location, rows = [] }) {
|
||||
if (!rows.length) return;
|
||||
if (location === '合同文件') {
|
||||
if (isContractSealMaterialLocation(location)) {
|
||||
this.contractFileRows = [...(this.contractFileRows || []), ...rows];
|
||||
return;
|
||||
}
|
||||
if (location === '其它附件') {
|
||||
if (isContractSupportingAttachmentLocation(location)) {
|
||||
this.attachmentRows = [...(this.attachmentRows || []), ...rows];
|
||||
}
|
||||
},
|
||||
@@ -2645,15 +2770,16 @@ export default {
|
||||
templateFlag: '是否范本',
|
||||
originalContractNo: '原件合同编号',
|
||||
electronicSealFlag: '是否电子章',
|
||||
remark: '备注',
|
||||
mainContent: '合同主要内容',
|
||||
remark: '其他事项说明',
|
||||
feeGenerationMode: '费用生成模式',
|
||||
billingPlanJson: '计费方案',
|
||||
settlementRuleJson: '结算单规则',
|
||||
preSettlementConfigJson: '预结算配置',
|
||||
formalSettlementConfigJson: '正式结算配置',
|
||||
paymentRatioJson: '付款比例设置',
|
||||
contractFileJson: '合同文件',
|
||||
attachmentsJson: '其它附件',
|
||||
contractFileJson: '用印材料',
|
||||
attachmentsJson: '附件(佐证材料等)',
|
||||
changeAttachmentsJson: '变更材料',
|
||||
};
|
||||
return labels[field] || field;
|
||||
@@ -2820,13 +2946,8 @@ export default {
|
||||
detailValue(prop) {
|
||||
const value = this.detailRow[prop];
|
||||
if (prop === 'contractAmount') {
|
||||
return value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
Number(value) === -1 ||
|
||||
String(value) === '-1'
|
||||
? '-'
|
||||
: value;
|
||||
const amountText = formatContractAmountInput(value, '');
|
||||
return amountText === '' ? '-' : amountText;
|
||||
}
|
||||
if (
|
||||
prop === 'contractStage' ||
|
||||
@@ -2858,6 +2979,10 @@ export default {
|
||||
return;
|
||||
}
|
||||
if (operation.action === 'flow') {
|
||||
if (this.mkEnabled) {
|
||||
this.handleMkAudit(row);
|
||||
return;
|
||||
}
|
||||
this.openFlow(row);
|
||||
return;
|
||||
}
|
||||
@@ -2911,6 +3036,24 @@ export default {
|
||||
query: { mode: 'add', copyId: row.id, name: '新增合同管理' },
|
||||
});
|
||||
},
|
||||
handleMkAudit(row) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('合同ID为空,无法打开审核页');
|
||||
return;
|
||||
}
|
||||
getMKApprovalUrl({ bizId: String(row.id) })
|
||||
.then(res => {
|
||||
const url = res?.data?.data;
|
||||
if (!url) {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
});
|
||||
},
|
||||
openFlow(row, loaded = false) {
|
||||
const processInstanceId = row.processInstanceId || row.processInstanceID || row.procInstId;
|
||||
if (!processInstanceId && !loaded && row.id) {
|
||||
@@ -3117,7 +3260,11 @@ export default {
|
||||
|
||||
&__remark {
|
||||
width: 100%;
|
||||
margin-bottom: 0 !important;
|
||||
margin-bottom: 24px !important;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__date-range {
|
||||
@@ -3191,7 +3338,7 @@ export default {
|
||||
|
||||
:deep(.el-form-item) {
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<basic-container class="loading-manage-page">
|
||||
<div v-if="!isStandalonePage" class="loading-manage-page__search">
|
||||
<div v-if="!isStandalonePage && searchVisible" class="loading-manage-page__search">
|
||||
<el-form :model="searchForm" label-position="right" label-width="88px">
|
||||
<div class="loading-manage-page__search-grid">
|
||||
<el-form-item label="配载单号">
|
||||
@@ -228,17 +228,13 @@
|
||||
<el-button type="primary" plain @click="handleExport">批量导出</el-button>
|
||||
<el-button type="primary" plain @click="handleBatchComplete">批量完成</el-button>
|
||||
</div>
|
||||
<div class="loading-manage-page__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button :icon="Refresh" text @click="loadTable" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="表格密度" placement="top">
|
||||
<el-button :icon="Rank" text @click="toggleTableSize" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="列设置" placement="top">
|
||||
<el-button :icon="Setting" text @click="handleColumnSetting" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<table-header-tools
|
||||
:columns="columnSettingOptions"
|
||||
v-model:visible-column-keys="visibleColumnKeys"
|
||||
v-model:table-size="tableSize"
|
||||
v-model:search-visible="searchVisible"
|
||||
@refresh="loadTable"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
@@ -254,7 +250,12 @@
|
||||
>
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="58" fixed="left" align="center" />
|
||||
<el-table-column label="配载单号" prop="loadingNo" min-width="170">
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('loadingNo')"
|
||||
label="配载单号"
|
||||
prop="loadingNo"
|
||||
min-width="170"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
type="primary"
|
||||
@@ -264,7 +265,12 @@
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运单号" prop="loadingSubNos" min-width="220">
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('loadingSubNos')"
|
||||
label="运单号"
|
||||
prop="loadingSubNos"
|
||||
min-width="220"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="loading-manage-page__sub-nos">
|
||||
<template v-if="splitText(row.loadingSubNos).length">
|
||||
@@ -283,11 +289,40 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="车牌号" prop="vehicleNo" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column label="司机" prop="driverName" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="联系电话" prop="driverPhone" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="承运类型" prop="carrierType" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="发货地" prop="departureAddress" min-width="180">
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('vehicleNo')"
|
||||
label="车牌号"
|
||||
prop="vehicleNo"
|
||||
min-width="170"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('driverName')"
|
||||
label="司机"
|
||||
prop="driverName"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('driverPhone')"
|
||||
label="联系电话"
|
||||
prop="driverPhone"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('carrierType')"
|
||||
label="承运类型"
|
||||
prop="carrierType"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('departureAddress')"
|
||||
label="发货地"
|
||||
prop="departureAddress"
|
||||
min-width="180"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tooltip
|
||||
v-if="formatFullRouteAddress(row, 'departure')"
|
||||
@@ -304,7 +339,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="途经地" prop="transitAddress" min-width="190">
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('transitAddress')"
|
||||
label="途经地"
|
||||
prop="transitAddress"
|
||||
min-width="190"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tooltip
|
||||
v-if="formatFullRouteAddress(row, 'transit')"
|
||||
@@ -321,7 +361,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="到货地" prop="arrivalAddress" min-width="180">
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('arrivalAddress')"
|
||||
label="到货地"
|
||||
prop="arrivalAddress"
|
||||
min-width="180"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tooltip
|
||||
v-if="formatFullRouteAddress(row, 'arrival')"
|
||||
@@ -338,20 +383,40 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="承运商" prop="carrierName" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="运输方式" prop="transportType" min-width="150" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('carrierName')"
|
||||
label="承运商"
|
||||
prop="carrierName"
|
||||
min-width="160"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('transportType')"
|
||||
label="运输方式"
|
||||
prop="transportType"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ transportTypeLabel(row.transportType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数据来源" prop="dataSource" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('dataSource')"
|
||||
label="数据来源"
|
||||
prop="dataSource"
|
||||
min-width="130"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('createUserName')"
|
||||
label="创建人"
|
||||
prop="createUserName"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('createTime')"
|
||||
label="创建时间"
|
||||
prop="createTime"
|
||||
min-width="170"
|
||||
@@ -359,6 +424,7 @@
|
||||
sortable
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('updateTime')"
|
||||
label="更新时间"
|
||||
prop="updateTime"
|
||||
min-width="170"
|
||||
@@ -366,12 +432,20 @@
|
||||
sortable
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('currentProcessNode')"
|
||||
label="过程节点"
|
||||
prop="currentProcessNode"
|
||||
min-width="130"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="状态" prop="businessStatus" width="110" fixed="right" align="center">
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('businessStatus')"
|
||||
label="状态"
|
||||
prop="businessStatus"
|
||||
width="110"
|
||||
fixed="right"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span :class="['loading-manage-page__status', `is-${row.businessStatus || 'unknown'}`]">
|
||||
{{ statusText(row) }}
|
||||
@@ -484,12 +558,16 @@
|
||||
<div class="loading-detail__field loading-detail__attachment-field">
|
||||
<span class="loading-detail__label">附件</span>
|
||||
<span class="loading-detail__value"
|
||||
><el-link
|
||||
><span
|
||||
v-for="file in detailAttachments"
|
||||
:key="file.url || file.name"
|
||||
type="primary"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(file)"
|
||||
>{{ file.name }}</el-link
|
||||
@keydown.enter="previewAttachment(file)"
|
||||
@keydown.space.prevent="previewAttachment(file)"
|
||||
>{{ file.name }}</span
|
||||
><span v-if="!detailAttachments.length">-</span></span
|
||||
>
|
||||
</div>
|
||||
@@ -555,15 +633,12 @@
|
||||
prop="packageType"
|
||||
label="包装"
|
||||
min-width="100" /><el-table-column
|
||||
prop="weight"
|
||||
label="重量(吨)"
|
||||
min-width="110" /><el-table-column
|
||||
prop="volume"
|
||||
label="体积(方)"
|
||||
min-width="110" /><el-table-column
|
||||
prop="quantity"
|
||||
label="数量"
|
||||
min-width="90" /><el-table-column
|
||||
prop="quantityUnit"
|
||||
label="数量单位"
|
||||
min-width="100" /><el-table-column
|
||||
prop="materialCode"
|
||||
label="物料编码"
|
||||
min-width="120" /><el-table-column
|
||||
@@ -665,13 +740,6 @@
|
||||
record.statusName || (record.punched ? '已打卡' : '未打卡')
|
||||
}}</el-tag
|
||||
>
|
||||
<el-tag
|
||||
v-if="record.type === 'enroute'"
|
||||
size="small"
|
||||
type="info"
|
||||
effect="plain"
|
||||
>在途</el-tag
|
||||
>
|
||||
<el-tag
|
||||
v-if="record.exceptionFlag"
|
||||
size="small"
|
||||
@@ -701,22 +769,12 @@
|
||||
>
|
||||
备注:{{ record.remark }}
|
||||
</div>
|
||||
<div
|
||||
<punch-record-photos
|
||||
v-if="record.photos && record.photos.length"
|
||||
class="loading-detail__punch-record-photos"
|
||||
>
|
||||
<div
|
||||
v-for="(photo, pi) in record.photos"
|
||||
:key="`${record.nodeCode || index}-${pi}`"
|
||||
class="loading-detail__driver-upload-item"
|
||||
@click="previewDriverUploadPhoto(record.photos, pi)"
|
||||
>
|
||||
<el-image :src="photo.url" fit="cover" lazy />
|
||||
<div class="loading-detail__driver-upload-label">
|
||||
{{ photo.label || `${record.nodeName}-凭证` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
:photos="record.photos"
|
||||
lazy
|
||||
@preview="previewDriverUploadPhoto(record.photos, $event)"
|
||||
/>
|
||||
</template>
|
||||
<div v-else class="loading-detail__punch-record-line">暂未打卡</div>
|
||||
</div>
|
||||
@@ -1066,6 +1124,7 @@
|
||||
label="运单号"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.waybillNo" type="primary" @click="openWaybillDetail(row)">
|
||||
@@ -1357,7 +1416,7 @@
|
||||
<el-table-column prop="cargoType" label="货物类型" min-width="140" />
|
||||
<el-table-column prop="packageType" label="包装" min-width="100" />
|
||||
<el-table-column prop="quantity" label="数量" min-width="100" />
|
||||
<el-table-column prop="quantityUnit" label="单位" min-width="100" />
|
||||
<el-table-column prop="quantityUnit" label="数量单位" min-width="100" />
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
|
||||
<el-table-column prop="equipmentCode" label="设备编码" min-width="130" />
|
||||
<el-table-column prop="brand" label="品牌" min-width="120" />
|
||||
@@ -1400,7 +1459,11 @@
|
||||
<el-option
|
||||
v-for="item in taskCarrierOptions"
|
||||
:key="item.id || item.carrierName"
|
||||
:label="item.carrierName || item.partyB"
|
||||
:label="
|
||||
dialogForm.carrierType === '承运商'
|
||||
? formatTaskCarrierContractLabel(item)
|
||||
: item.carrierName || item.partyB
|
||||
"
|
||||
:value="
|
||||
dialogForm.carrierType === '承运商'
|
||||
? item.id
|
||||
@@ -1622,9 +1685,7 @@
|
||||
label="变更人"
|
||||
min-width="120" /><el-table-column prop="content" label="变更内容" min-width="360"
|
||||
><template #default="{ row }"
|
||||
><div class="loading-route-change-dialog__record-content">
|
||||
{{ row.content || '-' }}
|
||||
</div></template
|
||||
><div class="loading-route-change-dialog__record-content">{{ formatRouteChangeContent(row.content) }}</div></template
|
||||
></el-table-column
|
||||
><el-table-column
|
||||
prop="remark"
|
||||
@@ -1716,12 +1777,14 @@ import { getParentOptions as getCargoTypeOptions } from '@/api/base/cargo-type';
|
||||
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
|
||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import {
|
||||
getList as getProcessConfigList,
|
||||
getDetail as getProcessConfigDetail,
|
||||
getVoucherImages as getProcessConfigVoucherImages,
|
||||
} from '@/api/business/process-config';
|
||||
import VoucherExecutionDetail from './components/voucher-execution-detail.vue';
|
||||
import PunchRecordPhotos from './components/punch-record-photos.vue';
|
||||
import {
|
||||
carrierTypeOptions,
|
||||
loadingManageConfig,
|
||||
@@ -1730,10 +1793,12 @@ import {
|
||||
} from '@/option/business/loading-manage';
|
||||
import { option as waybillOption } from '@/option/business/waybill-manage';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Location, Rank, Refresh, Search, Setting } from '@element-plus/icons-vue';
|
||||
import { Location, Rank, Search } from '@element-plus/icons-vue';
|
||||
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { formatRouteChangeContent } from '@/utils/route-change';
|
||||
|
||||
const defaultTransportType = '';
|
||||
const defaultCandidateTransportType = '';
|
||||
@@ -1927,21 +1992,44 @@ const uniqueText = (rows, prop) =>
|
||||
),
|
||||
].join('、');
|
||||
|
||||
const LOADING_TABLE_COLUMNS = [
|
||||
{ prop: 'loadingNo', label: '配载单号' },
|
||||
{ prop: 'loadingSubNos', label: '运单号' },
|
||||
{ prop: 'vehicleNo', label: '车牌号' },
|
||||
{ prop: 'driverName', label: '司机' },
|
||||
{ prop: 'driverPhone', label: '联系电话' },
|
||||
{ prop: 'carrierType', label: '承运类型' },
|
||||
{ prop: 'departureAddress', label: '发货地' },
|
||||
{ prop: 'transitAddress', label: '途经地' },
|
||||
{ prop: 'arrivalAddress', label: '到货地' },
|
||||
{ prop: 'carrierName', label: '承运商' },
|
||||
{ prop: 'transportType', label: '运输方式' },
|
||||
{ prop: 'dataSource', label: '数据来源' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
{ prop: 'updateTime', label: '更新时间' },
|
||||
{ prop: 'currentProcessNode', label: '过程节点' },
|
||||
{ prop: 'businessStatus', label: '状态' },
|
||||
];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Rank,
|
||||
Search,
|
||||
TableHeaderTools,
|
||||
VoucherExecutionDetail,
|
||||
PunchRecordPhotos,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
Rank,
|
||||
Location,
|
||||
Refresh,
|
||||
Search,
|
||||
Setting,
|
||||
loading: false,
|
||||
tableSize: 'default',
|
||||
searchVisible: true,
|
||||
columnSettingOptions: LOADING_TABLE_COLUMNS,
|
||||
visibleColumnKeys: LOADING_TABLE_COLUMNS.map(column => column.prop),
|
||||
tableData: [],
|
||||
page: {
|
||||
currentPage: 1,
|
||||
@@ -2189,6 +2277,15 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatRouteChangeContent,
|
||||
formatTaskCarrierContractLabel(item = {}) {
|
||||
const carrierName = item.carrierName || item.partyB || '';
|
||||
const contractLabel = formatContractOptionLabel(item);
|
||||
if (carrierName && contractLabel) {
|
||||
return `${carrierName}(${contractLabel})`;
|
||||
}
|
||||
return carrierName || contractLabel;
|
||||
},
|
||||
syncStandaloneTagTitle() {
|
||||
if (!this.isStandalonePage) return;
|
||||
const title = this.isStandaloneDetailPage ? this.detailPageTitle : this.formPageDefaultTitle;
|
||||
@@ -2248,13 +2345,8 @@ export default {
|
||||
selectionChange(rows) {
|
||||
this.selectionRows = rows || [];
|
||||
},
|
||||
toggleTableSize() {
|
||||
const order = ['default', 'small', 'large'];
|
||||
const index = order.indexOf(this.tableSize);
|
||||
this.tableSize = order[(index + 1) % order.length];
|
||||
},
|
||||
handleColumnSetting() {
|
||||
ElMessage.info('列设置按原型保留入口,当前默认展示全部列表字段');
|
||||
isColumnVisible(prop) {
|
||||
return this.visibleColumnKeys.includes(prop);
|
||||
},
|
||||
statusText(row) {
|
||||
return row.businessStatusName || loadingStatusMap[row.businessStatus] || '未知';
|
||||
@@ -2757,7 +2849,6 @@ export default {
|
||||
if (status === 'draft') {
|
||||
return [
|
||||
{ type: 'edit', label: '编辑' },
|
||||
{ type: 'copy', label: '复制' },
|
||||
{ type: 'delete', label: '删除' },
|
||||
];
|
||||
}
|
||||
@@ -2769,36 +2860,24 @@ export default {
|
||||
if (row.driverRejected) {
|
||||
actions.push({ type: 'reassign', label: '重新派单' });
|
||||
}
|
||||
actions.push(
|
||||
{ type: 'edit', label: '编辑' },
|
||||
{ type: 'copy', label: '复制' },
|
||||
{ type: 'cancel', label: '取消' }
|
||||
);
|
||||
actions.push({ type: 'edit', label: '编辑' }, { type: 'cancel', label: '取消' });
|
||||
return actions;
|
||||
}
|
||||
if (status === 'running') {
|
||||
return [
|
||||
{ type: 'view', label: '详情' },
|
||||
{ type: 'complete', label: '完成' },
|
||||
{ type: 'copy', label: '复制' },
|
||||
];
|
||||
}
|
||||
if (status === 'completed' || status === 'cancelled') {
|
||||
return [
|
||||
{ type: 'view', label: '详情' },
|
||||
{ type: 'copy', label: '复制' },
|
||||
];
|
||||
return [{ type: 'view', label: '详情' }];
|
||||
}
|
||||
return [
|
||||
{ type: 'view', label: '详情' },
|
||||
{ type: 'copy', label: '复制' },
|
||||
];
|
||||
return [{ type: 'view', label: '详情' }];
|
||||
},
|
||||
handleRowAction(type, row) {
|
||||
const actionMap = {
|
||||
view: () => this.gotoLoadingDetail(row),
|
||||
edit: () => this.openLoadingDialog('edit', row),
|
||||
copy: () => this.gotoLoadingCopy(row),
|
||||
reassign: () => this.gotoLoadingReassign(row),
|
||||
delete: () => this.deleteLoading(row),
|
||||
start: () => this.startLoading(row),
|
||||
@@ -4473,8 +4552,7 @@ export default {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.loading-manage-page__toolbar-left,
|
||||
.loading-manage-page__toolbar-right {
|
||||
.loading-manage-page__toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
@@ -4733,7 +4811,7 @@ export default {
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
@@ -4928,7 +5006,6 @@ export default {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
.loading-detail__punch-record-photos,
|
||||
.loading-detail__driver-upload-grid,
|
||||
.loading-detail__voucher-image-grid {
|
||||
display: grid;
|
||||
@@ -5052,8 +5129,9 @@ export default {
|
||||
gap: 4px;
|
||||
}
|
||||
.loading-route-change-dialog__record-content {
|
||||
white-space: normal;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<basic-container class="master-order-page">
|
||||
<template v-if="mode === 'list'">
|
||||
<div class="master-order-page__search">
|
||||
<div v-show="searchVisible" class="master-order-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="88px" @submit.prevent>
|
||||
<div class="master-order-page__search-grid">
|
||||
<el-form-item v-for="field in primaryFields" :key="field.prop" :label="field.label">
|
||||
@@ -55,8 +55,16 @@
|
||||
</div>
|
||||
<section class="master-list-panel">
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="goCreate()">新建多联总单</el-button
|
||||
><el-button type="primary" plain @click="download">批量导出</el-button>
|
||||
<div class="toolbar-left">
|
||||
<el-button type="primary" @click="goCreate()">新建多联总单</el-button>
|
||||
<el-button type="primary" plain @click="download">批量导出</el-button>
|
||||
</div>
|
||||
<table-header-tools
|
||||
:show-column="false"
|
||||
:show-grid="false"
|
||||
v-model:search-visible="searchVisible"
|
||||
@refresh="load"
|
||||
/>
|
||||
</div>
|
||||
<div v-loading="loading" class="master-cards">
|
||||
<article v-for="row in records" :key="row.id" class="master-card">
|
||||
@@ -147,12 +155,13 @@
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/business/master-order';
|
||||
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||||
import MasterOrderEditor from './components/master-order-editor.vue';
|
||||
import MasterOrderDispatch from './components/master-order-dispatch.vue';
|
||||
import MasterOrderDetail from './components/master-order-detail.vue';
|
||||
|
||||
export default {
|
||||
components: { MasterOrderEditor, MasterOrderDispatch, MasterOrderDetail },
|
||||
components: { TableHeaderTools, MasterOrderEditor, MasterOrderDispatch, MasterOrderDetail },
|
||||
data() {
|
||||
return {
|
||||
// 实例创建时锁定所属路由。keep-alive 缓存后 $route 会变成其它页面,
|
||||
@@ -160,6 +169,7 @@ export default {
|
||||
//(mode=add 且无 id)会落入详情分支并请求缺少 id 的详情接口。
|
||||
routePathLocked: this.$route.path,
|
||||
searchExpanded: false,
|
||||
searchVisible: true,
|
||||
loading: false,
|
||||
records: [],
|
||||
query: { businessStatus: '' },
|
||||
@@ -633,8 +643,18 @@ export default {
|
||||
}
|
||||
.master-list-panel {
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
.master-card {
|
||||
display: flex;
|
||||
|
||||
@@ -429,7 +429,7 @@ const NODE_TEMPLATES = [
|
||||
type: 'confirm',
|
||||
supportVoucher: false,
|
||||
},
|
||||
{ key: 'arrive_scene', name: '到场', type: 'punch', enabled: false },
|
||||
{ key: 'arrive_scene', name: '到场', type: 'punch', enabled: false, supportCargo: false },
|
||||
{
|
||||
key: 'load',
|
||||
name: '装货',
|
||||
@@ -485,6 +485,7 @@ const NODE_TEMPLATES = [
|
||||
key: 'return',
|
||||
name: '回单',
|
||||
type: 'confirm',
|
||||
supportCargo: false,
|
||||
supportVoucher: true,
|
||||
voucherOptions: ['磅单', '签收单'],
|
||||
},
|
||||
@@ -565,7 +566,13 @@ export default {
|
||||
});
|
||||
},
|
||||
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] || ''
|
||||
);
|
||||
},
|
||||
applyDefaultDeptSearch() {
|
||||
const deptId = this.defaultDeptId();
|
||||
@@ -580,15 +587,32 @@ export default {
|
||||
});
|
||||
},
|
||||
flattenDept(tree, level = 0) {
|
||||
return tree.flatMap(item => [
|
||||
{
|
||||
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
||||
value: item.id,
|
||||
},
|
||||
...(item.children && item.children.length
|
||||
? this.flattenDept(item.children, level + 1)
|
||||
: []),
|
||||
]);
|
||||
return tree.flatMap(item => {
|
||||
const id =
|
||||
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
|
||||
if (!id) return [];
|
||||
return [
|
||||
{
|
||||
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
||||
value: id,
|
||||
},
|
||||
...(item.children && item.children.length
|
||||
? this.flattenDept(item.children, level + 1)
|
||||
: []),
|
||||
];
|
||||
});
|
||||
},
|
||||
normalizeListQuery(params = {}) {
|
||||
const query = { ...params, ...this.query };
|
||||
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;
|
||||
},
|
||||
displayStatus(status) {
|
||||
const statusMap = {
|
||||
@@ -631,7 +655,9 @@ export default {
|
||||
enabled: false,
|
||||
punch: true,
|
||||
location: true,
|
||||
uploadCargo: true,
|
||||
// 不支持货量的节点(到场/回单/接单等)不得默认勾上,否则会写出
|
||||
// 「上传货量为是、货量类型为空」的配置,而这类节点在界面上没有货量开关可关
|
||||
uploadCargo: !!template?.supportCargo,
|
||||
cargoTypes: [],
|
||||
uploadVoucher: undefined,
|
||||
voucherTypes: [],
|
||||
@@ -646,6 +672,10 @@ export default {
|
||||
confirmInternal: false,
|
||||
...template,
|
||||
};
|
||||
if (!node.supportCargo) {
|
||||
node.uploadCargo = false;
|
||||
node.cargoTypes = [];
|
||||
}
|
||||
if ((node.key === 'accept' || node.name === '接单') && node.confirmMode === 'yes') {
|
||||
node.confirmDriver = true;
|
||||
} else if ((node.key === 'accept' || node.name === '接单') && node.confirmMode !== 'yes') {
|
||||
@@ -918,6 +948,15 @@ export default {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 勾了上传货量却没选类型,在小程序侧等同于不采集货量;此处直接拦住,
|
||||
// 避免保存出「显示采集、实际不采集」的配置
|
||||
const cargoWithoutTypes = this.nodeRows.find(
|
||||
item => item.enabled && item.supportCargo && item.uploadCargo && !(item.cargoTypes || []).length
|
||||
);
|
||||
if (cargoWithoutTypes) {
|
||||
this.$message.warning(`「${cargoWithoutTypes.name}」已选择上传货量,请至少勾选一个货量类型`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
needsDuplicateVoucherConfirm() {
|
||||
@@ -1074,9 +1113,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) {
|
||||
@@ -1099,12 +1138,10 @@ export default {
|
||||
},
|
||||
onLoad(page, params = {}) {
|
||||
this.loading = true;
|
||||
const query = this.normalizeListQuery(params);
|
||||
this.query = query;
|
||||
api
|
||||
.getList(page.currentPage, page.pageSize, {
|
||||
...params,
|
||||
...this.query,
|
||||
allDept: this.isAdmin ? 1 : 0,
|
||||
})
|
||||
.getList(page.currentPage, page.pageSize, query)
|
||||
.then(res => {
|
||||
const result = res.data.data || {};
|
||||
this.page.total = result.total || 0;
|
||||
@@ -1117,8 +1154,7 @@ export default {
|
||||
},
|
||||
buildExportParams() {
|
||||
return {
|
||||
...this.query,
|
||||
allDept: this.isAdmin ? 1 : 0,
|
||||
...this.normalizeListQuery(this.query),
|
||||
ids: this.ids,
|
||||
[this.website.tokenHeader]: getToken(),
|
||||
};
|
||||
|
||||
@@ -231,6 +231,9 @@
|
||||
<el-select
|
||||
v-model="form.undertakeDeptId"
|
||||
placeholder="请选择"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
clearable
|
||||
filterable
|
||||
:disabled="isBasicInfoReadonly"
|
||||
@@ -573,7 +576,7 @@
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入"
|
||||
maxlength="200"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -583,7 +586,7 @@
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入"
|
||||
maxlength="200"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -593,7 +596,7 @@
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入"
|
||||
maxlength="200"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -603,7 +606,7 @@
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入"
|
||||
maxlength="200"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -645,11 +648,22 @@
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文件名" min-width="220" align="center" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="220"
|
||||
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="240" align="center">
|
||||
@@ -846,15 +860,15 @@
|
||||
v-model="userQuery.realName"
|
||||
placeholder="用户姓名"
|
||||
clearable
|
||||
@keyup.enter="loadUsers"
|
||||
@keyup.enter="searchUsers"
|
||||
/>
|
||||
<el-input
|
||||
v-model="userQuery.phone"
|
||||
placeholder="手机号"
|
||||
clearable
|
||||
@keyup.enter="loadUsers"
|
||||
@keyup.enter="searchUsers"
|
||||
/>
|
||||
<el-button type="primary" @click="loadUsers">查询</el-button>
|
||||
<el-button type="primary" @click="searchUsers">查询</el-button>
|
||||
<el-button @click="resetUserQuery">重置</el-button>
|
||||
</div>
|
||||
<el-table :data="userData" border v-loading="userLoading" @row-dblclick="selectUser">
|
||||
@@ -945,7 +959,7 @@ const emptyForm = () => ({
|
||||
projectShortName: '',
|
||||
businessDeptId: '',
|
||||
businessDeptName: '',
|
||||
undertakeDeptId: '',
|
||||
undertakeDeptId: [],
|
||||
undertakeDeptName: '',
|
||||
projectSource: '',
|
||||
sourceRemark: '',
|
||||
@@ -1135,7 +1149,15 @@ export default {
|
||||
{ max: 20, message: '项目简称不能超过20个字符', trigger: 'blur' },
|
||||
],
|
||||
businessDeptId: [{ required: true, message: '请选择业务部门', trigger: 'change' }],
|
||||
undertakeDeptId: [{ required: true, message: '请选择平台公司', trigger: 'change' }],
|
||||
undertakeDeptId: [
|
||||
{
|
||||
type: 'array',
|
||||
required: true,
|
||||
min: 1,
|
||||
message: '请选择平台公司',
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
businessMode: [{ required: true, message: '请选择业务模式', trigger: 'change' }],
|
||||
customerNames: [{ required: true, message: '请选择客户名称', trigger: 'change' }],
|
||||
carrierNames: [{ required: true, message: '请选择下游承运商', trigger: 'change' }],
|
||||
@@ -1188,6 +1210,18 @@ export default {
|
||||
changeReason: [
|
||||
{ max: 2000, message: '变更原因不能超过2000个字符', trigger: 'blur' },
|
||||
],
|
||||
projectIntro: [
|
||||
{ max: 1000, message: '项目简介不能超过1000个字符', trigger: 'blur' },
|
||||
],
|
||||
profitRemark: [
|
||||
{ max: 1000, message: '盈利模式及利润测算情况说明不能超过1000个字符', trigger: 'blur' },
|
||||
],
|
||||
riskPoint: [
|
||||
{ max: 1000, message: '项目风险点不能超过1000个字符', trigger: 'blur' },
|
||||
],
|
||||
emergencyPlan: [
|
||||
{ max: 1000, message: '风险防范措施及应急预案不能超过1000个字符', trigger: 'blur' },
|
||||
],
|
||||
},
|
||||
deptOptions: [],
|
||||
businessDeptTreeOptions: [],
|
||||
@@ -1485,13 +1519,13 @@ export default {
|
||||
this.businessDeptTreeOptions = [{ label, value: detail.businessDeptId }];
|
||||
this.deptOptions = [{ label, rawLabel: label, value: detail.businessDeptId }];
|
||||
}
|
||||
if (detail.undertakeDeptId) {
|
||||
this.platformCompanyOptions = [
|
||||
{
|
||||
label: detail.undertakeDeptName || String(detail.undertakeDeptId),
|
||||
value: detail.undertakeDeptId,
|
||||
},
|
||||
];
|
||||
const undertakeDeptIds = this.parseUndertakeDeptIds(detail.undertakeDeptId);
|
||||
const undertakeDeptNames = this.parseUndertakeDeptNames(detail.undertakeDeptName);
|
||||
if (undertakeDeptIds.length) {
|
||||
this.platformCompanyOptions = undertakeDeptIds.map((id, index) => ({
|
||||
label: undertakeDeptNames[index] || String(id),
|
||||
value: id,
|
||||
}));
|
||||
}
|
||||
},
|
||||
resolvePublicDictOptions(options, currentValue) {
|
||||
@@ -1837,6 +1871,8 @@ export default {
|
||||
this.form = {
|
||||
...form,
|
||||
...situation,
|
||||
undertakeDeptId: this.parseUndertakeDeptIds(form.undertakeDeptId),
|
||||
undertakeDeptName: form.undertakeDeptName || '',
|
||||
customerNames: this.getCustomerRowNames(customerRows, 'customer') || form.customerNames,
|
||||
carrierNames: this.getCustomerRowNames(carrierRows, 'carrier') || form.carrierNames,
|
||||
};
|
||||
@@ -2003,8 +2039,13 @@ export default {
|
||||
});
|
||||
},
|
||||
normalizeSubmitForm(options = {}) {
|
||||
const undertakeDeptIds = this.parseUndertakeDeptIds(this.form.undertakeDeptId);
|
||||
const undertakeDeptName =
|
||||
this.resolveUndertakeDeptNames(undertakeDeptIds) || this.form.undertakeDeptName || '';
|
||||
const submitRow = {
|
||||
...this.form,
|
||||
undertakeDeptId: undertakeDeptIds.join(','),
|
||||
undertakeDeptName,
|
||||
fundLimit: this.formatAmount(this.form.fundLimit),
|
||||
receivableLimit: this.formatAmount(this.form.receivableLimit),
|
||||
...Object.fromEntries(
|
||||
@@ -2396,11 +2437,40 @@ export default {
|
||||
this.$refs.projectForm?.validateField('businessDeptId');
|
||||
},
|
||||
handleUndertakeDeptChange(value) {
|
||||
const id = Array.isArray(value) ? value.at(-1) : value;
|
||||
const dept = this.platformCompanyOptions.find(item => String(item.value) === String(id));
|
||||
this.form.undertakeDeptName = dept ? dept.label : '';
|
||||
const ids = this.parseUndertakeDeptIds(value);
|
||||
this.form.undertakeDeptId = ids;
|
||||
this.form.undertakeDeptName = this.resolveUndertakeDeptNames(ids);
|
||||
this.$refs.projectForm?.validateField('undertakeDeptId');
|
||||
},
|
||||
parseUndertakeDeptIds(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map(item => String(item ?? '').trim())
|
||||
.filter(item => item && item !== '-1');
|
||||
}
|
||||
return String(value ?? '')
|
||||
.split(/[,,]/)
|
||||
.map(item => item.trim())
|
||||
.filter(item => item && item !== '-1');
|
||||
},
|
||||
parseUndertakeDeptNames(value) {
|
||||
return String(value ?? '')
|
||||
.split(/[,,、]/)
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
},
|
||||
resolveUndertakeDeptNames(ids = []) {
|
||||
return ids
|
||||
.map(id => {
|
||||
const dept = this.platformCompanyOptions.find(item => String(item.value) === String(id));
|
||||
return dept?.label || '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('、');
|
||||
},
|
||||
firstUndertakeDeptId(value) {
|
||||
return this.parseUndertakeDeptIds(value)[0] || '';
|
||||
},
|
||||
// 由部门 id 反查「根 → 该节点」的 id 路径,供级联回显(找不到返回 null)
|
||||
findDeptPath(id, tree = [], parents = []) {
|
||||
const target = Array.isArray(id) ? id.at(-1) : id;
|
||||
@@ -2439,15 +2509,23 @@ export default {
|
||||
});
|
||||
},
|
||||
ensureCurrentPlatformOption() {
|
||||
const id = this.form.undertakeDeptId;
|
||||
if (!id) return;
|
||||
const exists = this.platformCompanyOptions.some(item => String(item.value) === String(id));
|
||||
if (!exists) {
|
||||
this.platformCompanyOptions.unshift({
|
||||
label: this.form.undertakeDeptName || String(id),
|
||||
value: id,
|
||||
});
|
||||
}
|
||||
const ids = this.parseUndertakeDeptIds(this.form.undertakeDeptId);
|
||||
if (!ids.length) return;
|
||||
const names = this.parseUndertakeDeptNames(this.form.undertakeDeptName);
|
||||
ids.forEach((id, index) => {
|
||||
const exists = this.platformCompanyOptions.some(item => String(item.value) === String(id));
|
||||
if (!exists) {
|
||||
this.platformCompanyOptions.unshift({
|
||||
label: names[index] || String(id),
|
||||
value: id,
|
||||
});
|
||||
}
|
||||
});
|
||||
// 选项 value 与已选 ID 类型对齐,避免回显失败
|
||||
this.form.undertakeDeptId = ids.map(id => {
|
||||
const matched = this.platformCompanyOptions.find(item => String(item.value) === String(id));
|
||||
return matched ? matched.value : id;
|
||||
});
|
||||
},
|
||||
excludeExternalOrganization(list = []) {
|
||||
return list.reduce((result, item) => {
|
||||
@@ -2756,11 +2834,20 @@ export default {
|
||||
if (this.dialogReadonly) return;
|
||||
this.userPickType = type;
|
||||
this.userBox = true;
|
||||
this.userPage.currentPage = 1;
|
||||
this.loadUsers();
|
||||
},
|
||||
searchUsers() {
|
||||
this.userPage.currentPage = 1;
|
||||
this.loadUsers();
|
||||
},
|
||||
loadUsers() {
|
||||
this.userLoading = true;
|
||||
getUserList(this.userPage.currentPage, this.userPage.pageSize, this.userQuery)
|
||||
const params = {
|
||||
realName: String(this.userQuery.realName || '').trim(),
|
||||
phone: String(this.userQuery.phone || '').trim(),
|
||||
};
|
||||
getUserList(this.userPage.currentPage, this.userPage.pageSize, params)
|
||||
.then(res => {
|
||||
const result = res.data.data || {};
|
||||
this.userData = result.records || [];
|
||||
@@ -2810,7 +2897,7 @@ export default {
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
//margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
|
||||
@@ -99,11 +99,21 @@
|
||||
<el-table :data="attachmentRows" border @selection-change="selectedAttachments = $event">
|
||||
<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" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名称"
|
||||
min-width="240"
|
||||
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">
|
||||
@@ -184,6 +194,7 @@
|
||||
title="查看临时额度申请"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
align-center
|
||||
:width="formDialogWidth"
|
||||
class="temporary-credit-limit-dialog temporary-credit-limit-detail-dialog"
|
||||
>
|
||||
@@ -216,11 +227,21 @@
|
||||
</div>
|
||||
<el-table :data="attachmentRows" empty-text="暂无附件">
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名称" min-width="240" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名称"
|
||||
min-width="240"
|
||||
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" show-overflow-tooltip>
|
||||
@@ -615,6 +636,12 @@ export default {
|
||||
}
|
||||
const value = row[prop];
|
||||
if (value === undefined || value === null || value === '') return '-';
|
||||
if (
|
||||
(prop === 'usedFundLimit' || prop === 'remainingFundLimit') &&
|
||||
Number(value) === -1
|
||||
) {
|
||||
return '-';
|
||||
}
|
||||
return value;
|
||||
},
|
||||
applyDetail(detail) {
|
||||
@@ -1188,10 +1215,10 @@ export default {
|
||||
}
|
||||
|
||||
// 新增/编辑/查看:宽度与高度规则统一(Avue setPx 对 '1100px' 会拼成 '1100pxpx',故 option 用数字 1100)
|
||||
// margin:auto 配合下方 unscoped 的 overlay flex,实现垂直水平居中
|
||||
.temporary-credit-limit-dialog.el-dialog {
|
||||
width: 1100px !important;
|
||||
margin-top: 20px !important;
|
||||
margin-bottom: 20px !important;
|
||||
margin: auto !important;
|
||||
height: auto !important;
|
||||
max-height: calc(100vh - 40px) !important;
|
||||
display: flex;
|
||||
@@ -1329,3 +1356,16 @@ export default {
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<!-- append-to-body 弹窗挂到 body,overlay 无 scoped 属性,需全局样式才能居中 -->
|
||||
<style lang="scss">
|
||||
.el-overlay-dialog:has(> .temporary-credit-limit-dialog) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.temporary-credit-limit-dialog.el-dialog {
|
||||
margin: auto !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -585,6 +585,10 @@ export default {
|
||||
this.$message.warning(`第${index + 1}条调度明细缺少车牌号`);
|
||||
return;
|
||||
}
|
||||
if (!this.resolveDispatchQuantityUnit(row)) {
|
||||
this.$message.warning(`第${index + 1}条调度明细缺少数量单位`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.$confirm(
|
||||
@@ -600,32 +604,57 @@ export default {
|
||||
this.submitDispatch();
|
||||
});
|
||||
},
|
||||
resolveDispatchQuantityUnit(row = {}) {
|
||||
const topUnit = String(row.quantityUnit || '').trim();
|
||||
if (topUnit) return topUnit;
|
||||
const goodsList = this.parseGoodsList(row.goodsJson);
|
||||
for (const goods of goodsList) {
|
||||
const unit = String(
|
||||
goods?.quantityUnit || goods?.goodsQuantityUnit || goods?.unit || ''
|
||||
).trim();
|
||||
if (unit) return unit;
|
||||
}
|
||||
return TRANSPORT_PLAN_QUANTITY_UNIT;
|
||||
},
|
||||
enrichDispatchGoodsJson(row = {}, quantityUnit) {
|
||||
const goodsList = this.parseGoodsList(row.goodsJson);
|
||||
if (goodsList.length) {
|
||||
const nextGoods = goodsList.map(goods => ({
|
||||
...goods,
|
||||
quantityUnit: String(goods.quantityUnit || quantityUnit || '').trim() || quantityUnit,
|
||||
}));
|
||||
return JSON.stringify(nextGoods);
|
||||
}
|
||||
return JSON.stringify([
|
||||
{
|
||||
cargoName: row.cargoName,
|
||||
cargoType: row.cargoType,
|
||||
quantity: row.quantity,
|
||||
quantityUnit,
|
||||
specification: row.specification,
|
||||
model: row.model,
|
||||
packageType: row.packageType,
|
||||
brand: row.brand,
|
||||
},
|
||||
]);
|
||||
},
|
||||
submitDispatch() {
|
||||
this.saving = true;
|
||||
const payload = {
|
||||
id: this.planId,
|
||||
mode: 'submit',
|
||||
waybills: this.dispatchList.map(row => ({
|
||||
...row,
|
||||
taskEntryMode: row.taskEntryMode || 'simple',
|
||||
taskRemark: row.remark || '',
|
||||
estimatedStartTime: row.estimatedStartTime || this.planData.planStartDate || '',
|
||||
estimatedEndTime: row.estimatedEndTime || this.planData.planEndDate || '',
|
||||
goodsJson:
|
||||
row.goodsJson ||
|
||||
JSON.stringify([
|
||||
{
|
||||
cargoName: row.cargoName,
|
||||
cargoType: row.cargoType,
|
||||
quantity: row.quantity,
|
||||
quantityUnit: row.quantityUnit,
|
||||
specification: row.specification,
|
||||
model: row.model,
|
||||
packageType: row.packageType,
|
||||
brand: row.brand,
|
||||
},
|
||||
]),
|
||||
})),
|
||||
waybills: this.dispatchList.map(row => {
|
||||
const quantityUnit = this.resolveDispatchQuantityUnit(row);
|
||||
return {
|
||||
...row,
|
||||
quantityUnit,
|
||||
taskEntryMode: row.taskEntryMode || 'simple',
|
||||
taskRemark: row.remark || '',
|
||||
estimatedStartTime: row.estimatedStartTime || this.planData.planStartDate || '',
|
||||
estimatedEndTime: row.estimatedEndTime || this.planData.planEndDate || '',
|
||||
goodsJson: this.enrichDispatchGoodsJson(row, quantityUnit),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
dispatch(payload)
|
||||
@@ -639,7 +668,7 @@ export default {
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('调度失败:', err);
|
||||
this.$message.error('调度失败');
|
||||
this.$message.error(err?.response?.data?.msg || err?.message || '调度失败');
|
||||
})
|
||||
.finally(() => {
|
||||
this.saving = false;
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<el-option
|
||||
v-for="item in contractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -165,6 +165,7 @@ import { getList as getContractList } from '@/api/business/contract-manage';
|
||||
import { getList as getProjectList } from '@/api/business/project-apply';
|
||||
import { importTransportPlan, validateTransportPlan } from '@/api/business/transport-plan';
|
||||
import { config } from '@/option/business/transport-plan';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@@ -271,6 +272,7 @@ export default {
|
||||
this.loadProjectOptions();
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
loadProjectOptions() {
|
||||
if (this.projectLoading) return;
|
||||
this.projectLoading = true;
|
||||
@@ -651,7 +653,7 @@ export default {
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,11 +96,25 @@ const uploadReplaceFile = async file => {
|
||||
if (!task?.id || !task.objectKey) throw new Error('创建分片上传任务失败');
|
||||
replaceProgress.value = 0;
|
||||
for (let partNumber = 1; partNumber <= task.chunkTotal; partNumber += 1) {
|
||||
const urlRes = await api.getPartUploadUrl(task.id, partNumber);
|
||||
const part = file.slice((partNumber - 1) * chunkSize, Math.min(partNumber * chunkSize, file.size));
|
||||
const response = await fetch(urlRes.data?.data, { method: 'PUT', body: part });
|
||||
if (!response.ok) throw new Error(`分片${partNumber}上传失败`);
|
||||
const etag = String(response.headers.get('etag') || '').replaceAll('"', '');
|
||||
const urlRes = await api.getPartUploadUrl(task.id, partNumber);
|
||||
const url = urlRes.data?.data;
|
||||
let etag = '';
|
||||
const needProxy =
|
||||
typeof window !== 'undefined' &&
|
||||
window.location.protocol === 'https:' &&
|
||||
String(url || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.startsWith('http:');
|
||||
if (!url || needProxy) {
|
||||
const proxyRes = await api.uploadPartByProxy(task.id, partNumber, part);
|
||||
etag = String(proxyRes.data?.data || '').replaceAll('"', '');
|
||||
} else {
|
||||
const response = await fetch(url, { method: 'PUT', body: part });
|
||||
if (!response.ok) throw new Error(`分片${partNumber}上传失败`);
|
||||
etag = String(response.headers.get('etag') || '').replaceAll('"', '');
|
||||
}
|
||||
if (!etag) throw new Error('未获取到分片 ETag');
|
||||
await api.updateFileTask({ id: task.id, partNumber, etag });
|
||||
replaceProgress.value = Math.round((partNumber / task.chunkTotal) * 100);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<basic-container class="voucher-manage-page">
|
||||
<section class="voucher-manage-page__search">
|
||||
<section v-show="searchVisible" class="voucher-manage-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="88px">
|
||||
<div class="voucher-manage-page__search-grid">
|
||||
<el-form-item label="凭证批次号"
|
||||
@@ -55,10 +55,25 @@
|
||||
</section>
|
||||
<section>
|
||||
<div class="voucher-manage-page__toolbar">
|
||||
<el-button type="primary" @click="openUpload">批量导入凭证</el-button
|
||||
><el-button plain @click="openProgress">上传进度</el-button>
|
||||
<div class="voucher-manage-page__toolbar-left">
|
||||
<el-button type="primary" @click="openUpload">批量导入凭证</el-button>
|
||||
<el-button plain @click="openProgress">上传进度</el-button>
|
||||
</div>
|
||||
<table-header-tools
|
||||
:columns="columnSettingOptions"
|
||||
v-model:visible-column-keys="visibleColumnKeys"
|
||||
v-model:table-size="tableSize"
|
||||
v-model:search-visible="searchVisible"
|
||||
@refresh="load"
|
||||
/>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" border class="voucher-manage-page__table">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
:size="tableSize"
|
||||
border
|
||||
class="voucher-manage-page__table"
|
||||
>
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" /><el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
@@ -66,27 +81,47 @@
|
||||
fixed="left"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column prop="voucherBatchNo" label="凭证批次号" min-width="150" fixed="left"
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('voucherBatchNo')"
|
||||
prop="voucherBatchNo"
|
||||
label="凭证批次号"
|
||||
min-width="150"
|
||||
fixed="left"
|
||||
><template #default="{ row }"
|
||||
><el-link class="voucher-manage-page__link" type="primary" @click="view(row)">{{
|
||||
row.voucherBatchNo
|
||||
}}</el-link></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column prop="waybillBatchNo" label="运单批次号" min-width="170" /><el-table-column
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('waybillBatchNo')"
|
||||
prop="waybillBatchNo"
|
||||
label="运单批次号"
|
||||
min-width="170"
|
||||
/><el-table-column
|
||||
v-if="isColumnVisible('fileName')"
|
||||
prop="fileName"
|
||||
label="文件名"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column v-if="!isCarrier" prop="uploadSource" label="上传来源" width="100" /><el-table-column
|
||||
v-if="!isCarrier"
|
||||
<el-table-column
|
||||
v-if="!isCarrier && isColumnVisible('uploadSource')"
|
||||
prop="uploadSource"
|
||||
label="上传来源"
|
||||
width="100"
|
||||
/><el-table-column
|
||||
v-if="!isCarrier && isColumnVisible('carrierName')"
|
||||
prop="carrierName"
|
||||
label="承运商名称"
|
||||
min-width="160"
|
||||
><template #default="{ row }">{{ row.carrierName || '-' }}</template></el-table-column
|
||||
>
|
||||
<el-table-column prop="processStatus" label="处理状态" width="120"
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('processStatus')"
|
||||
prop="processStatus"
|
||||
label="处理状态"
|
||||
width="120"
|
||||
><template #default="{ row }"
|
||||
><span
|
||||
:class="{ 'voucher-manage-page__process-failed': row.processStatus === '处理失败' }"
|
||||
@@ -94,17 +129,35 @@
|
||||
></template
|
||||
></el-table-column
|
||||
><el-table-column
|
||||
v-if="isColumnVisible('voucherCount')"
|
||||
prop="voucherCount"
|
||||
label="凭证数量"
|
||||
width="100"
|
||||
/><el-table-column
|
||||
v-if="isColumnVisible('relatedWaybillCount')"
|
||||
prop="relatedWaybillCount"
|
||||
label="凭证已关联数量"
|
||||
width="125"
|
||||
/> <el-table-column prop="unRelatedWaybillCount" label="凭证未关联数量" width="125" />
|
||||
<el-table-column prop="createUserName" label="创建人" min-width="120" />
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" />
|
||||
/><el-table-column
|
||||
v-if="isColumnVisible('unRelatedWaybillCount')"
|
||||
prop="unRelatedWaybillCount"
|
||||
label="凭证未关联数量"
|
||||
width="125"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('createUserName')"
|
||||
prop="createUserName"
|
||||
label="创建人"
|
||||
min-width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('createTime')"
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="170"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('auditStatus')"
|
||||
prop="auditStatus"
|
||||
label="审核状态"
|
||||
width="120"
|
||||
@@ -512,6 +565,7 @@ import { WarnTriangleFilled } from '@element-plus/icons-vue';
|
||||
import { getList as getProjectList } from '@/api/business/project-apply';
|
||||
import * as api from '@/api/business/voucher-manage';
|
||||
import SectionCard from '@/components/section-card/main.vue';
|
||||
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
@@ -539,6 +593,47 @@ const checkIfCarrier = () => {
|
||||
isCarrier.value = topDeptName === '外部组织';
|
||||
};
|
||||
|
||||
const VOUCHER_BASE_COLUMNS = [
|
||||
{ prop: 'voucherBatchNo', label: '凭证批次号' },
|
||||
{ prop: 'waybillBatchNo', label: '运单批次号' },
|
||||
{ prop: 'fileName', label: '文件名' },
|
||||
{ prop: 'uploadSource', label: '上传来源', carrierHidden: true },
|
||||
{ prop: 'carrierName', label: '承运商名称', carrierHidden: true },
|
||||
{ prop: 'processStatus', label: '处理状态' },
|
||||
{ prop: 'voucherCount', label: '凭证数量' },
|
||||
{ prop: 'relatedWaybillCount', label: '凭证已关联数量' },
|
||||
{ prop: 'unRelatedWaybillCount', label: '凭证未关联数量' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
{ prop: 'auditStatus', label: '审核状态' },
|
||||
];
|
||||
|
||||
const searchVisible = ref(true);
|
||||
const tableSize = ref('default');
|
||||
const visibleColumnKeys = ref([]);
|
||||
const columnSettingOptions = computed(() =>
|
||||
VOUCHER_BASE_COLUMNS.filter(column => !isCarrier.value || !column.carrierHidden).map(column => ({
|
||||
prop: column.prop,
|
||||
label: column.label,
|
||||
}))
|
||||
);
|
||||
const isColumnVisible = prop => visibleColumnKeys.value.includes(prop);
|
||||
|
||||
watch(
|
||||
columnSettingOptions,
|
||||
options => {
|
||||
const props = options.map(column => column.prop);
|
||||
if (!visibleColumnKeys.value.length) {
|
||||
visibleColumnKeys.value = props;
|
||||
return;
|
||||
}
|
||||
const visible = new Set(visibleColumnKeys.value);
|
||||
visibleColumnKeys.value = props.filter(prop => visible.has(prop));
|
||||
if (!visibleColumnKeys.value.length) visibleColumnKeys.value = props;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
checkIfCarrier();
|
||||
});
|
||||
@@ -742,6 +837,30 @@ const calculateMd5 = async file => {
|
||||
const buffer = await file.arrayBuffer();
|
||||
return md5.base64(buffer);
|
||||
};
|
||||
const needsUploadProxy = url =>
|
||||
typeof window !== 'undefined' &&
|
||||
window.location.protocol === 'https:' &&
|
||||
String(url || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.startsWith('http:');
|
||||
const uploadPartWithFallback = async (taskId, partNumber, part, signal) => {
|
||||
const urlRes = await api.getPartUploadUrl(taskId, partNumber);
|
||||
if (signal?.aborted) throw new DOMException('上传已暂停', 'AbortError');
|
||||
const url = urlRes.data?.data;
|
||||
// HTTPS 页面直连 http://IP MinIO 会被 Mixed Content 拦截,回退服务端代传
|
||||
if (!url || needsUploadProxy(url)) {
|
||||
const proxyRes = await api.uploadPartByProxy(taskId, partNumber, part);
|
||||
return String(proxyRes.data?.data || '').replaceAll('"', '');
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
body: part,
|
||||
signal,
|
||||
});
|
||||
if (!response.ok) throw new Error(`分片${partNumber}上传失败`);
|
||||
return String(response.headers.get('etag') || '').replaceAll('"', '');
|
||||
};
|
||||
const uploadParts = async (file, task, sessionId) => {
|
||||
uploading.value = true;
|
||||
activeUploadTaskId.value = task.id;
|
||||
@@ -756,21 +875,13 @@ const uploadParts = async (file, task, sessionId) => {
|
||||
) {
|
||||
ensureUploadSession(sessionId, task.id);
|
||||
if (abortController.signal.aborted) throw new DOMException('上传已暂停', 'AbortError');
|
||||
const urlRes = await api.getPartUploadUrl(task.id, partNumber);
|
||||
ensureUploadSession(sessionId, task.id);
|
||||
if (abortController.signal.aborted) throw new DOMException('上传已暂停', 'AbortError');
|
||||
const url = urlRes.data?.data;
|
||||
const part = file.slice(
|
||||
(partNumber - 1) * chunkSize,
|
||||
Math.min(partNumber * chunkSize, file.size)
|
||||
);
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
body: part,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
if (!response.ok) throw new Error(`分片${partNumber}上传失败`);
|
||||
const etag = String(response.headers.get('etag') || '').replaceAll('"', '');
|
||||
const etag = await uploadPartWithFallback(task.id, partNumber, part, abortController.signal);
|
||||
if (!etag) throw new Error('MinIO 未返回分片 ETag,请确认跨域配置已暴露 ETag 响应头');
|
||||
await api.updateFileTask({ id: task.id, partNumber, etag });
|
||||
ensureUploadSession(sessionId, task.id);
|
||||
@@ -1329,9 +1440,15 @@ load();
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
&__toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
&__table {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1370,7 +1487,7 @@ load();
|
||||
display: none;
|
||||
}
|
||||
&__upload-form :deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
&__upload-tip {
|
||||
margin-left: 16px;
|
||||
|
||||
@@ -529,7 +529,7 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bill-ledger-form-page__form :deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.bill-ledger-form-page__basic-grid {
|
||||
display: grid;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<basic-container class="bill-ledger-page">
|
||||
<bill-ledger-search
|
||||
v-if="searchVisible"
|
||||
:query="query"
|
||||
:loading="loading"
|
||||
@search="handleSearch"
|
||||
@@ -20,10 +21,12 @@
|
||||
:rows="rows"
|
||||
:loading="loading"
|
||||
:page="page"
|
||||
v-model:search-visible="searchVisible"
|
||||
@add="openCreate"
|
||||
@view="openView"
|
||||
@edit="openEdit"
|
||||
@delete="handleDelete"
|
||||
@refresh="loadData"
|
||||
@page-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
@@ -124,6 +127,7 @@ export default {
|
||||
return {
|
||||
query: emptyQuery(),
|
||||
counts: { all: 0, within30: 0, within90: 0, over90: 0 },
|
||||
searchVisible: true,
|
||||
rows: [],
|
||||
loading: false,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
|
||||
@@ -367,7 +367,7 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bill-payment-form-page__form :deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.bill-payment-form-page__form :deep(.el-input),
|
||||
.bill-payment-form-page__form :deep(.el-input-number),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<basic-container class="bill-payment-page">
|
||||
<bill-payment-search
|
||||
v-if="searchVisible"
|
||||
:query="query"
|
||||
:loading="loading"
|
||||
@search="handleSearch"
|
||||
@@ -11,6 +12,7 @@
|
||||
:loading="loading"
|
||||
:page="page"
|
||||
:mk-enabled="mkEnabled"
|
||||
v-model:search-visible="searchVisible"
|
||||
@add="openCreate"
|
||||
@view="openView"
|
||||
@edit="openEdit"
|
||||
@@ -20,6 +22,7 @@
|
||||
@approve="handleApprove"
|
||||
@return="handleReturn"
|
||||
@void="handleVoid"
|
||||
@refresh="loadTable"
|
||||
@page-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
@@ -47,6 +50,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
query: emptyQuery(),
|
||||
searchVisible: true,
|
||||
rows: [],
|
||||
loading: false,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
|
||||
@@ -4,11 +4,19 @@
|
||||
<el-button v-if="hasPermission('bill_ledger_add')" type="primary" @click="$emit('add')">
|
||||
新增
|
||||
</el-button>
|
||||
<table-header-tools
|
||||
:columns="columns"
|
||||
v-model:visible-column-keys="visibleColumnKeys"
|
||||
v-model:table-size="tableSize"
|
||||
:search-visible="searchVisible"
|
||||
@update:search-visible="$emit('update:searchVisible', $event)"
|
||||
@refresh="$emit('refresh')"
|
||||
/>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" border>
|
||||
<el-table v-loading="loading" :data="rows" :size="tableSize" border>
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in columns"
|
||||
v-for="column in visibleColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
@@ -60,20 +68,30 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { billLedgerTableColumns } from '@/option/payment/billLedger';
|
||||
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||||
|
||||
export default {
|
||||
name: 'BillLedgerTable',
|
||||
components: { TableHeaderTools },
|
||||
props: {
|
||||
rows: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
page: { type: Object, required: true },
|
||||
searchVisible: { type: Boolean, default: true },
|
||||
},
|
||||
emits: ['add', 'view', 'edit', 'delete', 'page-change', 'size-change'],
|
||||
emits: ['add', 'view', 'edit', 'delete', 'page-change', 'size-change', 'refresh', 'update:searchVisible'],
|
||||
data() {
|
||||
return { columns: billLedgerTableColumns };
|
||||
return {
|
||||
columns: billLedgerTableColumns,
|
||||
visibleColumnKeys: billLedgerTableColumns.map(column => column.prop),
|
||||
tableSize: 'default',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission']),
|
||||
visibleColumns() {
|
||||
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
@@ -96,7 +114,7 @@ export default {
|
||||
.bill-ledger-table__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.bill-ledger-table__links {
|
||||
|
||||
@@ -4,11 +4,19 @@
|
||||
<el-button v-if="hasPermission('bill_payment_add')" type="primary" @click="$emit('add')">
|
||||
新增
|
||||
</el-button>
|
||||
<table-header-tools
|
||||
:columns="columns"
|
||||
v-model:visible-column-keys="visibleColumnKeys"
|
||||
v-model:table-size="tableSize"
|
||||
:search-visible="searchVisible"
|
||||
@update:search-visible="$emit('update:searchVisible', $event)"
|
||||
@refresh="$emit('refresh')"
|
||||
/>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" border>
|
||||
<el-table v-loading="loading" :data="rows" :size="tableSize" border>
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in columns"
|
||||
v-for="column in visibleColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
@@ -61,7 +69,7 @@
|
||||
<el-link
|
||||
v-if="
|
||||
mkEnabled &&
|
||||
row.approvalStatus === 'reviewing' &&
|
||||
['reviewing', 'approved'].includes(row.approvalStatus) &&
|
||||
hasPermission('bill_payment_approve')
|
||||
"
|
||||
type="primary"
|
||||
@@ -115,15 +123,18 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { billPaymentTableColumns } from '@/option/payment/billPayment';
|
||||
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
export default {
|
||||
name: 'BillPaymentTable',
|
||||
components: { TableHeaderTools },
|
||||
props: {
|
||||
rows: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
page: { type: Object, required: true },
|
||||
mkEnabled: { type: Boolean, default: false },
|
||||
searchVisible: { type: Boolean, default: true },
|
||||
},
|
||||
emits: [
|
||||
'add',
|
||||
@@ -137,12 +148,21 @@ export default {
|
||||
'void',
|
||||
'page-change',
|
||||
'size-change',
|
||||
'refresh',
|
||||
'update:searchVisible',
|
||||
],
|
||||
data() {
|
||||
return { columns: billPaymentTableColumns };
|
||||
return {
|
||||
columns: billPaymentTableColumns,
|
||||
visibleColumnKeys: billPaymentTableColumns.map(column => column.prop),
|
||||
tableSize: 'default',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
visibleColumns() {
|
||||
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
||||
},
|
||||
isAdmin() {
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
@@ -173,7 +193,8 @@ export default {
|
||||
}
|
||||
.bill-payment-table__toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.bill-payment-table__links {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<basic-container class="invoice-page">
|
||||
<section class="invoice-page__search">
|
||||
<section v-if="searchVisible" class="invoice-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="88px" @submit.prevent>
|
||||
<div class="invoice-page__search-grid">
|
||||
<el-form-item label="单据号">
|
||||
@@ -79,18 +79,26 @@
|
||||
>导出</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="invoice-page__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button text :icon="Refresh" @click="loadTable" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<table-header-tools
|
||||
:columns="columns"
|
||||
v-model:visible-column-keys="visibleColumnKeys"
|
||||
v-model:table-size="tableSize"
|
||||
v-model:search-visible="searchVisible"
|
||||
@refresh="loadTable"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="rows" border @selection-change="selection = $event">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
:size="tableSize"
|
||||
border
|
||||
@selection-change="selection = $event"
|
||||
>
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in columns"
|
||||
v-for="column in visibleColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
@@ -161,7 +169,7 @@
|
||||
v-if="
|
||||
mkEnabled &&
|
||||
hasPermission('invoice_application_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
['reviewing', 'approved'].includes(row.approvalStatus)
|
||||
"
|
||||
type="primary"
|
||||
@click="handleMkAudit(row)"
|
||||
@@ -218,12 +226,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArrowDown, ArrowUp, Refresh } from '@element-plus/icons-vue';
|
||||
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import * as XLSX from 'xlsx';
|
||||
import * as api from '@/api/payment/invoiceApplication';
|
||||
import * as formalSettlementApi from '@/api/settlement/formalSettlement';
|
||||
import { invoiceApplicationTableColumns } from '@/option/payment/invoiceApplication';
|
||||
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||||
import organizationSearch from '@/mixins/organization-search';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
@@ -239,17 +248,20 @@ const emptyQuery = () => ({
|
||||
|
||||
export default {
|
||||
name: 'InvoiceApplication',
|
||||
components: { TableHeaderTools },
|
||||
mixins: [organizationSearch],
|
||||
data() {
|
||||
return {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Refresh,
|
||||
query: emptyQuery(),
|
||||
searchExpanded: false,
|
||||
searchVisible: true,
|
||||
tableSize: 'default',
|
||||
approvalStatusOptions: api.approvalStatusOptions,
|
||||
kingdeeStatusOptions: api.kingdeeStatusOptions,
|
||||
columns: invoiceApplicationTableColumns,
|
||||
visibleColumnKeys: invoiceApplicationTableColumns.map(column => column.prop),
|
||||
rows: [],
|
||||
selection: [],
|
||||
loading: false,
|
||||
@@ -260,6 +272,9 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission']),
|
||||
visibleColumns() {
|
||||
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadOrganizationOptions();
|
||||
@@ -531,7 +546,6 @@ export default {
|
||||
padding: 12px 0;
|
||||
}
|
||||
.invoice-page__toolbar-left,
|
||||
.invoice-page__toolbar-right,
|
||||
.invoice-page__links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -610,7 +610,7 @@ export default {
|
||||
padding-bottom: 72px;
|
||||
}
|
||||
.receipt-form-page__form :deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.receipt-form-page__form :deep(.el-select),
|
||||
.receipt-form-page__form :deep(.el-input-number) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<basic-container class="receipt-page">
|
||||
<section class="receipt-page__search">
|
||||
<section v-if="searchVisible" class="receipt-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="88px" @submit.prevent>
|
||||
<div class="receipt-page__search-grid">
|
||||
<el-form-item label="发票号码">
|
||||
@@ -85,18 +85,26 @@
|
||||
>导出</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="receipt-page__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button text :icon="Refresh" @click="loadTable" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<table-header-tools
|
||||
:columns="columns"
|
||||
v-model:visible-column-keys="visibleColumnKeys"
|
||||
v-model:table-size="tableSize"
|
||||
v-model:search-visible="searchVisible"
|
||||
@refresh="loadTable"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="rows" border @selection-change="selection = $event">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
:size="tableSize"
|
||||
border
|
||||
@selection-change="selection = $event"
|
||||
>
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in columns"
|
||||
v-for="column in visibleColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
@@ -150,7 +158,7 @@
|
||||
v-if="
|
||||
mkEnabled &&
|
||||
hasPermission('invoice_receipt_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
['reviewing', 'approved'].includes(row.approvalStatus)
|
||||
"
|
||||
type="primary"
|
||||
@click="handleMkAudit(row)"
|
||||
@@ -203,11 +211,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArrowDown, ArrowUp, Refresh } from '@element-plus/icons-vue';
|
||||
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import * as XLSX from 'xlsx';
|
||||
import * as api from '@/api/payment/invoiceReceipt';
|
||||
import { invoiceReceiptTableColumns } from '@/option/payment/invoiceReceipt';
|
||||
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||||
import organizationSearch from '@/mixins/organization-search';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
@@ -224,17 +233,20 @@ const emptyQuery = () => ({
|
||||
|
||||
export default {
|
||||
name: 'InvoiceReceipt',
|
||||
components: { TableHeaderTools },
|
||||
mixins: [organizationSearch],
|
||||
data() {
|
||||
return {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Refresh,
|
||||
query: emptyQuery(),
|
||||
searchExpanded: false,
|
||||
searchVisible: true,
|
||||
tableSize: 'default',
|
||||
approvalStatusOptions: api.approvalStatusOptions,
|
||||
kingdeePushStatusOptions: api.kingdeePushStatusOptions,
|
||||
columns: invoiceReceiptTableColumns,
|
||||
visibleColumnKeys: invoiceReceiptTableColumns.map(column => column.prop),
|
||||
rows: [],
|
||||
selection: [],
|
||||
loading: false,
|
||||
@@ -245,6 +257,9 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission']),
|
||||
visibleColumns() {
|
||||
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadOrganizationOptions();
|
||||
@@ -461,7 +476,6 @@ export default {
|
||||
padding: 12px 0;
|
||||
}
|
||||
.receipt-page__toolbar-left,
|
||||
.receipt-page__toolbar-right,
|
||||
.receipt-page__links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -36,23 +36,25 @@
|
||||
<el-col v-if="isSettlementRelatedType" :span="6"
|
||||
><el-form-item label="结算单号" prop="referenceId">
|
||||
<div class="payment-form-page__settlement-nos">
|
||||
<template v-if="referenceSettlementLinks.length">
|
||||
<el-link
|
||||
v-for="item in referenceSettlementLinks"
|
||||
:key="`${item.id || item.settlementNo}`"
|
||||
type="primary"
|
||||
@click="openSettlementDetail(item)"
|
||||
>
|
||||
{{ item.settlementNo }}
|
||||
</el-link>
|
||||
</template>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="referenceLabel"
|
||||
readonly
|
||||
<el-select
|
||||
ref="settlementNoSelectRef"
|
||||
:model-value="referenceSettlementNos"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:max-collapse-tags="1"
|
||||
:disabled="readonly"
|
||||
placeholder="请选择预结算/正式结算单"
|
||||
/>
|
||||
class="payment-form-page__settlement-select"
|
||||
@visible-change="handleSettlementSelectVisible"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in referenceSettlementLinks"
|
||||
:key="`${item.id || item.settlementNo}`"
|
||||
:label="item.settlementNo"
|
||||
:value="item.settlementNo"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button
|
||||
v-if="!readonly"
|
||||
class="payment-form-page__settlement-select-btn"
|
||||
@@ -100,7 +102,7 @@
|
||||
<el-option
|
||||
v-for="item in contractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName || item.contractNo"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -251,7 +253,7 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section-card>
|
||||
<section-card v-if="isSettlementRelatedType" title="发票信息">
|
||||
<section-card v-if="showInvoiceSection" title="发票信息">
|
||||
<el-table :data="form.invoices" border>
|
||||
<el-table-column type="index" label="序号" width="70" />
|
||||
<el-table-column prop="settlementNo" label="结算单号" min-width="120">
|
||||
@@ -355,19 +357,22 @@
|
||||
value="special_approval" /><el-option
|
||||
label="其他"
|
||||
value="other" /></el-select></template></el-table-column
|
||||
><el-table-column label="文件名" min-width="220"
|
||||
><el-table-column
|
||||
label="文件名"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
><template #default="{ row }"
|
||||
><div
|
||||
class="payment-form-page__attachment-file-name-cell"
|
||||
:title="attachmentFileName(row) || '-'"
|
||||
>
|
||||
<span
|
||||
class="payment-form-page__attachment-file-name"
|
||||
:class="{ 'is-disabled': !attachmentFileUrl(row) }"
|
||||
@click="attachmentFileUrl(row) && previewAttachment(row)"
|
||||
>{{ attachmentFileName(row) || '-' }}</span
|
||||
>
|
||||
</div></template
|
||||
><span
|
||||
class="attachment-file-name"
|
||||
:class="{ 'is-disabled': !attachmentFileUrl(row) }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="attachmentFileUrl(row) && previewAttachment(row)"
|
||||
@keydown.enter="attachmentFileUrl(row) && previewAttachment(row)"
|
||||
@keydown.space.prevent="attachmentFileUrl(row) && previewAttachment(row)"
|
||||
>{{ attachmentFileName(row) || '-' }}</span
|
||||
></template
|
||||
></el-table-column
|
||||
><el-table-column label="附件描述" min-width="220"
|
||||
><template #default="{ row }"
|
||||
@@ -671,6 +676,7 @@ import {
|
||||
getList as getCustomerArchiveList,
|
||||
} from '@/api/vehicle/customer-archive';
|
||||
import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { downloadFileByUrl } from '@/utils/util';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import {
|
||||
@@ -887,6 +893,10 @@ export default {
|
||||
isSettlementRelatedType() {
|
||||
return ['progress_advance', 'settlement_payment'].includes(this.form.paymentType);
|
||||
},
|
||||
// 项目预付、进度预付不展示发票信息,仅尾款付款展示
|
||||
showInvoiceSection() {
|
||||
return this.form.paymentType === 'settlement_payment';
|
||||
},
|
||||
canSelectPreSettlement() {
|
||||
// 进度预付仅预结算;尾款付款可正式/预结算
|
||||
return ['progress_advance', 'settlement_payment'].includes(this.form.paymentType);
|
||||
@@ -939,6 +949,9 @@ export default {
|
||||
referenceType: this.form.preSettlementId ? 'pre' : 'formal',
|
||||
}));
|
||||
},
|
||||
referenceSettlementNos() {
|
||||
return this.referenceSettlementLinks.map(item => item.settlementNo).filter(Boolean);
|
||||
},
|
||||
settlementRows() {
|
||||
if (this.referenceRows.length) {
|
||||
return this.referenceRows.map(item => ({
|
||||
@@ -1038,6 +1051,7 @@ export default {
|
||||
this.initialize();
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
hasPermission(code) {
|
||||
return this.permission?.[code] !== false;
|
||||
},
|
||||
@@ -1122,7 +1136,7 @@ export default {
|
||||
callback();
|
||||
},
|
||||
validateInvoices() {
|
||||
if (this.form.paymentType === 'project_advance') return;
|
||||
if (!this.showInvoiceSection) return;
|
||||
for (const invoice of this.form.invoices || []) {
|
||||
if (String(invoice.settlementNo || '').length > 100) {
|
||||
throw new Error('结算单号不能超过100个字符');
|
||||
@@ -1904,6 +1918,7 @@ export default {
|
||||
this.contractPaymentRatioExceeded = exceeded;
|
||||
},
|
||||
handleTypeChange() {
|
||||
// 切换类型时清空发票;预付类型本身也不再展示/提交发票
|
||||
this.form.invoices = [];
|
||||
this.paymentRecords = [];
|
||||
Object.assign(this.form, {
|
||||
@@ -2091,8 +2106,20 @@ export default {
|
||||
const project = this.projectOptions.find(item => String(item.id) === String(projectId));
|
||||
this.form.projectName = project?.projectName || project?.projectShortName || '';
|
||||
Object.assign(this.form, {
|
||||
deptId: project?.undertakeDeptId || project?.businessDeptId || null,
|
||||
deptName: project?.undertakeDeptName || project?.businessDeptName || '',
|
||||
deptId:
|
||||
String(project?.undertakeDeptId || '')
|
||||
.split(/[,,]/)
|
||||
.map(item => item.trim())
|
||||
.find(Boolean) ||
|
||||
project?.businessDeptId ||
|
||||
null,
|
||||
deptName:
|
||||
String(project?.undertakeDeptName || '')
|
||||
.split(/[,,、]/)
|
||||
.map(item => item.trim())
|
||||
.find(Boolean) ||
|
||||
project?.businessDeptName ||
|
||||
'',
|
||||
contractId: null,
|
||||
contractNo: '',
|
||||
contractName: '',
|
||||
@@ -2260,6 +2287,13 @@ export default {
|
||||
}
|
||||
return Promise.all(loaders);
|
||||
},
|
||||
handleSettlementSelectVisible(visible) {
|
||||
// 结算单号仅做展示,通过「选择」按钮维护,避免下拉改值
|
||||
if (!visible) return;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.settlementNoSelectRef?.blur?.();
|
||||
});
|
||||
},
|
||||
handleReferenceSearch() {
|
||||
this.formalPage.current = 1;
|
||||
this.prePage.current = 1;
|
||||
@@ -2623,7 +2657,7 @@ export default {
|
||||
attachmentsJson: JSON.stringify(attachments || []),
|
||||
remark,
|
||||
invoices:
|
||||
paymentType === 'project_advance'
|
||||
paymentType === 'project_advance' || paymentType === 'progress_advance'
|
||||
? []
|
||||
: invoices.map(invoice => ({
|
||||
settlementNo: invoice.settlementNo || '',
|
||||
@@ -2704,13 +2738,18 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
min-height: 32px;
|
||||
}
|
||||
.payment-form-page__settlement-nos :deep(.el-input) {
|
||||
.payment-form-page__settlement-select {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
min-width: 0;
|
||||
}
|
||||
.payment-form-page__settlement-select :deep(.el-select__wrapper) {
|
||||
width: 100%;
|
||||
}
|
||||
.payment-form-page__settlement-select :deep(.el-tag__close) {
|
||||
display: none;
|
||||
}
|
||||
.payment-form-page__settlement-select-btn {
|
||||
flex-shrink: 0;
|
||||
@@ -2787,38 +2826,6 @@ export default {
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.payment-form-page__attachment-file-name-cell {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.payment-form-page__attachment-file-name {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
color: var(--el-text-color-regular);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
color: var(--el-text-color-regular);
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
}
|
||||
.payment-form-page__attachment-upload .vehicle-attachment-upload {
|
||||
width: auto;
|
||||
}
|
||||
@@ -2826,7 +2833,7 @@ export default {
|
||||
display: inline-flex;
|
||||
}
|
||||
.payment-form-page :deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.payment-form-page :deep(.el-input-number .el-input__inner) {
|
||||
text-align: left;
|
||||
|
||||
@@ -1,297 +1,179 @@
|
||||
<template>
|
||||
<basic-container class="payment-page">
|
||||
<section class="payment-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="88px">
|
||||
<div class="payment-page__search-grid">
|
||||
<el-form-item label="单据号"
|
||||
><el-input v-model="query.paymentNo" clearable
|
||||
/></el-form-item>
|
||||
<el-form-item label="日期"
|
||||
><el-date-picker
|
||||
v-model="query.applyDateRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
range-separator="~"
|
||||
start-placeholder="年/月/日"
|
||||
end-placeholder="年/月/日"
|
||||
/></el-form-item>
|
||||
<el-form-item label="客户名称"
|
||||
><el-input v-model="query.payeeName" clearable
|
||||
/></el-form-item>
|
||||
<el-form-item label="项目名称"
|
||||
><el-input v-model="query.projectName" clearable
|
||||
/></el-form-item>
|
||||
<template v-if="searchExpanded">
|
||||
<el-form-item label="所属组织"
|
||||
><el-cascader
|
||||
v-model="query.deptPath"
|
||||
:options="organizationTreeOptions"
|
||||
:props="organizationCascaderProps"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 100%"
|
||||
placeholder="请选择 所属组织"
|
||||
@change="val => { query.deptName = findOrgById(val)?.rawLabel || '' }"
|
||||
/></el-form-item>
|
||||
<el-form-item label="关联结算单"
|
||||
><el-input v-model="query.settlementNo" clearable
|
||||
/></el-form-item>
|
||||
<el-form-item label="付款类型"
|
||||
><el-select v-model="query.paymentType" clearable
|
||||
><el-option
|
||||
v-for="item in paymentTypeOptions"
|
||||
:key="item.value"
|
||||
v-bind="item" /></el-select
|
||||
></el-form-item>
|
||||
<el-form-item label="审核状态"
|
||||
><el-select v-model="query.approvalStatus" clearable
|
||||
><el-option
|
||||
v-for="item in approvalStatusOptions"
|
||||
:key="item.value"
|
||||
v-bind="item" /></el-select
|
||||
></el-form-item>
|
||||
<el-form-item label="金蝶单据状态"
|
||||
><el-select v-model="query.kingdeeStatus" clearable
|
||||
><el-option
|
||||
v-for="item in kingdeeStatusOptions"
|
||||
:key="item.value"
|
||||
v-bind="item" /></el-select
|
||||
></el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
<div class="payment-page__search-actions">
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
<el-button text @click="searchExpanded = !searchExpanded"
|
||||
><el-icon><component :is="searchExpanded ? ArrowUp : ArrowDown" /></el-icon
|
||||
></el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
<section class="payment-page__table-panel">
|
||||
<div class="payment-page__toolbar">
|
||||
<div class="payment-page__toolbar-left">
|
||||
<el-button
|
||||
v-if="hasPermission('payment_application_add')"
|
||||
type="primary"
|
||||
@click="openCreate"
|
||||
>新增付款申请</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('payment_application_sync')"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="syncLoading"
|
||||
@click="handleSync"
|
||||
>批量同步</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('payment_application_sync')"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="resultSyncing"
|
||||
@click="handleSyncResult"
|
||||
>同步付款结果</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('payment_application_add')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="openSettlementPayment"
|
||||
>申请付尾款</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('payment_application_add')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="openProjectAdvance"
|
||||
>预付申请</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('payment_application_export')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleExport"
|
||||
>导出</el-button
|
||||
>
|
||||
</div>
|
||||
<el-button text :icon="Refresh" @click="loadTable" />
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" border @selection-change="selection = $event">
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in columns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
<avue-crud
|
||||
ref="crud"
|
||||
:option="option"
|
||||
:table-loading="loading"
|
||||
:data="rows"
|
||||
v-model:page="page"
|
||||
@search-change="searchChange"
|
||||
@search-reset="searchReset"
|
||||
@selection-change="selectionChange"
|
||||
@current-change="currentChange"
|
||||
@size-change="sizeChange"
|
||||
@refresh-change="refreshChange"
|
||||
@on-load="onLoad"
|
||||
>
|
||||
<template #menu-left>
|
||||
<el-button
|
||||
v-if="hasPermission('payment_application_add')"
|
||||
type="primary"
|
||||
@click="openCreate"
|
||||
>新增付款申请</el-button
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="column.link" type="primary" @click="openView(row)">{{
|
||||
row[column.prop]
|
||||
}}</el-link>
|
||||
<el-tag
|
||||
v-else-if="column.status"
|
||||
:type="statusType(row.approvalStatus)"
|
||||
class="status-text"
|
||||
>{{ row[column.prop] || '-' }}</el-tag
|
||||
>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<template v-else-if="column.prop === 'kingdeeLockStatus'">
|
||||
<el-tooltip v-if="lockTag(row).show" :content="lockTag(row).tip" placement="top">
|
||||
<el-tag :type="lockTag(row).type" class="status-text">{{ lockTag(row).label }}</el-tag>
|
||||
</el-tooltip>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="320" fixed="right" align="center">
|
||||
<template #default="{ row }"
|
||||
><div class="payment-page__links">
|
||||
<el-link
|
||||
v-if="hasPermission('payment_application_view')"
|
||||
type="primary"
|
||||
@click="openView(row)"
|
||||
>查看</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('payment_application_edit') &&
|
||||
['draft', 'returned'].includes(row.approvalStatus)
|
||||
"
|
||||
type="primary"
|
||||
@click="openEdit(row)"
|
||||
>编辑</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('payment_application_submit') &&
|
||||
['draft', 'returned'].includes(row.approvalStatus)
|
||||
"
|
||||
type="primary"
|
||||
@click="handleSubmit(row)"
|
||||
>提交</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
mkEnabled &&
|
||||
hasPermission('payment_application_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="primary"
|
||||
@click="handleMkAudit(row)"
|
||||
>流程</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
!mkEnabled &&
|
||||
hasPermission('payment_application_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="primary"
|
||||
@click="handleApprove(row)"
|
||||
>通过</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
!mkEnabled &&
|
||||
hasPermission('payment_application_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleReturn(row)"
|
||||
>驳回</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('payment_application_void') &&
|
||||
row.approvalStatus === 'approved' &&
|
||||
row.kingdeeLockStatus !== 'locked'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleVoid(row)"
|
||||
>作废</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('payment_application_delete') && row.approvalStatus === 'draft'"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
>删除</el-link
|
||||
>
|
||||
</div></template
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="payment-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadTable"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<el-button
|
||||
v-if="hasPermission('payment_application_sync')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleSync"
|
||||
>批量同步</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('payment_application_add')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="openSettlementPayment"
|
||||
>申请付尾款</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('payment_application_add')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="openProjectAdvance"
|
||||
>预付申请</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('payment_application_export')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleExport"
|
||||
>导出</el-button
|
||||
>
|
||||
</template>
|
||||
|
||||
<template #paymentNo="{ row }">
|
||||
<el-link type="primary" @click="openView(row)">{{ row.paymentNo }}</el-link>
|
||||
</template>
|
||||
|
||||
<template #approvalStatusName="{ row }">
|
||||
<el-tag :type="statusType(row.approvalStatus)" class="status-text">{{
|
||||
row.approvalStatusName || '-'
|
||||
}}</el-tag>
|
||||
</template>
|
||||
|
||||
<template #menu="{ row }">
|
||||
<el-link
|
||||
v-if="hasPermission('payment_application_view')"
|
||||
type="primary"
|
||||
@click="openView(row)"
|
||||
>查看</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('payment_application_edit') &&
|
||||
['draft', 'returned'].includes(row.approvalStatus)
|
||||
"
|
||||
type="primary"
|
||||
@click="openEdit(row)"
|
||||
>编辑</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('payment_application_submit') &&
|
||||
['draft', 'returned'].includes(row.approvalStatus)
|
||||
"
|
||||
type="primary"
|
||||
@click="handleSubmit(row)"
|
||||
>提交</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
mkEnabled &&
|
||||
hasPermission('payment_application_approve') &&
|
||||
['reviewing', 'approved'].includes(row.approvalStatus)
|
||||
"
|
||||
type="primary"
|
||||
@click="handleMkAudit(row)"
|
||||
>流程</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
!mkEnabled &&
|
||||
hasPermission('payment_application_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="primary"
|
||||
@click="handleApprove(row)"
|
||||
>通过</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
!mkEnabled &&
|
||||
hasPermission('payment_application_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleReturn(row)"
|
||||
>驳回</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('payment_application_void') && row.approvalStatus === 'approved'"
|
||||
type="danger"
|
||||
@click="handleVoid(row)"
|
||||
>作废</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('payment_application_delete') && row.approvalStatus === 'draft'"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
>删除</el-link
|
||||
>
|
||||
</template>
|
||||
</avue-crud>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArrowDown, ArrowUp, Refresh } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import * as api from '@/api/payment/paymentApplication';
|
||||
import { paymentApplicationTableColumns } from '@/option/payment/paymentApplication';
|
||||
import { paymentApplicationOption } from '@/option/payment/paymentApplication';
|
||||
import organizationSearch from '@/mixins/organization-search';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
paymentNo: '',
|
||||
applyDateRange: [],
|
||||
payeeName: '',
|
||||
projectName: '',
|
||||
deptName: '',
|
||||
deptPath: [],
|
||||
settlementNo: '',
|
||||
paymentType: '',
|
||||
approvalStatus: '',
|
||||
kingdeeStatus: '',
|
||||
});
|
||||
export default {
|
||||
name: 'PaymentApplication',
|
||||
mixins: [organizationSearch],
|
||||
data() {
|
||||
return {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Refresh,
|
||||
query: emptyQuery(),
|
||||
searchExpanded: false,
|
||||
paymentTypeOptions: api.paymentTypeOptions,
|
||||
approvalStatusOptions: api.approvalStatusOptions,
|
||||
kingdeeStatusOptions: api.kingdeeStatusOptions,
|
||||
columns: paymentApplicationTableColumns,
|
||||
rows: [],
|
||||
selection: [],
|
||||
loading: false,
|
||||
syncLoading: false,
|
||||
resultSyncing: false,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
query: {},
|
||||
option: {},
|
||||
page: {
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
total: 0,
|
||||
},
|
||||
mkEnabled: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission']),
|
||||
visibleSearchFields() {
|
||||
return this.searchExpanded ? this.columns : this.columns.slice(0, 4);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.option = paymentApplicationOption({
|
||||
paymentTypeOptions: api.paymentTypeOptions,
|
||||
approvalStatusOptions: api.approvalStatusOptions,
|
||||
kingdeeStatusOptions: api.kingdeeStatusOptions,
|
||||
});
|
||||
this.loadOrganizationOptions();
|
||||
},
|
||||
mounted() {
|
||||
this.loadOrganizationOptions();
|
||||
this.loadTable();
|
||||
loadMkEnabled()
|
||||
.then(enabled => {
|
||||
this.mkEnabled = !!enabled;
|
||||
@@ -308,39 +190,52 @@ export default {
|
||||
const body = response?.data || response || {};
|
||||
return body?.data || body;
|
||||
},
|
||||
buildParams() {
|
||||
const params = { ...this.query };
|
||||
onLoad(page) {
|
||||
this.loading = true;
|
||||
api
|
||||
.getList(page.currentPage, page.pageSize, { ...this.query })
|
||||
.then(res => {
|
||||
const data = this.unwrapData(res);
|
||||
this.rows = data.records || [];
|
||||
this.page.total = Number(data.total || 0);
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
this.selection = [];
|
||||
this.$refs.crud?.toggleSelection?.();
|
||||
});
|
||||
},
|
||||
searchChange(params, done) {
|
||||
// 所属组织级联返回 id 路径数组,转成部门名称给后端
|
||||
this.normalizeOrganizationSearch(params, 'deptName');
|
||||
// 日期区间拆成后端需要的起止字段
|
||||
const range = params.applyDateRange || [];
|
||||
delete params.applyDateRange;
|
||||
if (range.length === 2) {
|
||||
params.applyStartDate = range[0];
|
||||
params.applyEndDate = range[1];
|
||||
}
|
||||
return params;
|
||||
this.query = {
|
||||
...params,
|
||||
applyStartDate: range[0] || '',
|
||||
applyEndDate: range[1] || '',
|
||||
};
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page);
|
||||
done();
|
||||
},
|
||||
async loadTable() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const data = this.unwrapData(
|
||||
await api.getList(this.page.current, this.page.size, this.buildParams())
|
||||
);
|
||||
this.rows = data.records || [];
|
||||
this.page.total = data.total || 0;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
searchReset() {
|
||||
this.query = {};
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
handleSearch() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
currentChange(currentPage) {
|
||||
this.page.currentPage = currentPage;
|
||||
},
|
||||
resetSearch() {
|
||||
this.query = emptyQuery();
|
||||
this.handleSearch();
|
||||
sizeChange(pageSize) {
|
||||
this.page.pageSize = pageSize;
|
||||
},
|
||||
handleSizeChange() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
refreshChange() {
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
selectionChange(list) {
|
||||
this.selection = list;
|
||||
},
|
||||
openCreate() {
|
||||
this.$router.push({ path: '/payment/payment-application/form', query: { mode: 'add' } });
|
||||
@@ -380,7 +275,7 @@ export default {
|
||||
await this.$confirm('确认删除该付款申请?', '提示', { type: 'warning' });
|
||||
await api.remove(row.id);
|
||||
this.$message.success('删除成功');
|
||||
this.loadTable();
|
||||
this.refreshChange();
|
||||
},
|
||||
async handleSubmit(row) {
|
||||
const previousApprovalStatus = row.approvalStatus || '';
|
||||
@@ -392,7 +287,7 @@ export default {
|
||||
approvalStatus: previousApprovalStatus,
|
||||
});
|
||||
this.$message.success('提交成功');
|
||||
this.loadTable();
|
||||
this.refreshChange();
|
||||
},
|
||||
async handleApprove(row) {
|
||||
const res = await api.approve({ id: row.id });
|
||||
@@ -402,7 +297,7 @@ export default {
|
||||
} else {
|
||||
this.$message.success(res.data.msg || '审批通过');
|
||||
}
|
||||
this.loadTable();
|
||||
this.refreshChange();
|
||||
},
|
||||
handleMkAudit(row) {
|
||||
if (!row?.id) {
|
||||
@@ -426,18 +321,18 @@ export default {
|
||||
const { value } = await this.$prompt('请输入驳回原因', '审批驳回');
|
||||
await api.returnBill({ id: row.id, reason: value });
|
||||
this.$message.success('已驳回');
|
||||
this.loadTable();
|
||||
this.refreshChange();
|
||||
},
|
||||
async handleVoid(row) {
|
||||
const { value } = await this.$prompt('请输入作废原因', '作废付款申请');
|
||||
await api.voidBill({ id: row.id, reason: value });
|
||||
this.$message.success('作废成功');
|
||||
this.loadTable();
|
||||
this.refreshChange();
|
||||
},
|
||||
async handleSyncRow(row) {
|
||||
const data = this.unwrapData(await api.syncKingdee(row.id));
|
||||
this.$message.success(`已生成金蝶付款单:${data}`);
|
||||
await this.loadTable();
|
||||
await this.refreshChange();
|
||||
this.warnAttachmentNotUploaded([row.id]);
|
||||
},
|
||||
async handleSync() {
|
||||
@@ -460,7 +355,7 @@ export default {
|
||||
} else {
|
||||
this.$message.success(`已同步${data.length}条付款申请`);
|
||||
}
|
||||
await this.loadTable();
|
||||
await this.refreshChange();
|
||||
this.warnAttachmentNotUploaded(rows.map(row => row.id));
|
||||
} finally {
|
||||
this.syncLoading = false;
|
||||
@@ -490,9 +385,15 @@ export default {
|
||||
this.resultSyncing = false;
|
||||
}
|
||||
},
|
||||
|
||||
handleExport() {
|
||||
const headers = this.columns.map(item => item.label);
|
||||
const rows = this.rows.map(row => this.columns.map(item => row[item.prop] || ''));
|
||||
const columns = this.option.column.filter(column => !column.hide && column.prop);
|
||||
const headers = columns.map(item => item.label);
|
||||
const rows = this.rows.map(row =>
|
||||
columns.map(item =>
|
||||
row[item.prop] === null || row[item.prop] === undefined ? '' : row[item.prop]
|
||||
)
|
||||
);
|
||||
const csv = [headers, ...rows]
|
||||
.map(item => item.map(value => `"${String(value).replaceAll('"', '""')}"`).join(','))
|
||||
.join('\n');
|
||||
@@ -542,54 +443,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.payment-page__search {
|
||||
padding: 12px 12px 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.payment-page__search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px 24px;
|
||||
}
|
||||
.payment-page__search :deep(.el-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.payment-page__search :deep(.el-input),
|
||||
.payment-page__search :deep(.el-select),
|
||||
.payment-page__search :deep(.el-date-editor) {
|
||||
width: 100%;
|
||||
}
|
||||
.payment-page__search-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.payment-page__table-panel {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.payment-page__toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.payment-page__toolbar-left {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.payment-page__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.payment-page__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.payment-page :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
}
|
||||
@@ -599,14 +452,4 @@ export default {
|
||||
:deep(.payment-page.basic-container .basic-container__card > .el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.payment-page__search-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.payment-page__search-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<basic-container class="claim-record-page">
|
||||
<section-card class="claim-record-page__search">
|
||||
<section-card v-if="searchVisible" class="claim-record-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="88px" @submit.prevent>
|
||||
<div class="claim-record-page__search-grid">
|
||||
<el-form-item label="认领通知单">
|
||||
@@ -96,9 +96,19 @@
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" border>
|
||||
<div class="claim-record-page__toolbar">
|
||||
<table-header-tools
|
||||
:columns="columns"
|
||||
v-model:visible-column-keys="visibleColumnKeys"
|
||||
v-model:table-size="tableSize"
|
||||
v-model:search-visible="searchVisible"
|
||||
@refresh="loadTable"
|
||||
/>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" :size="tableSize" border>
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in columns"
|
||||
v-for="column in visibleColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
@@ -157,6 +167,7 @@ import { mapGetters } from 'vuex';
|
||||
import * as api from '@/api/payment/receiptClaimRecord';
|
||||
import { receiptClaimRecordTableColumns } from '@/option/payment/receiptClaimRecord';
|
||||
import SectionCard from '@/components/section-card/main.vue';
|
||||
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
@@ -176,17 +187,20 @@ const emptyQuery = () => ({
|
||||
|
||||
export default {
|
||||
name: 'ReceiptClaimRecord',
|
||||
components: { SectionCard },
|
||||
components: { SectionCard, TableHeaderTools },
|
||||
data() {
|
||||
return {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
query: emptyQuery(),
|
||||
searchExpanded: false,
|
||||
searchVisible: true,
|
||||
tableSize: 'default',
|
||||
claimStatusOptions: api.claimStatusOptions,
|
||||
kingdeeBillStatusOptions: api.kingdeeBillStatusOptions,
|
||||
pushStatusOptions: api.pushStatusOptions,
|
||||
columns: receiptClaimRecordTableColumns,
|
||||
visibleColumnKeys: receiptClaimRecordTableColumns.map(column => column.prop),
|
||||
rows: [],
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
@@ -195,6 +209,9 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
visibleColumns() {
|
||||
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
||||
},
|
||||
isAdmin() {
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
@@ -314,9 +331,8 @@ export default {
|
||||
}
|
||||
.claim-record-page__toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.claim-record-page__pagination {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<basic-container class="receipt-flow-page">
|
||||
<section class="receipt-flow-page__search">
|
||||
<section v-if="searchVisible" class="receipt-flow-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="88px" @submit.prevent>
|
||||
<div class="receipt-flow-page__search-grid">
|
||||
<el-form-item label="认领通知单">
|
||||
@@ -97,33 +97,43 @@
|
||||
|
||||
<section class="receipt-flow-page__table-panel">
|
||||
<div class="receipt-flow-page__toolbar">
|
||||
<el-button
|
||||
v-if="hasPermission('receipt_flow_sync')"
|
||||
type="primary"
|
||||
:loading="syncing"
|
||||
@click="handleSync"
|
||||
>
|
||||
手动同步流水
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission('receipt_flow_claim')"
|
||||
type="primary"
|
||||
:disabled="!selectedRows.length"
|
||||
@click="openBatchClaim"
|
||||
>
|
||||
批量认领
|
||||
</el-button>
|
||||
<div class="receipt-flow-page__toolbar-left">
|
||||
<el-button
|
||||
v-if="hasPermission('receipt_flow_sync')"
|
||||
type="primary"
|
||||
:loading="syncing"
|
||||
@click="handleSync"
|
||||
>
|
||||
手动同步流水
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission('receipt_flow_claim')"
|
||||
type="primary"
|
||||
:disabled="!selectedRows.length"
|
||||
@click="openBatchClaim"
|
||||
>
|
||||
批量认领
|
||||
</el-button>
|
||||
</div>
|
||||
<table-header-tools
|
||||
:columns="columns"
|
||||
v-model:visible-column-keys="visibleColumnKeys"
|
||||
v-model:table-size="tableSize"
|
||||
v-model:search-visible="searchVisible"
|
||||
@refresh="loadTable"
|
||||
/>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
:size="tableSize"
|
||||
border
|
||||
@selection-change="selectedRows = $event"
|
||||
>
|
||||
<el-table-column type="selection" width="52" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in columns"
|
||||
v-for="column in visibleColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
@@ -271,6 +281,7 @@ import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import * as api from '@/api/payment/receiptFlow';
|
||||
import { receiptFlowTableColumns } from '@/option/payment/receiptFlow';
|
||||
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
@@ -291,6 +302,7 @@ const emptyQuery = () => ({
|
||||
|
||||
export default {
|
||||
name: 'ReceiptFlow',
|
||||
components: { TableHeaderTools },
|
||||
data() {
|
||||
return {
|
||||
ArrowDown,
|
||||
@@ -300,6 +312,9 @@ export default {
|
||||
pushStatusOptions: api.pushStatusOptions,
|
||||
kingdeeBillStatusOptions: api.kingdeeBillStatusOptions,
|
||||
columns: receiptFlowTableColumns,
|
||||
visibleColumnKeys: receiptFlowTableColumns.map(column => column.prop),
|
||||
searchVisible: true,
|
||||
tableSize: 'default',
|
||||
rows: [],
|
||||
selectedRows: [],
|
||||
loading: false,
|
||||
@@ -312,6 +327,9 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
visibleColumns() {
|
||||
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
||||
},
|
||||
isAdmin() {
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
@@ -507,11 +525,15 @@ export default {
|
||||
}
|
||||
.receipt-flow-page__toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.receipt-flow-page__toolbar-left {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.receipt-flow-page__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<el-option
|
||||
v-for="item in contracts"
|
||||
:key="item.id"
|
||||
:label="`${item.contractNo} / ${item.contractName}`"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -176,6 +176,17 @@
|
||||
<section-card title="结算明细">
|
||||
<template #extra>
|
||||
<div class="formal-editor__actions">
|
||||
<el-button
|
||||
v-if="hasPermission('formal_settlement_export')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="exportDetails"
|
||||
>
|
||||
导出
|
||||
</el-button>
|
||||
<el-button v-if="editable" type="primary" plain @click="openDetailDialog">
|
||||
选择结算明细
|
||||
</el-button>
|
||||
<el-button type="primary" text @click="detailCollapsed = !detailCollapsed">
|
||||
{{ detailCollapsed ? '展开明细' : '收起明细' }}
|
||||
</el-button>
|
||||
@@ -183,33 +194,46 @@
|
||||
</template>
|
||||
<el-form
|
||||
:model="detailQuery"
|
||||
inline
|
||||
label-position="right"
|
||||
label-width="88px"
|
||||
label-width="auto"
|
||||
class="formal-editor__detail-filter"
|
||||
@submit.prevent
|
||||
>
|
||||
<el-form-item label="单据号">
|
||||
<el-input v-model="detailQuery.documentNo" clearable @keyup.enter="handleDetailQuery" />
|
||||
<el-input
|
||||
v-model="detailQuery.documentNo"
|
||||
clearable
|
||||
placeholder="请输入"
|
||||
@keyup.enter="handleDetailQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="运单号">
|
||||
<el-input v-model="detailQuery.waybillNo" clearable @keyup.enter="handleDetailQuery" />
|
||||
<el-input
|
||||
v-model="detailQuery.waybillNo"
|
||||
clearable
|
||||
placeholder="请输入"
|
||||
@keyup.enter="handleDetailQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="批次号">
|
||||
<el-input v-model="detailQuery.batchNo" clearable @keyup.enter="handleDetailQuery" />
|
||||
<el-input
|
||||
v-model="detailQuery.batchNo"
|
||||
clearable
|
||||
placeholder="请输入"
|
||||
@keyup.enter="handleDetailQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="货物名称">
|
||||
<el-input v-model="detailQuery.cargoName" clearable @keyup.enter="handleDetailQuery" />
|
||||
<el-input
|
||||
v-model="detailQuery.cargoName"
|
||||
clearable
|
||||
placeholder="请输入"
|
||||
@keyup.enter="handleDetailQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="formal-editor__detail-filter-actions">
|
||||
<el-button type="primary" @click="handleDetailQuery">查询</el-button>
|
||||
<el-button @click="resetDetailQuery">重置</el-button>
|
||||
<el-button type="primary" :disabled="!details.length" @click="exportDetails">
|
||||
导出
|
||||
</el-button>
|
||||
<el-button v-if="editable" type="primary" @click="openDetailDialog">
|
||||
选择结算明细
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-show="!detailCollapsed" :data="pagedDetails" border>
|
||||
@@ -299,15 +323,23 @@
|
||||
<span v-else>{{ attachmentTypeName(row.type) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文件名" min-width="220" align="left" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="220"
|
||||
align="left"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
:disabled="!getAttachmentUrl(row)"
|
||||
type="primary"
|
||||
@click="previewAttachment(row)"
|
||||
>
|
||||
{{ row.originalName || row.name || '-' }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
:class="{ 'is-disabled': !getAttachmentUrl(row) }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="getAttachmentUrl(row) && previewAttachment(row)"
|
||||
@keydown.enter="getAttachmentUrl(row) && previewAttachment(row)"
|
||||
@keydown.space.prevent="getAttachmentUrl(row) && previewAttachment(row)"
|
||||
>{{ row.originalName || row.name || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploadUserName" label="上传人" min-width="120" align="center">
|
||||
@@ -610,9 +642,10 @@
|
||||
</section-card>
|
||||
</div>
|
||||
<template v-if="!pageMode" #footer>
|
||||
<el-button v-if="editable" @click="handleSyncPlaceholder">同步</el-button>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button
|
||||
v-if="editable && !recordId"
|
||||
v-if="editable"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="draftSaving"
|
||||
@@ -625,9 +658,10 @@
|
||||
>
|
||||
</template>
|
||||
<div v-if="pageMode && !publicMode" class="formal-editor__page-actions">
|
||||
<el-button v-if="editable" @click="handleSyncPlaceholder">同步</el-button>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button
|
||||
v-if="editable && !recordId"
|
||||
v-if="editable"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="draftSaving"
|
||||
@@ -966,6 +1000,7 @@ import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { h } from 'vue';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { downloadFileByUrl } from '@/utils/util';
|
||||
import {
|
||||
downloadKingdeeInvoiceFile,
|
||||
@@ -974,6 +1009,7 @@ import {
|
||||
} from '@/utils/kingdee-attachment';
|
||||
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
export default {
|
||||
name: 'FormalSettlementEditor',
|
||||
@@ -1110,7 +1146,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo']),
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
visible: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
@@ -1138,6 +1174,9 @@ export default {
|
||||
editable() {
|
||||
return !this.readonly;
|
||||
},
|
||||
isAdmin() {
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
title() {
|
||||
return this.readonly ? '查看正式结算单' : this.recordId ? '编辑正式结算单' : '新增正式结算单';
|
||||
},
|
||||
@@ -1203,6 +1242,10 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission?.[code], false);
|
||||
},
|
||||
async loadFormalDetail(id) {
|
||||
const response = this.publicMode
|
||||
? await getMkPublicDetail('formal-settlement', id)
|
||||
@@ -2506,7 +2549,11 @@ export default {
|
||||
remark: this.form.remark,
|
||||
};
|
||||
},
|
||||
handleSyncPlaceholder() {
|
||||
this.$message.info('开发中');
|
||||
},
|
||||
async handleDraftSave() {
|
||||
// 保存草稿:不做表单校验,直接落草稿并返回列表刷新
|
||||
this.draftSaving = true;
|
||||
try {
|
||||
await save({
|
||||
@@ -2514,8 +2561,8 @@ export default {
|
||||
approvalStatus: 'draft',
|
||||
});
|
||||
this.$message.success('保存成功');
|
||||
this.visible = false;
|
||||
this.$emit('success');
|
||||
this.visible = false;
|
||||
} finally {
|
||||
this.draftSaving = false;
|
||||
}
|
||||
@@ -2819,20 +2866,24 @@ export default {
|
||||
}
|
||||
.formal-editor__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.formal-editor__detail-filter {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
|
||||
align-items: center;
|
||||
gap: 8px 16px;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px 12px 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.formal-editor__detail-filter-actions {
|
||||
grid-column: 1 / -1;
|
||||
width: auto;
|
||||
margin-left: 0;
|
||||
white-space: nowrap;
|
||||
justify-self: end;
|
||||
}
|
||||
.formal-editor__page-actions {
|
||||
@@ -2862,7 +2913,7 @@ export default {
|
||||
gap: 8px;
|
||||
}
|
||||
.formal-editor :deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.formal-editor :deep(.el-form-item__content) {
|
||||
line-height: normal;
|
||||
@@ -2963,9 +3014,29 @@ export default {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* 结算明细筛选区:覆盖全局 el-form-item 24px 行距,保证筛选控件与「查询/重置」同行居中对齐 */
|
||||
.formal-editor__detail-filter :deep(.el-form-item) {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.formal-editor__detail-filter :deep(.el-form-item__content) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.formal-editor__detail-filter :deep(.el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
.formal-editor__detail-filter-actions :deep(.el-form-item__content) {
|
||||
flex: none;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.formal-editor__detail-filter {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr)) auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -40,16 +40,20 @@
|
||||
>导出</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="fs-table-panel__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button :icon="Refresh" text @click="$emit('refresh')" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<settlement-table-tools
|
||||
:columns="columnSettingOptions"
|
||||
v-model:visible-column-keys="visibleColumnKeys"
|
||||
v-model:table-size="tableSize"
|
||||
:search-visible="searchVisible"
|
||||
@update:search-visible="$emit('update:searchVisible', $event)"
|
||||
@refresh="$emit('refresh')"
|
||||
/>
|
||||
</div>
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
:size="tableSize"
|
||||
border
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
@@ -58,7 +62,7 @@
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in columns"
|
||||
v-for="column in visibleColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
@@ -126,15 +130,16 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Refresh } from '@element-plus/icons-vue';
|
||||
import {
|
||||
invoiceStatusName,
|
||||
kingdeeSyncStatusOptions,
|
||||
paymentStatusOptions,
|
||||
} from '@/option/settlement/formalSettlementSearch';
|
||||
import SettlementTableTools from './settlement-table-tools.vue';
|
||||
|
||||
export default {
|
||||
name: 'FormalSettlementTablePanel',
|
||||
components: { SettlementTableTools },
|
||||
props: {
|
||||
rows: { type: Array, default: () => [] },
|
||||
columns: { type: Array, default: () => [] },
|
||||
@@ -143,6 +148,7 @@ export default {
|
||||
page: { type: Object, required: true },
|
||||
hasPermission: { type: Function, required: true },
|
||||
mkEnabled: { type: Boolean, default: false },
|
||||
searchVisible: { type: Boolean, default: true },
|
||||
},
|
||||
emits: [
|
||||
'create',
|
||||
@@ -154,14 +160,47 @@ export default {
|
||||
'page-change',
|
||||
'action',
|
||||
'update:selection',
|
||||
'update:searchVisible',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
Refresh,
|
||||
selection: [],
|
||||
tableSize: 'default',
|
||||
visibleColumnKeys: [],
|
||||
knownColumnProps: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
columnSettingOptions() {
|
||||
return this.columns.map(column => ({
|
||||
prop: column.prop,
|
||||
label: column.label,
|
||||
}));
|
||||
},
|
||||
visibleColumns() {
|
||||
if (!this.visibleColumnKeys.length) return this.columns;
|
||||
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
columns: {
|
||||
immediate: true,
|
||||
handler(columns) {
|
||||
const props = columns.map(column => column.prop);
|
||||
if (!this.visibleColumnKeys.length) {
|
||||
this.visibleColumnKeys = props;
|
||||
this.knownColumnProps = props;
|
||||
return;
|
||||
}
|
||||
const visible = new Set(this.visibleColumnKeys);
|
||||
const known = new Set(this.knownColumnProps || []);
|
||||
props.forEach(prop => {
|
||||
if (!known.has(prop)) visible.add(prop);
|
||||
});
|
||||
this.knownColumnProps = props;
|
||||
this.visibleColumnKeys = props.filter(prop => visible.has(prop));
|
||||
},
|
||||
},
|
||||
rows() {
|
||||
this.selection = [];
|
||||
this.$refs.tableRef?.clearSelection();
|
||||
@@ -202,7 +241,11 @@ export default {
|
||||
actions.push({ type: 'delete', label: '删除', danger: true });
|
||||
if (this.hasPermission('formal_settlement_submit') && this.isEditable(row))
|
||||
actions.push({ type: 'submit', label: '提交' });
|
||||
if (this.mkEnabled && this.hasPermission('formal_settlement_approve') && row.approvalStatus === 'reviewing') {
|
||||
if (
|
||||
this.mkEnabled &&
|
||||
this.hasPermission('formal_settlement_approve') &&
|
||||
['reviewing', 'approved'].includes(row.approvalStatus)
|
||||
) {
|
||||
actions.push({ type: 'mkAudit', label: '流程' });
|
||||
}
|
||||
if (!this.mkEnabled && this.hasPermission('formal_settlement_approve') && row.approvalStatus === 'reviewing') {
|
||||
@@ -271,11 +314,6 @@ export default {
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.fs-table-panel__toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.fs-table-panel__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<el-form-item :label="field.label" :prop="field.prop">
|
||||
<el-input
|
||||
v-if="field.type === 'contract'"
|
||||
v-model="form.contractName"
|
||||
:model-value="formatContractOptionLabel(form)"
|
||||
readonly
|
||||
:disabled="!editable || details.length > 0"
|
||||
placeholder="请选择合同"
|
||||
@@ -354,11 +354,21 @@
|
||||
<span v-else>{{ attachmentTypeName(row.type) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文件名称" min-width="260" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名称"
|
||||
min-width="260"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220">
|
||||
@@ -963,6 +973,7 @@ import {
|
||||
settlementDetailColumns,
|
||||
settlementSummaryColumns,
|
||||
} from '@/option/settlement/preSettlementTable';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { downloadFileByUrl } from '@/utils/util';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import * as XLSX from 'xlsx';
|
||||
@@ -1266,6 +1277,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission?.[code], false);
|
||||
},
|
||||
@@ -3003,7 +3015,7 @@ export default {
|
||||
}
|
||||
|
||||
:deep(.pre-settlement-editor .el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:deep(.pre-settlement-editor .el-table),
|
||||
|
||||
@@ -193,11 +193,21 @@
|
||||
<el-table :data="attachments" border @selection-change="selectedAttachments = $event">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column label="文件名" min-width="220" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="240">
|
||||
@@ -638,7 +648,7 @@ export default {
|
||||
gap: 8px;
|
||||
}
|
||||
.settlement-adjustment-editor :deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.settlement-adjustment-editor :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<table-header-tools v-bind="$attrs" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||||
|
||||
export default {
|
||||
name: 'SettlementTableTools',
|
||||
components: { TableHeaderTools },
|
||||
inheritAttrs: false,
|
||||
};
|
||||
</script>
|
||||
@@ -2072,7 +2072,7 @@ export default {
|
||||
display: none;
|
||||
}
|
||||
.reconciliation-editor :deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.reconciliation-editor :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
|
||||
@@ -10,22 +10,26 @@
|
||||
>
|
||||
<el-button type="primary" plain @click="$emit('export')">导出</el-button>
|
||||
</div>
|
||||
<div class="tr-table-panel__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button :icon="Refresh" text @click="$emit('refresh')" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<settlement-table-tools
|
||||
:columns="columnSettingOptions"
|
||||
v-model:visible-column-keys="visibleColumnKeys"
|
||||
v-model:table-size="tableSize"
|
||||
:search-visible="searchVisible"
|
||||
@update:search-visible="$emit('update:searchVisible', $event)"
|
||||
@refresh="$emit('refresh')"
|
||||
/>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
:size="tableSize"
|
||||
border
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in columns"
|
||||
v-for="column in visibleColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
@@ -73,24 +77,68 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Refresh } from '@element-plus/icons-vue';
|
||||
import SettlementTableTools from './settlement-table-tools.vue';
|
||||
|
||||
export default {
|
||||
name: 'TransportReconciliationTablePanel',
|
||||
components: { SettlementTableTools },
|
||||
props: {
|
||||
rows: { type: Array, default: () => [] },
|
||||
columns: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
page: { type: Object, required: true },
|
||||
hasPermission: { type: Function, required: true },
|
||||
searchVisible: { type: Boolean, default: true },
|
||||
},
|
||||
emits: ['create', 'export', 'refresh', 'page-change', 'update:selection', 'action'],
|
||||
emits: [
|
||||
'create',
|
||||
'export',
|
||||
'refresh',
|
||||
'page-change',
|
||||
'update:selection',
|
||||
'action',
|
||||
'update:searchVisible',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
Refresh,
|
||||
selection: [],
|
||||
tableSize: 'default',
|
||||
visibleColumnKeys: [],
|
||||
knownColumnProps: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
columnSettingOptions() {
|
||||
return this.columns.map(column => ({
|
||||
prop: column.prop,
|
||||
label: column.label,
|
||||
}));
|
||||
},
|
||||
visibleColumns() {
|
||||
if (!this.visibleColumnKeys.length) return this.columns;
|
||||
return this.columns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
columns: {
|
||||
immediate: true,
|
||||
handler(columns) {
|
||||
const props = columns.map(column => column.prop);
|
||||
if (!this.visibleColumnKeys.length) {
|
||||
this.visibleColumnKeys = props;
|
||||
this.knownColumnProps = props;
|
||||
return;
|
||||
}
|
||||
const visible = new Set(this.visibleColumnKeys);
|
||||
const known = new Set(this.knownColumnProps || []);
|
||||
props.forEach(prop => {
|
||||
if (!known.has(prop)) visible.add(prop);
|
||||
});
|
||||
this.knownColumnProps = props;
|
||||
this.visibleColumnKeys = props.filter(prop => visible.has(prop));
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleSelectionChange(value) {
|
||||
this.selection = value;
|
||||
@@ -127,11 +175,6 @@ export default {
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.tr-table-panel__toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.tr-table-panel__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
:record-id="recordId"
|
||||
:readonly="readonly"
|
||||
:initial-data="editorInitialData"
|
||||
@success="markListNeedRefresh"
|
||||
/>
|
||||
</basic-container>
|
||||
</template>
|
||||
@@ -16,6 +17,8 @@
|
||||
import FormalSettlementEditor from './components/formal-settlement-editor.vue';
|
||||
import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer';
|
||||
|
||||
const LIST_REFRESH_KEY = 'formal-settlement-list-refresh';
|
||||
|
||||
export default {
|
||||
name: 'FormalSettlementForm',
|
||||
components: { FormalSettlementEditor },
|
||||
@@ -54,6 +57,9 @@ export default {
|
||||
this.syncTagTitle();
|
||||
},
|
||||
methods: {
|
||||
markListNeedRefresh() {
|
||||
sessionStorage.setItem(LIST_REFRESH_KEY, '1');
|
||||
},
|
||||
goBack() {
|
||||
removeSettlementTransfer(this.$route.query.transferToken);
|
||||
this.$router.$avueRouter.closeTag();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<basic-container class="formal-page">
|
||||
<section class="formal-page__search">
|
||||
<section v-show="searchVisible" class="formal-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="88px" @submit.prevent>
|
||||
<div class="formal-page__search-grid">
|
||||
<el-form-item v-for="field in visibleSearchFields" :key="field.prop" :label="field.label">
|
||||
@@ -66,6 +66,7 @@
|
||||
:has-permission="hasPermissionFn"
|
||||
:sync-loading="syncLoading"
|
||||
:mk-enabled="mkEnabled"
|
||||
v-model:search-visible="searchVisible"
|
||||
@create="openCreate"
|
||||
@payment="openPaymentDialog"
|
||||
@sync="handleSync"
|
||||
@@ -86,6 +87,7 @@
|
||||
:has-permission="hasPermissionFn"
|
||||
:sync-loading="syncLoading"
|
||||
:mk-enabled="mkEnabled"
|
||||
v-model:search-visible="searchVisible"
|
||||
@create="openCreate"
|
||||
@payment="openPaymentDialog"
|
||||
@sync="handleSync"
|
||||
@@ -214,6 +216,7 @@ export default {
|
||||
ArrowUp,
|
||||
query: emptyQuery(),
|
||||
searchExpanded: false,
|
||||
searchVisible: true,
|
||||
activeSettlementType: 'payable',
|
||||
searchFields: formalSettlementSearchFields,
|
||||
columns: formalSettlementTableColumns,
|
||||
@@ -262,6 +265,12 @@ export default {
|
||||
this.mkEnabled = false;
|
||||
});
|
||||
},
|
||||
activated() {
|
||||
if (sessionStorage.getItem('formal-settlement-list-refresh') === '1') {
|
||||
sessionStorage.removeItem('formal-settlement-list-refresh');
|
||||
this.loadTable();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
return this.permission?.[code] !== false;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<basic-container class="pre-settlement-page">
|
||||
<section class="pre-settlement-page__search">
|
||||
<section v-show="searchVisible" class="pre-settlement-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="88px" @submit.prevent>
|
||||
<div class="pre-settlement-page__search-grid">
|
||||
<template v-for="field in visibleSearchFields" :key="field.prop">
|
||||
@@ -94,16 +94,19 @@
|
||||
导出
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="pre-settlement-page__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button :icon="Refresh" text @click="loadTable" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<settlement-table-tools
|
||||
:columns="columnSettingOptions"
|
||||
v-model:visible-column-keys="visibleColumnKeys"
|
||||
v-model:table-size="tableSize"
|
||||
v-model:search-visible="searchVisible"
|
||||
@refresh="loadTable"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
:size="tableSize"
|
||||
border
|
||||
class="pre-settlement-page__table"
|
||||
@selection-change="handleSelectionChange"
|
||||
@@ -111,7 +114,7 @@
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in tableColumns"
|
||||
v-for="column in visibleTableColumns"
|
||||
:key="column.prop"
|
||||
:prop="column.prop"
|
||||
:label="column.label"
|
||||
@@ -185,7 +188,7 @@
|
||||
v-if="
|
||||
mkEnabled &&
|
||||
hasPermission('pre_settlement_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
['reviewing', 'approved'].includes(row.approvalStatus)
|
||||
"
|
||||
type="primary"
|
||||
@click="handleMkAudit(row)"
|
||||
@@ -285,7 +288,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArrowDown, ArrowUp, Refresh } from '@element-plus/icons-vue';
|
||||
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import {
|
||||
approve,
|
||||
@@ -306,6 +309,7 @@ import { downloadFile } from '@/utils/util';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import PreSettlementEditor from './components/pre-settlement-editor.vue';
|
||||
import SettlementTableTools from './components/settlement-table-tools.vue';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
@@ -323,18 +327,20 @@ const emptyQuery = () => ({
|
||||
|
||||
export default {
|
||||
name: 'PreSettlement',
|
||||
components: { PreSettlementEditor },
|
||||
components: { PreSettlementEditor, SettlementTableTools },
|
||||
mixins: [organizationSearch],
|
||||
data() {
|
||||
return {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Refresh,
|
||||
loading: false,
|
||||
query: emptyQuery(),
|
||||
searchFields: preSettlementSearchFields,
|
||||
searchExpanded: false,
|
||||
searchVisible: true,
|
||||
tableSize: 'default',
|
||||
tableColumns: preSettlementTableColumns,
|
||||
visibleColumnKeys: preSettlementTableColumns.map(column => column.prop),
|
||||
rows: [],
|
||||
selection: [],
|
||||
page: {
|
||||
@@ -365,6 +371,15 @@ export default {
|
||||
visibleSearchFields() {
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||||
},
|
||||
columnSettingOptions() {
|
||||
return this.tableColumns.map(column => ({
|
||||
prop: column.prop,
|
||||
label: column.label,
|
||||
}));
|
||||
},
|
||||
visibleTableColumns() {
|
||||
return this.tableColumns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'$route.query.detailNo'(detailNo) {
|
||||
@@ -776,7 +791,6 @@ export default {
|
||||
}
|
||||
|
||||
&__toolbar-left,
|
||||
&__toolbar-right,
|
||||
&__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<basic-container class="settlement-detail-page">
|
||||
<section class="settlement-detail-page__search">
|
||||
<section v-show="searchVisible" class="settlement-detail-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="88px" @submit.prevent>
|
||||
<div class="settlement-detail-page__search-grid">
|
||||
<template v-for="field in visibleSearchFields" :key="field.prop">
|
||||
@@ -44,19 +44,19 @@
|
||||
</el-button>
|
||||
<el-button type="primary" plain @click="handleExport">导出</el-button>
|
||||
</div>
|
||||
<div class="settlement-detail-page__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button :icon="Refresh" text @click="loadTable" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="列设置" placement="top">
|
||||
<el-button :icon="Setting" text @click="noop" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<settlement-table-tools
|
||||
:columns="columnSettingOptions"
|
||||
v-model:visible-column-keys="visibleColumnKeys"
|
||||
v-model:table-size="tableSize"
|
||||
v-model:search-visible="searchVisible"
|
||||
@refresh="loadTable"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
:size="tableSize"
|
||||
border
|
||||
class="settlement-detail-page__table"
|
||||
@selection-change="selectionChange"
|
||||
@@ -64,7 +64,7 @@
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in displayTableColumns"
|
||||
v-for="column in visibleTableColumns"
|
||||
:key="column.prop"
|
||||
:prop="column.prop"
|
||||
:label="column.label"
|
||||
@@ -164,8 +164,8 @@
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default>
|
||||
<el-link type="primary" @click="openChangeRecordsDialog">查看详情</el-link>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openChangeRecordsDialog(row)">查看详情</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -938,7 +938,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArrowDown, ArrowUp, Close, Refresh, Setting } from '@element-plus/icons-vue';
|
||||
import { ArrowDown, ArrowUp, Close } from '@element-plus/icons-vue';
|
||||
import {
|
||||
changeRecordColumns,
|
||||
feeDetailBaseColumns,
|
||||
@@ -950,6 +950,7 @@ import {
|
||||
tableColumns,
|
||||
transferSearchFields,
|
||||
} from '@/option/settlement/receivable-payable-detail';
|
||||
import SettlementTableTools from './components/settlement-table-tools.vue';
|
||||
import * as api from '@/api/settlement/receivable-payable-detail';
|
||||
import { getList as getContractList } from '@/api/business/contract-manage';
|
||||
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
||||
@@ -957,6 +958,7 @@ import { getContractOptions as getSettlementContractOptions } from '@/api/settle
|
||||
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { createSettlementTransfer } from '@/utils/settlement-transfer';
|
||||
import {
|
||||
@@ -988,6 +990,7 @@ const ADJUST_BILLING_TYPE_MAP = {
|
||||
};
|
||||
|
||||
export default {
|
||||
components: { SettlementTableTools },
|
||||
props: {
|
||||
settlementType: {
|
||||
type: String,
|
||||
@@ -999,8 +1002,6 @@ export default {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Close,
|
||||
Refresh,
|
||||
Setting,
|
||||
searchFields,
|
||||
tableColumns,
|
||||
tableFeeItemNames: [],
|
||||
@@ -1009,6 +1010,10 @@ export default {
|
||||
generateWaybillColumns,
|
||||
loading: false,
|
||||
searchExpanded: false,
|
||||
searchVisible: true,
|
||||
tableSize: 'default',
|
||||
visibleColumnKeys: [],
|
||||
knownColumnProps: [],
|
||||
query: {},
|
||||
rows: [],
|
||||
selection: [],
|
||||
@@ -1044,6 +1049,7 @@ export default {
|
||||
priceUnitLoading: false,
|
||||
priceUnitRequest: null,
|
||||
changeRows: [],
|
||||
changeBatchRecords: [],
|
||||
changePage: { current: 1, size: 10, total: 0 },
|
||||
latestChangeRows: [],
|
||||
latestChangeLoading: false,
|
||||
@@ -1103,14 +1109,14 @@ export default {
|
||||
const contract = this.contractOptions.find(
|
||||
item => String(item.id) === String(this.generateQuery.contractId)
|
||||
);
|
||||
return contract?.contractName || '';
|
||||
return formatContractOptionLabel(contract);
|
||||
},
|
||||
updateContractName() {
|
||||
const contractId = this.updateFeeForm.contractId;
|
||||
const contract = [...this.updateContractOptions, ...this.contractOptions].find(
|
||||
item => String(item.id) === String(contractId)
|
||||
);
|
||||
return contract?.contractName || '';
|
||||
return formatContractOptionLabel(contract);
|
||||
},
|
||||
generateContractCategory() {
|
||||
return this.settlementType === 'payable' ? '承运商合同' : '客户合同';
|
||||
@@ -1168,6 +1174,18 @@ export default {
|
||||
columns.splice(totalIndex, 0, ...dynamicColumns);
|
||||
return columns;
|
||||
},
|
||||
columnSettingOptions() {
|
||||
return this.displayTableColumns.map(column => ({
|
||||
prop: column.prop,
|
||||
label: column.label,
|
||||
}));
|
||||
},
|
||||
visibleTableColumns() {
|
||||
if (!this.visibleColumnKeys.length) return this.displayTableColumns;
|
||||
return this.displayTableColumns.filter(column =>
|
||||
this.visibleColumnKeys.includes(column.prop)
|
||||
);
|
||||
},
|
||||
previewColumns() {
|
||||
return [...generatePreviewColumns, ...this.dynamicFeeColumns, ...feeDetailTailColumns];
|
||||
},
|
||||
@@ -1190,6 +1208,27 @@ export default {
|
||||
'$route.fullPath'() {
|
||||
this.openRouteDetail();
|
||||
},
|
||||
displayTableColumns: {
|
||||
immediate: true,
|
||||
handler(columns) {
|
||||
const props = columns.map(column => column.prop);
|
||||
if (!this.visibleColumnKeys.length) {
|
||||
this.visibleColumnKeys = props;
|
||||
this.knownColumnProps = [...props];
|
||||
return;
|
||||
}
|
||||
const known = new Set(this.knownColumnProps);
|
||||
const visible = new Set(this.visibleColumnKeys);
|
||||
props.forEach(prop => {
|
||||
if (!known.has(prop)) {
|
||||
known.add(prop);
|
||||
visible.add(prop);
|
||||
}
|
||||
});
|
||||
this.knownColumnProps = [...known];
|
||||
this.visibleColumnKeys = props.filter(prop => visible.has(prop));
|
||||
},
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.loadTransportTypeOptions();
|
||||
@@ -1992,34 +2031,64 @@ export default {
|
||||
this.latestChangeLoading = true;
|
||||
this.latestChangeRows = [];
|
||||
try {
|
||||
const res = await api.getChangeRecords(1, 1, {
|
||||
const res = await api.getChangeRecords(1, 1000, {
|
||||
detailId: this.detailDialog.row.id,
|
||||
});
|
||||
const data = this.unwrapPage(res);
|
||||
this.latestChangeRows = (data.records || []).slice(0, 1);
|
||||
this.latestChangeRows = this.buildChangeRecordBatches(data.records || []);
|
||||
} finally {
|
||||
this.latestChangeLoading = false;
|
||||
}
|
||||
},
|
||||
openChangeRecordsDialog() {
|
||||
buildChangeRecordBatches(records = []) {
|
||||
const batchMap = new Map();
|
||||
records.forEach(record => {
|
||||
const batchKey = record.adjustTime || '';
|
||||
if (!batchMap.has(batchKey)) {
|
||||
batchMap.set(batchKey, []);
|
||||
}
|
||||
batchMap.get(batchKey).push(record);
|
||||
});
|
||||
return Array.from(batchMap.entries()).map(([batchKey, batchRecords]) => {
|
||||
const first = batchRecords[0] || {};
|
||||
const lineNos = [
|
||||
...new Set(batchRecords.map(item => item.lineNo).filter(value => value !== undefined && value !== null && value !== '')),
|
||||
];
|
||||
const cargoNames = [
|
||||
...new Set(
|
||||
batchRecords.map(item => item.cargoName).filter(value => value !== undefined && value !== null && value !== '')
|
||||
),
|
||||
];
|
||||
const changeContents = batchRecords
|
||||
.map(item => item.changeContent)
|
||||
.filter(value => value !== undefined && value !== null && value !== '');
|
||||
return {
|
||||
...first,
|
||||
lineNo: lineNos.join('、'),
|
||||
cargoName: cargoNames.join('、'),
|
||||
changeContent: changeContents.join(';'),
|
||||
adjustUserName: first.adjustUserName,
|
||||
adjustReason: first.adjustReason,
|
||||
adjustTime: first.adjustTime || batchKey,
|
||||
_batchKey: batchKey,
|
||||
_batchRecords: batchRecords,
|
||||
};
|
||||
});
|
||||
},
|
||||
openChangeRecordsDialog(row) {
|
||||
if (!this.detailDialog.row) return;
|
||||
this.changeBatchRecords = Array.isArray(row?._batchRecords) ? row._batchRecords : [];
|
||||
this.changeRecordsDialog.visible = true;
|
||||
this.changePage.current = 1;
|
||||
this.loadChangeRecords();
|
||||
},
|
||||
async loadChangeRecords() {
|
||||
if (!this.detailDialog.row) return;
|
||||
this.changeRecordsDialog.loading = true;
|
||||
try {
|
||||
const res = await api.getChangeRecords(this.changePage.current, this.changePage.size, {
|
||||
detailId: this.detailDialog.row.id,
|
||||
});
|
||||
const data = this.unwrapPage(res);
|
||||
this.changeRows = data.records || [];
|
||||
this.changePage.total = data.total || 0;
|
||||
} finally {
|
||||
this.changeRecordsDialog.loading = false;
|
||||
}
|
||||
loadChangeRecords() {
|
||||
const records = this.changeBatchRecords || [];
|
||||
const size = Number(this.changePage.size) || 10;
|
||||
const current = Number(this.changePage.current) || 1;
|
||||
const fromIndex = Math.max(0, (current - 1) * size);
|
||||
this.changeRows = records.slice(fromIndex, fromIndex + size);
|
||||
this.changePage.total = records.length;
|
||||
},
|
||||
handleChangeSizeChange() {
|
||||
this.changePage.current = 1;
|
||||
@@ -2624,6 +2693,10 @@ export default {
|
||||
return this.formatCell(row?.[prop]);
|
||||
},
|
||||
formatFeeDetailCell(row, prop) {
|
||||
if (prop === 'transportQuantityText' || prop === 'transportQuantity') {
|
||||
if (this.isBlankTransportQuantity(row)) return '-';
|
||||
return this.formatDetailCell(row, prop);
|
||||
}
|
||||
if (prop !== 'originalAmountText') return this.formatDetailCell(row, prop);
|
||||
|
||||
const adjustAmount = Number(row?.adjustAmount || 0);
|
||||
@@ -2642,6 +2715,17 @@ export default {
|
||||
formatCell(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
isBlankTransportQuantity(row = {}) {
|
||||
const quantity = row.transportQuantity;
|
||||
if (quantity !== null && quantity !== undefined && quantity !== '') {
|
||||
const amount = Number(quantity);
|
||||
return !Number.isFinite(amount) || amount === 0 || amount === -1;
|
||||
}
|
||||
const text = String(row.transportQuantityText || '').trim();
|
||||
if (!text || text === '-') return true;
|
||||
const amount = Number(text.split(/\s+/)[0]);
|
||||
return Number.isFinite(amount) && (amount === 0 || amount === -1);
|
||||
},
|
||||
parseJson(value) {
|
||||
if (!value) return [];
|
||||
try {
|
||||
@@ -2663,7 +2747,6 @@ export default {
|
||||
if (Array.isArray(data?.data?.records)) return data.data.records;
|
||||
return [];
|
||||
},
|
||||
noop() {},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -2717,8 +2800,7 @@ export default {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.settlement-detail-page__toolbar-left,
|
||||
.settlement-detail-page__toolbar-right {
|
||||
.settlement-detail-page__toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -2758,7 +2840,7 @@ export default {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<basic-container class="settlement-adjustment-page">
|
||||
<section class="settlement-adjustment-page__search">
|
||||
<section v-show="searchVisible" class="settlement-adjustment-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="88px" @submit.prevent>
|
||||
<div class="settlement-adjustment-page__search-grid">
|
||||
<el-form-item v-for="field in visibleSearchFields" :key="field.prop" :label="field.label">
|
||||
@@ -55,13 +55,22 @@
|
||||
type="primary"
|
||||
@click="openCreate"
|
||||
>新增</el-button
|
||||
><div class="settlement-adjustment-page__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button :icon="Refresh" text @click="loadTable" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
>
|
||||
<settlement-table-tools
|
||||
:columns="columnSettingOptions"
|
||||
v-model:visible-column-keys="visibleColumnKeys"
|
||||
v-model:table-size="tableSize"
|
||||
v-model:search-visible="searchVisible"
|
||||
@refresh="loadTable"
|
||||
/>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" border @selection-change="selection = $event">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
:size="tableSize"
|
||||
border
|
||||
@selection-change="selection = $event"
|
||||
>
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" /><el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
@@ -70,7 +79,7 @@
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-for="column in tableColumns"
|
||||
v-for="column in visibleTableColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
@@ -165,13 +174,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArrowDown, ArrowUp, Refresh } from '@element-plus/icons-vue';
|
||||
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import * as api from '@/api/settlement/settlementAdjustment';
|
||||
import { settlementAdjustmentSearchFields } from '@/option/settlement/settlementAdjustmentSearch';
|
||||
import organizationSearch from '@/mixins/organization-search';
|
||||
import { settlementAdjustmentTableColumns } from '@/option/settlement/settlementAdjustmentTable';
|
||||
import SettlementAdjustmentEditor from './components/settlement-adjustment-editor.vue';
|
||||
import SettlementTableTools from './components/settlement-table-tools.vue';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
@@ -186,17 +196,19 @@ const emptyQuery = () => ({
|
||||
});
|
||||
export default {
|
||||
name: 'SettlementAdjustment',
|
||||
components: { SettlementAdjustmentEditor },
|
||||
components: { SettlementAdjustmentEditor, SettlementTableTools },
|
||||
mixins: [organizationSearch],
|
||||
data: () => ({
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Refresh,
|
||||
loading: false,
|
||||
query: emptyQuery(),
|
||||
searchFields: settlementAdjustmentSearchFields,
|
||||
searchExpanded: false,
|
||||
searchVisible: true,
|
||||
tableSize: 'default',
|
||||
tableColumns: settlementAdjustmentTableColumns,
|
||||
visibleColumnKeys: settlementAdjustmentTableColumns.map(column => column.prop),
|
||||
rows: [],
|
||||
selection: [],
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
@@ -207,6 +219,15 @@ export default {
|
||||
visibleSearchFields() {
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||||
},
|
||||
columnSettingOptions() {
|
||||
return this.tableColumns.map(column => ({
|
||||
prop: column.prop,
|
||||
label: column.label,
|
||||
}));
|
||||
},
|
||||
visibleTableColumns() {
|
||||
return this.tableColumns.filter(column => this.visibleColumnKeys.includes(column.prop));
|
||||
},
|
||||
isAdmin() {
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
@@ -359,11 +380,6 @@ export default {
|
||||
align-items: center;
|
||||
min-height: 56px;
|
||||
}
|
||||
&__toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
&__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<basic-container class="reconciliation-page">
|
||||
<section class="reconciliation-page__search">
|
||||
<section v-show="searchVisible" class="reconciliation-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="88px" @submit.prevent>
|
||||
<div class="reconciliation-page__search-grid">
|
||||
<el-form-item v-for="field in visibleSearchFields" :key="field.prop" :label="field.label">
|
||||
@@ -54,6 +54,7 @@
|
||||
:loading="loading"
|
||||
:page="page"
|
||||
:has-permission="hasPermission"
|
||||
v-model:search-visible="searchVisible"
|
||||
@create="openCreate"
|
||||
@export="handleExport"
|
||||
@refresh="loadTable"
|
||||
@@ -69,6 +70,7 @@
|
||||
:loading="loading"
|
||||
:page="page"
|
||||
:has-permission="hasPermission"
|
||||
v-model:search-visible="searchVisible"
|
||||
@create="openCreate"
|
||||
@export="handleExport"
|
||||
@refresh="loadTable"
|
||||
@@ -91,7 +93,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArrowDown, ArrowUp, Refresh } from '@element-plus/icons-vue';
|
||||
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { downloadFile } from '@/utils/util';
|
||||
import * as api from '@/api/settlement/transportReconciliation';
|
||||
@@ -121,9 +123,9 @@ export default {
|
||||
return {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Refresh,
|
||||
query: emptyQuery(),
|
||||
searchExpanded: false,
|
||||
searchVisible: true,
|
||||
searchFields: transportReconciliationSearchFields,
|
||||
columns: transportReconciliationTableColumns,
|
||||
settlementType: 'payable',
|
||||
|
||||
+310
-61
@@ -35,8 +35,16 @@
|
||||
plain
|
||||
:loading="oaSyncLoading"
|
||||
v-if="isAdministrator"
|
||||
@click="handleOaOrgSync"
|
||||
>自动同步组织
|
||||
@click="handleOaOrgFullSync"
|
||||
>全量同步
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:loading="oaSyncLoading"
|
||||
v-if="isAdministrator"
|
||||
@click="handleOaOrgIncrementalSync"
|
||||
>增量同步
|
||||
</el-button>
|
||||
</template>
|
||||
<template #menu="scope">
|
||||
@@ -189,6 +197,22 @@ import func from '@/utils/func';
|
||||
|
||||
const escapeRegExp = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const ORGANIZATION_TYPE_DICT_CODE = 'organization_type';
|
||||
/**
|
||||
* 组织类型名称 → 字典数字键(兼容 dictKey 配成中文的情况)
|
||||
* 与业务字典 organization_type 及历史别名对齐;优先仍以数字 dictKey 为准
|
||||
*/
|
||||
const ORGANIZATION_TYPE_CODE_BY_LABEL = {
|
||||
集团: 1,
|
||||
一级集团: 1,
|
||||
公司: 2,
|
||||
事业部: 2,
|
||||
二级事业部: 2,
|
||||
部门: 3,
|
||||
分公司: 4,
|
||||
三级分公司: 4,
|
||||
物流园区: 5,
|
||||
承运商: 6,
|
||||
};
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -220,6 +244,7 @@ export default {
|
||||
oaSyncCancelled: false,
|
||||
oaSyncStatus: 'running',
|
||||
oaSyncStage: 'company',
|
||||
oaSyncMode: 'full',
|
||||
oaSyncClearNonTop: false,
|
||||
oaSyncController: null,
|
||||
oaSyncProgress: {
|
||||
@@ -264,6 +289,7 @@ export default {
|
||||
minWidth: 160,
|
||||
order: 80,
|
||||
search: true,
|
||||
searchOrder: 2,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
@@ -307,9 +333,17 @@ export default {
|
||||
callback(new Error('上级组织未配置组织编码,请先完善上级组织编码'));
|
||||
return;
|
||||
}
|
||||
const pattern = new RegExp(`^${escapeRegExp(this.parentDeptCode)}-\\d+$`);
|
||||
// 公司:GWWL+三位数字;部门:所属公司编码+三位数字;其他:上级编码+三位数字(中间不加-)
|
||||
let expectedPrefix = this.parentDeptCode;
|
||||
const category = Number(this.form?.deptCategory);
|
||||
if (category === 2) {
|
||||
expectedPrefix = 'GWWL';
|
||||
} else if (category === 3 && this.form?.belongCompanyCode) {
|
||||
expectedPrefix = String(this.form.belongCompanyCode).trim();
|
||||
}
|
||||
const pattern = new RegExp(`^${escapeRegExp(expectedPrefix)}\\d{3}$`);
|
||||
if (!pattern.test(deptCode)) {
|
||||
callback(new Error(`组织编码格式应为:${this.parentDeptCode}-分段数字`));
|
||||
callback(new Error(`组织编码格式应为:${expectedPrefix}三位数字(不足补零,中间不加-)`));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
@@ -343,11 +377,40 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '合同编码前缀',
|
||||
prop: 'contractCodePrefix',
|
||||
minWidth: 160,
|
||||
order: 62,
|
||||
maxlength: 64,
|
||||
showWordLimit: true,
|
||||
placeholder: '如 GWZH成都',
|
||||
tip: '合同编号规则:前缀(年份)三位流水号,流水按本部门独立编号',
|
||||
rules: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
const prefix = String(value || '').trim();
|
||||
if (!prefix) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (prefix.length > 64) {
|
||||
callback(new Error('合同编码前缀不能超过64个字符'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '组织简称',
|
||||
prop: 'shortName',
|
||||
minWidth: 140,
|
||||
order: 60,
|
||||
search: true,
|
||||
searchOrder: 1,
|
||||
},
|
||||
{
|
||||
label: '助记码',
|
||||
@@ -421,15 +484,33 @@ export default {
|
||||
{
|
||||
label: '排序',
|
||||
prop: 'sort',
|
||||
type: 'number',
|
||||
type: 'input',
|
||||
align: 'right',
|
||||
width: 80,
|
||||
order: 30,
|
||||
change: ({ value }) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return;
|
||||
}
|
||||
const digits = String(value).replace(/\D/g, '');
|
||||
if (String(value) !== digits) {
|
||||
this.form.sort = digits;
|
||||
}
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入排序',
|
||||
trigger: 'blur',
|
||||
validator: (rule, value, callback) => {
|
||||
if (value === '' || value === undefined || value === null) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (!/^\d+$/.test(String(value).trim())) {
|
||||
callback(new Error('排序只能输入数字'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -468,12 +549,10 @@ export default {
|
||||
value: 'tenantId',
|
||||
},
|
||||
hide: true,
|
||||
search: website.tenantMode,
|
||||
},
|
||||
{
|
||||
label: '机构全称',
|
||||
prop: 'fullName',
|
||||
search: true,
|
||||
hide: true,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
@@ -527,16 +606,17 @@ export default {
|
||||
return ids.join(',');
|
||||
},
|
||||
oaSyncDialogTitle() {
|
||||
const modeLabel = this.oaSyncMode === 'incremental' ? '增量同步' : '全量同步';
|
||||
if (this.oaSyncStatus === 'done') {
|
||||
return '同步完成';
|
||||
return `${modeLabel}完成`;
|
||||
}
|
||||
if (this.oaSyncStatus === 'cancelled') {
|
||||
return '已取消同步';
|
||||
return `已取消${modeLabel}`;
|
||||
}
|
||||
if (this.oaSyncStatus === 'error') {
|
||||
return '同步失败';
|
||||
return `${modeLabel}失败`;
|
||||
}
|
||||
return '自动同步组织';
|
||||
return modeLabel;
|
||||
},
|
||||
oaSyncStageLabel() {
|
||||
if (this.oaSyncStage === 'clear') {
|
||||
@@ -576,10 +656,10 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleOaOrgSync() {
|
||||
handleOaOrgFullSync() {
|
||||
this.$confirm(
|
||||
'是否清除非顶级组织?选择“是”将先删除顶级以外的组织后再同步;选择“否”则直接同步。',
|
||||
'提示',
|
||||
'全量同步将拉取 OA 全部公司/部门;已存在且 OA 更新时间一致的记录会跳过。是否清除后同步?选择“是”将保留顶级「外部组织」与集团锚点(id=135/GWWL),删除其余组织后再同步;选择“否”则直接全量同步。',
|
||||
'全量同步',
|
||||
{
|
||||
confirmButtonText: '是',
|
||||
cancelButtonText: '否',
|
||||
@@ -589,20 +669,37 @@ export default {
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
this.startOaOrgSync(true);
|
||||
this.startOaOrgSync({ clearNonTop: true, incremental: false });
|
||||
})
|
||||
.catch(action => {
|
||||
if (action === 'cancel') {
|
||||
this.startOaOrgSync(false);
|
||||
this.startOaOrgSync({ clearNonTop: false, incremental: false });
|
||||
}
|
||||
});
|
||||
},
|
||||
startOaOrgSync(clearNonTop) {
|
||||
handleOaOrgIncrementalSync() {
|
||||
this.$confirm(
|
||||
'增量同步仅拉取本地最大 OA 更新时间之后有变更的组织;不存在的新增,已存在且更新时间不一致的修改,一致的跳过。是否继续?',
|
||||
'增量同步',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
closeOnClickModal: false,
|
||||
type: 'info',
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
this.startOaOrgSync({ clearNonTop: false, incremental: true });
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
startOaOrgSync({ clearNonTop = false, incremental = false } = {}) {
|
||||
this.oaSyncLoading = true;
|
||||
this.oaSyncVisible = true;
|
||||
this.oaSyncRunning = true;
|
||||
this.oaSyncCancelled = false;
|
||||
this.oaSyncStatus = 'running';
|
||||
this.oaSyncMode = incremental ? 'incremental' : 'full';
|
||||
this.oaSyncClearNonTop = !!clearNonTop;
|
||||
this.oaSyncStage = clearNonTop ? 'clear' : 'company';
|
||||
this.oaSyncController = new AbortController();
|
||||
@@ -625,10 +722,21 @@ export default {
|
||||
},
|
||||
async runOaOrgSyncStage(syncApi) {
|
||||
const size = 20;
|
||||
const incremental = this.oaSyncMode === 'incremental';
|
||||
let current = 1;
|
||||
let modifiedSince = '';
|
||||
while (!this.oaSyncCancelled) {
|
||||
const res = await syncApi(current, size, this.oaSyncController?.signal);
|
||||
const res = await syncApi(
|
||||
current,
|
||||
size,
|
||||
this.oaSyncController?.signal,
|
||||
incremental,
|
||||
modifiedSince
|
||||
);
|
||||
const page = res?.data?.data || {};
|
||||
if (!modifiedSince && page.modifiedSince) {
|
||||
modifiedSince = page.modifiedSince;
|
||||
}
|
||||
this.oaSyncProgress.current = page.current || current;
|
||||
this.oaSyncProgress.size = page.size || size;
|
||||
this.oaSyncProgress.total = page.total || 0;
|
||||
@@ -755,7 +863,7 @@ export default {
|
||||
.filter(Boolean);
|
||||
this.resetDeptCategoryOptions();
|
||||
if (list.length && !this.organizationTypeOptions.length) {
|
||||
this.$message.warning('组织类型字典 dictKey 须为数字 1~6,当前无法解析,请检查业务字典配置');
|
||||
this.$message.warning('组织类型字典无法解析,请检查业务字典 organization_type 的字典键配置');
|
||||
}
|
||||
// 字典异步返回后触发表格组织类型标签重绘
|
||||
if (Array.isArray(this.data) && this.data.length) {
|
||||
@@ -765,35 +873,46 @@ export default {
|
||||
},
|
||||
/**
|
||||
* 业务字典项转下拉选项。后端 deptCategory 为 Integer,value 必须始终为数字。
|
||||
* dictKey 为纯数字时直接使用;若配成中文则回退用 sort(1~6)作为层级码。
|
||||
* 优先用数字 dictKey;其次按名称映射;再回退 sort,避免字典项被静默丢弃。
|
||||
*/
|
||||
normalizeOrganizationTypeOption(item) {
|
||||
const label = item?.dictValue ?? item?.label;
|
||||
if (label === undefined || label === null || String(label).trim() === '') {
|
||||
return null;
|
||||
}
|
||||
const labelText = String(label).trim();
|
||||
const rawKey = item?.dictKey ?? item?.value;
|
||||
const keyText = rawKey === undefined || rawKey === null ? '' : String(rawKey).trim();
|
||||
const keyNumber = Number(keyText);
|
||||
const isPureNumber = keyText !== '' && !Number.isNaN(keyNumber) && String(keyNumber) === keyText;
|
||||
if (isPureNumber) {
|
||||
return {
|
||||
label: String(label),
|
||||
label: labelText,
|
||||
value: keyNumber,
|
||||
};
|
||||
}
|
||||
// 兼容 dictKey 误配成中文:用排序号作为 1~6 层级码,保证下拉有数据且可提交 Integer
|
||||
const sortNumber = Number(item?.sort);
|
||||
if (!Number.isNaN(sortNumber) && sortNumber >= 1 && sortNumber <= 6) {
|
||||
// dictKey / 名称 配成中文时,按标准名称映射到数字键
|
||||
const codeByLabel =
|
||||
ORGANIZATION_TYPE_CODE_BY_LABEL[labelText] ?? ORGANIZATION_TYPE_CODE_BY_LABEL[keyText];
|
||||
if (codeByLabel) {
|
||||
return {
|
||||
label: String(label),
|
||||
label: labelText,
|
||||
value: codeByLabel,
|
||||
};
|
||||
}
|
||||
// 未命中名称映射时,用排序号兜底,保证与业务字典项一致展示
|
||||
const sortNumber = Number(item?.sort);
|
||||
if (!Number.isNaN(sortNumber) && sortNumber >= 1) {
|
||||
return {
|
||||
label: labelText,
|
||||
value: sortNumber,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
/**
|
||||
* 提交前将组织类型规范为 Integer;空状态字段去掉,避免 "" 反序列化失败。
|
||||
* 提交前将组织类型规范为 Integer;空状态/排序字段去掉,避免 "" 反序列化失败。
|
||||
* 排序为空时由后端默认填充 1。
|
||||
*/
|
||||
prepareDeptSubmitRow(row) {
|
||||
const category = this.resolveDeptCategoryValue(row.deptCategory);
|
||||
@@ -806,6 +925,11 @@ export default {
|
||||
} else {
|
||||
row.status = Number(row.status);
|
||||
}
|
||||
if (row.sort === '' || row.sort === null || row.sort === undefined) {
|
||||
delete row.sort;
|
||||
} else {
|
||||
row.sort = Number(String(row.sort).trim());
|
||||
}
|
||||
return true;
|
||||
},
|
||||
resolveDeptCategoryValue(value) {
|
||||
@@ -813,18 +937,25 @@ export default {
|
||||
this.$message.warning('请选择组织类型');
|
||||
return null;
|
||||
}
|
||||
if (typeof value === 'number' && !Number.isNaN(value)) {
|
||||
return value;
|
||||
}
|
||||
const asNumber = Number(value);
|
||||
if (!Number.isNaN(asNumber) && String(asNumber) === String(value).trim()) {
|
||||
return asNumber;
|
||||
}
|
||||
// 容错:表单里残留了中文标签时,按字典项 label 反查数字编码
|
||||
// 容错:表单里残留了中文标签时,按字典项 label / 标准名称反查数字编码
|
||||
const matched = this.organizationTypeOptions.find(
|
||||
item => String(item.label) === String(value) || this.isSameDeptCategory(item.value, value)
|
||||
);
|
||||
if (matched && typeof matched.value === 'number' && !Number.isNaN(matched.value)) {
|
||||
return matched.value;
|
||||
}
|
||||
this.$message.warning('组织类型无效,请重新选择(字典键须为数字 1~6)');
|
||||
const codeByLabel = ORGANIZATION_TYPE_CODE_BY_LABEL[String(value).trim()];
|
||||
if (codeByLabel) {
|
||||
return codeByLabel;
|
||||
}
|
||||
this.$message.warning('组织类型无效,请重新选择');
|
||||
return null;
|
||||
},
|
||||
resetDeptCategoryOptions() {
|
||||
@@ -860,35 +991,9 @@ export default {
|
||||
const text = matched?.label || value;
|
||||
return String(text).includes('承运商');
|
||||
},
|
||||
filterOrganizationTypeOptions(values) {
|
||||
const valueSet = new Set(values.map(item => String(item)));
|
||||
const filtered = this.organizationTypeOptions.filter(item => valueSet.has(String(item.value)));
|
||||
// 字典 key 非数字时无法按历史层级码过滤,回退展示全部,避免下拉无选项
|
||||
return filtered.length ? filtered : [...this.organizationTypeOptions];
|
||||
},
|
||||
updateDeptCategoryOptions() {
|
||||
const categoryColumn = this.findColumn(this.option.column, 'deptCategory');
|
||||
const parent = this.parentDept;
|
||||
if (!parent || !categoryColumn) {
|
||||
return;
|
||||
}
|
||||
let dicData = [];
|
||||
if (this.isRootDept(parent)) {
|
||||
dicData =
|
||||
parent.deptName === '外部组织'
|
||||
? this.filterOrganizationTypeOptions([6])
|
||||
: this.filterOrganizationTypeOptions([1]);
|
||||
} else if (Number(parent.deptCategory) === 1) {
|
||||
dicData = this.filterOrganizationTypeOptions([2, 3]);
|
||||
} else if ([2, 3].includes(Number(parent.deptCategory))) {
|
||||
dicData = this.filterOrganizationTypeOptions([4, 5, 6]);
|
||||
} else {
|
||||
dicData = [...this.organizationTypeOptions];
|
||||
}
|
||||
categoryColumn.dicData = dicData;
|
||||
if (!dicData.some(item => this.isSameDeptCategory(item.value, this.form.deptCategory))) {
|
||||
this.form.deptCategory = dicData[0]?.value;
|
||||
}
|
||||
// 组织层级仅由 parentId 决定,组织类型不再按上级过滤,始终展示全部字典项
|
||||
this.resetDeptCategoryOptions();
|
||||
if (this.isCarrierDeptCategory(this.form.deptCategory)) {
|
||||
this.openCarrierDialog();
|
||||
}
|
||||
@@ -994,10 +1099,141 @@ export default {
|
||||
column.addDisabled = true;
|
||||
this.$refs.crud.rowAdd();
|
||||
},
|
||||
getDeptTableRef() {
|
||||
return this.$refs.crud?.$refs?.table;
|
||||
},
|
||||
getDeptTableScrollWrap() {
|
||||
const table = this.getDeptTableRef();
|
||||
const root = table?.$el || this.$refs.crud?.$el;
|
||||
if (!root || typeof root.querySelector !== 'function') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
root.querySelector('.el-table__body-wrapper .el-scrollbar__wrap') ||
|
||||
root.querySelector('.el-scrollbar__wrap') ||
|
||||
root.querySelector('.el-table__body-wrapper')
|
||||
);
|
||||
},
|
||||
getDeptTableScrollTop() {
|
||||
const wrap = this.getDeptTableScrollWrap();
|
||||
return wrap ? wrap.scrollTop : 0;
|
||||
},
|
||||
restoreDeptTableScroll(scrollTop) {
|
||||
const wrap = this.getDeptTableScrollWrap();
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
wrap.scrollTop = scrollTop;
|
||||
},
|
||||
findDeptRowById(list, id) {
|
||||
if (!Array.isArray(list) || id === undefined || id === null || id === '') {
|
||||
return null;
|
||||
}
|
||||
for (let index = 0; index < list.length; index += 1) {
|
||||
const item = list[index];
|
||||
if (String(item.id) === String(id)) {
|
||||
return item;
|
||||
}
|
||||
const child = this.findDeptRowById(item.children, id);
|
||||
if (child) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
syncLazyTreeAfterAdd(row, parentId) {
|
||||
const table = this.getDeptTableRef();
|
||||
if (!table || !parentId || String(parentId) === '0') {
|
||||
return;
|
||||
}
|
||||
const parentRow = this.findDeptRowById(this.data, parentId);
|
||||
if (parentRow) {
|
||||
parentRow.hasChildren = true;
|
||||
if (!Array.isArray(parentRow.children)) {
|
||||
parentRow.children = [];
|
||||
}
|
||||
if (!parentRow.children.some(item => String(item.id) === String(row.id))) {
|
||||
parentRow.children.push(row);
|
||||
}
|
||||
}
|
||||
const lazyMapRef = table.store?.states?.lazyTreeNodeMap;
|
||||
const treeDataRef = table.store?.states?.treeData;
|
||||
const lazyMap = lazyMapRef?.value ?? lazyMapRef;
|
||||
const treeData = treeDataRef?.value ?? treeDataRef;
|
||||
const mapKey =
|
||||
(lazyMap && Object.prototype.hasOwnProperty.call(lazyMap, parentId) && parentId) ||
|
||||
(lazyMap && Object.prototype.hasOwnProperty.call(lazyMap, String(parentId)) && String(parentId)) ||
|
||||
(parentRow && parentRow.id);
|
||||
if (mapKey !== undefined && mapKey !== null && lazyMap) {
|
||||
const currentChildren = Array.isArray(lazyMap[mapKey])
|
||||
? lazyMap[mapKey]
|
||||
: parentRow?.children || [];
|
||||
const nextChildren = currentChildren.some(item => String(item.id) === String(row.id))
|
||||
? [...currentChildren]
|
||||
: [...currentChildren, row];
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(lazyMap, mapKey) &&
|
||||
typeof table.updateKeyChildren === 'function'
|
||||
) {
|
||||
table.updateKeyChildren(mapKey, nextChildren);
|
||||
} else if (lazyMapRef && Object.prototype.hasOwnProperty.call(lazyMapRef, 'value')) {
|
||||
// 首次挂子节点:直接写入懒加载缓存,避免再次 treeLoad 导致滚动回顶
|
||||
lazyMapRef.value = {
|
||||
...lazyMapRef.value,
|
||||
[mapKey]: nextChildren,
|
||||
};
|
||||
} else {
|
||||
lazyMap[mapKey] = nextChildren;
|
||||
}
|
||||
}
|
||||
if (treeData && parentRow) {
|
||||
const treeKey = Object.prototype.hasOwnProperty.call(treeData, parentRow.id)
|
||||
? parentRow.id
|
||||
: Object.prototype.hasOwnProperty.call(treeData, String(parentRow.id))
|
||||
? String(parentRow.id)
|
||||
: parentRow.id;
|
||||
const nextNode = {
|
||||
...(treeData[treeKey] || {}),
|
||||
lazy: true,
|
||||
loaded: true,
|
||||
loading: false,
|
||||
expanded: true,
|
||||
hasChildren: true,
|
||||
};
|
||||
if (treeDataRef && Object.prototype.hasOwnProperty.call(treeDataRef, 'value')) {
|
||||
treeDataRef.value = {
|
||||
...treeDataRef.value,
|
||||
[treeKey]: nextNode,
|
||||
};
|
||||
} else {
|
||||
treeData[treeKey] = nextNode;
|
||||
}
|
||||
}
|
||||
if (parentRow && typeof table.toggleRowExpansion === 'function') {
|
||||
table.toggleRowExpansion(parentRow, true);
|
||||
}
|
||||
},
|
||||
scrollDeptRowIntoView(rowId) {
|
||||
if (rowId === undefined || rowId === null || rowId === '') {
|
||||
return;
|
||||
}
|
||||
const table = this.getDeptTableRef();
|
||||
const root = table?.$el || this.$refs.crud?.$el;
|
||||
if (!root || typeof root.querySelector !== 'function') {
|
||||
return;
|
||||
}
|
||||
const rowEl =
|
||||
root.querySelector(`tr[data-row-key="${rowId}"]`) ||
|
||||
root.querySelector(`tr[data-row-key='${rowId}']`);
|
||||
if (rowEl && typeof rowEl.scrollIntoView === 'function') {
|
||||
rowEl.scrollIntoView({ block: 'nearest', behavior: 'auto' });
|
||||
}
|
||||
},
|
||||
rowSave(row, done, loading) {
|
||||
row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId;
|
||||
row.deptCode = String(row.deptCode || '').trim();
|
||||
row.kingdeeDeptCode = String(row.kingdeeDeptCode || '').trim();
|
||||
row.contractCodePrefix = String(row.contractCodePrefix || '').trim();
|
||||
row.leaderId = func.join(row.leaderId);
|
||||
this.normalizeTopParent(row);
|
||||
if (!this.prepareDeptSubmitRow(row)) {
|
||||
@@ -1009,6 +1245,7 @@ export default {
|
||||
loading();
|
||||
return;
|
||||
}
|
||||
const scrollTop = this.getDeptTableScrollTop();
|
||||
add(row).then(
|
||||
res => {
|
||||
// 获取新增数据的相关字段
|
||||
@@ -1016,12 +1253,22 @@ export default {
|
||||
row.id = data.id;
|
||||
row.deptCategoryName = data.deptCategoryName;
|
||||
row.tenantId = data.tenantId;
|
||||
if (row.hasChildren === undefined || row.hasChildren === null) {
|
||||
row.hasChildren = false;
|
||||
}
|
||||
this.$message({
|
||||
type: 'success',
|
||||
message: '操作成功!',
|
||||
});
|
||||
// 数据回调进行刷新
|
||||
// 数据回调进行刷新(局部插入,避免整树重载跳回顶部)
|
||||
done(row);
|
||||
this.$nextTick(() => {
|
||||
this.syncLazyTreeAfterAdd(row, this.normalizeParentId(row.parentId));
|
||||
this.restoreDeptTableScroll(scrollTop);
|
||||
this.$nextTick(() => {
|
||||
this.scrollDeptRowIntoView(row.id);
|
||||
});
|
||||
});
|
||||
},
|
||||
error => {
|
||||
window.console.log(error);
|
||||
@@ -1033,6 +1280,7 @@ export default {
|
||||
row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId;
|
||||
row.deptCode = String(row.deptCode || '').trim();
|
||||
row.kingdeeDeptCode = String(row.kingdeeDeptCode || '').trim();
|
||||
row.contractCodePrefix = String(row.contractCodePrefix || '').trim();
|
||||
row.leaderId = func.join(row.leaderId);
|
||||
this.normalizeTopParent(row);
|
||||
if (!this.prepareDeptSubmitRow(row)) {
|
||||
@@ -1166,7 +1414,8 @@ export default {
|
||||
done();
|
||||
},
|
||||
beforeClose(done) {
|
||||
this.parentId = '';
|
||||
// 保持为根级查询条件,避免关闭弹窗后若触发重载时 parentId 为空导致整树重置
|
||||
this.parentId = 0;
|
||||
this.parentDeptCode = '';
|
||||
this.parentDept = null;
|
||||
const column = this.findColumn(this.option.column, 'parentId');
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
<template>
|
||||
<basic-container class="mp-user-page">
|
||||
<avue-crud
|
||||
:option="option"
|
||||
:table-loading="loading"
|
||||
:data="data"
|
||||
v-model:page="page"
|
||||
v-model="form"
|
||||
ref="crud"
|
||||
:permission="permissionList"
|
||||
:before-open="beforeOpen"
|
||||
@row-save="rowSave"
|
||||
@row-update="rowUpdate"
|
||||
@search-change="searchChange"
|
||||
@search-reset="searchReset"
|
||||
@selection-change="selectionChange"
|
||||
@current-change="currentChange"
|
||||
@size-change="sizeChange"
|
||||
@refresh-change="refreshChange"
|
||||
@on-load="onLoad"
|
||||
>
|
||||
<template #menu-left>
|
||||
<el-button
|
||||
type="primary"
|
||||
v-if="permission.mp_user_add"
|
||||
@click="$refs.crud.rowAdd()"
|
||||
>
|
||||
新增
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
v-if="permission.mp_user_reset"
|
||||
@click="handleReset"
|
||||
>
|
||||
密码重置
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
v-if="permission.mp_user_delete"
|
||||
@click="handleDelete"
|
||||
>
|
||||
批量删除
|
||||
</el-button>
|
||||
</template>
|
||||
<template #identities="{ row }">
|
||||
{{ row.identitiesName || formatIdentities(row.identities) }}
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'primary' : 'info'" class="status-text">
|
||||
{{ row.status === 1 ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template #updateUserName="{ row }">
|
||||
{{ formatUpdateUserName(row) }}
|
||||
</template>
|
||||
<template #menu="{ row, index }">
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="permission.mp_user_view"
|
||||
@click="$refs.crud.rowView(row, index)"
|
||||
>
|
||||
查看
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="permission.mp_user_edit"
|
||||
@click="$refs.crud.rowEdit(row, index)"
|
||||
>
|
||||
编辑
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="permission.mp_user_status"
|
||||
@click="handleStatus(row)"
|
||||
>
|
||||
{{ row.status === 1 ? '停用' : '启用' }}
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="permission.mp_user_reset"
|
||||
@click="handleResetForRow(row)"
|
||||
>
|
||||
重置密码
|
||||
</el-link>
|
||||
<el-link
|
||||
type="danger"
|
||||
v-if="permission.mp_user_delete"
|
||||
@click="rowDel(row)"
|
||||
>
|
||||
删除
|
||||
</el-link>
|
||||
</template>
|
||||
<template #menu-form="{ size }">
|
||||
<el-button
|
||||
v-if="permission.mp_user_reset && form.id && dialogType === 'edit'"
|
||||
:size="size"
|
||||
@click="handleResetForRow(form)"
|
||||
>
|
||||
重置密码
|
||||
</el-button>
|
||||
</template>
|
||||
</avue-crud>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { h } from 'vue';
|
||||
import { getList, getDetail, submit, remove, changeStatus, resetPassword } from '@/api/system/mpUser';
|
||||
import option from '@/option/system/mpUser';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
|
||||
const IDENTITY_LABEL = {
|
||||
driver: '司机',
|
||||
admin: '管理员',
|
||||
};
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: {},
|
||||
dialogType: '',
|
||||
query: {},
|
||||
loading: true,
|
||||
page: {
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
total: 0,
|
||||
},
|
||||
selectionList: [],
|
||||
option,
|
||||
data: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: this.validData(this.permission.mp_user_add, false),
|
||||
viewBtn: this.validData(this.permission.mp_user_view, false),
|
||||
delBtn: this.validData(this.permission.mp_user_delete, false),
|
||||
editBtn: this.validData(this.permission.mp_user_edit, false),
|
||||
};
|
||||
},
|
||||
ids() {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatUpdateUserName,
|
||||
formatIdentities(value) {
|
||||
if (!value) return '';
|
||||
return String(value)
|
||||
.split(',')
|
||||
.map(item => IDENTITY_LABEL[item] || item)
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
},
|
||||
normalizeSubmitRow(row = {}) {
|
||||
const payload = { ...row };
|
||||
if (Array.isArray(payload.identities)) {
|
||||
payload.identities = payload.identities.filter(Boolean).join(',');
|
||||
}
|
||||
if (payload.phone) {
|
||||
payload.phone = String(payload.phone).trim();
|
||||
}
|
||||
if (!payload.account && payload.phone) {
|
||||
payload.account = payload.phone;
|
||||
}
|
||||
delete payload.password;
|
||||
delete payload.identitiesName;
|
||||
delete payload.sexName;
|
||||
delete payload.createUserName;
|
||||
delete payload.updateUserName;
|
||||
return payload;
|
||||
},
|
||||
beforeOpen(done, type) {
|
||||
this.dialogType = type;
|
||||
if (['edit', 'view'].includes(type) && this.form.id) {
|
||||
getDetail(this.form.id).then(res => {
|
||||
const detail = res.data.data || {};
|
||||
if (detail.identities && typeof detail.identities === 'string') {
|
||||
detail.identities = detail.identities.split(',').filter(Boolean);
|
||||
}
|
||||
this.form = detail;
|
||||
done();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (type === 'add') {
|
||||
this.form = {
|
||||
status: 1,
|
||||
identities: ['driver'],
|
||||
};
|
||||
}
|
||||
done();
|
||||
},
|
||||
rowSave(row, done, loading) {
|
||||
submit(this.normalizeSubmitRow(row)).then(
|
||||
() => {
|
||||
this.onLoad(this.page);
|
||||
this.$message.success('操作成功');
|
||||
done();
|
||||
},
|
||||
() => loading()
|
||||
);
|
||||
},
|
||||
rowUpdate(row, index, done, loading) {
|
||||
submit(this.normalizeSubmitRow(row)).then(
|
||||
() => {
|
||||
this.onLoad(this.page);
|
||||
this.$message.success('操作成功');
|
||||
done();
|
||||
},
|
||||
() => loading()
|
||||
);
|
||||
},
|
||||
rowDel(row) {
|
||||
this.$confirm('确定删除该小程序用户?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => remove(row.id))
|
||||
.then(() => {
|
||||
this.onLoad(this.page);
|
||||
this.$message.success('操作成功');
|
||||
});
|
||||
},
|
||||
handleDelete() {
|
||||
if (this.selectionList.length === 0) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
this.$confirm('确定删除选中的小程序用户?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => remove(this.ids))
|
||||
.then(() => {
|
||||
this.onLoad(this.page);
|
||||
this.$message.success('操作成功');
|
||||
this.$refs.crud.toggleSelection();
|
||||
});
|
||||
},
|
||||
handleStatus(row) {
|
||||
const status = row.status === 1 ? 0 : 1;
|
||||
const text = status === 1 ? '启用' : '停用';
|
||||
this.$confirm(`是否${text}该小程序用户?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => changeStatus(row.id, status))
|
||||
.then(() => {
|
||||
this.$message.success('操作成功');
|
||||
this.onLoad(this.page);
|
||||
});
|
||||
},
|
||||
handleReset() {
|
||||
if (this.selectionList.length === 0) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
this.$confirm('确定将选中账号密码重置为随机密码?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => resetPassword(this.ids))
|
||||
.then(res => {
|
||||
this.showResetPassword(res.data.data);
|
||||
this.$refs.crud.toggleSelection();
|
||||
});
|
||||
},
|
||||
handleResetForRow(row) {
|
||||
if (!row || !row.id) {
|
||||
this.$message.warning('请先保存用户后再重置密码');
|
||||
return;
|
||||
}
|
||||
const account = row.account || row.phone || row.id;
|
||||
this.$confirm(`确定将账号【${account}】的密码重置为随机密码?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => resetPassword(row.id))
|
||||
.then(res => {
|
||||
this.showResetPassword(res.data.data);
|
||||
});
|
||||
},
|
||||
showResetPassword(passwordMap) {
|
||||
const passwordList = Object.keys(passwordMap || {}).map(account =>
|
||||
h('div', null, `${account}:${passwordMap[account]}`)
|
||||
);
|
||||
this.$alert(h('div', null, passwordList), '密码重置成功,请立即保存', {
|
||||
confirmButtonText: '我已保存',
|
||||
type: 'success',
|
||||
});
|
||||
},
|
||||
searchReset() {
|
||||
this.query = {};
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
searchChange(params, done) {
|
||||
this.query = params;
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page, params);
|
||||
done();
|
||||
},
|
||||
selectionChange(list) {
|
||||
this.selectionList = list;
|
||||
},
|
||||
currentChange(currentPage) {
|
||||
this.page.currentPage = currentPage;
|
||||
},
|
||||
sizeChange(pageSize) {
|
||||
this.page.pageSize = pageSize;
|
||||
},
|
||||
refreshChange() {
|
||||
this.onLoad(this.page, this.query);
|
||||
},
|
||||
onLoad(page, params = {}) {
|
||||
this.loading = true;
|
||||
const query = {
|
||||
...this.query,
|
||||
...params,
|
||||
};
|
||||
if (Array.isArray(query.identities)) {
|
||||
query.identities = query.identities.filter(Boolean).join(',');
|
||||
}
|
||||
getList(page.currentPage, page.pageSize, query)
|
||||
.then(res => {
|
||||
const result = res.data.data || {};
|
||||
this.page.total = result.total || 0;
|
||||
this.data = result.records || [];
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mp-user-page {
|
||||
:deep(.avue-crud__menu) {
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user