Compare commits
6 Commits
3e7110ca8c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7078de6385 | |||
| 4af9afda61 | |||
| 50ab33f369 | |||
| bcbc45bb03 | |||
| ff9fb9168b | |||
| 986feff7b7 |
@@ -38,3 +38,85 @@
|
||||
**校验**:dev server curl 该 .vue 与 scoped style 均 200。仍未 commit。
|
||||
|
||||
**未提交**:上述改动 + 上一轮 loading-manage 独立页改造均未 commit。
|
||||
|
||||
---
|
||||
|
||||
## 排查(未改码):配载单详情页反复弹「运单管理不存在」
|
||||
|
||||
用户截图:打开 `/business/loading-manage/detail?id=2097607162718441474&name=配载单详情`,连续弹出 6-7 条红色「运单管理不存在」,标签栏堆了一串「运单管理详情」。
|
||||
|
||||
**文案来源**:前端 src 里没有这句话,是后端 `BusinessException` 的 msg,被 `src/axios.js` 的响应拦截器(`status !== 200` → `ElMessage.error(message)`)统一弹出。
|
||||
|
||||
**主因(通道 A)**:`views/business/waybill-manage-detail.vue` 只有 23 行,写了 `:detail-id="$route.query.id"` —— 直接绑全局 `$route`,**没有路径守卫**。`waybill-manage-page.vue` 的 `detailId` watcher 是 `immediate` 且对任何非空 id 都 `openDetail({id})`。于是:
|
||||
1. 之前打开过的每个运单详情标签(`/business/waybill-manage/detail?id=A`、`?id=B`…)各是一个被 keep-alive 缓存的独立实例;
|
||||
2. 跳到配载单详情后,这些实例被 deactivate 但仍随 `$route` 重渲染 → `$route.query.id` 变成配载单 id;
|
||||
3. prop 变化 → `openDetail({id: 配载单id})` → `getDetail(配载单id)` 去查运单 → 后端「运单管理不存在」→ 拦截器弹一次。**几个旧标签就几条提示**;
|
||||
4. 且被污染实例的 `isStandaloneWaybillDetailPage` 仍是 live computed(false) → `openDetail` 走 `$router.push` 分支 → 又生成新的「运单管理详情」标签(标签栏那串的来源)。
|
||||
|
||||
同理风险:`waybill-manage.vue:6` 与 `transport-plan.vue:6` 的 `:detail-id="$route.query.detailId"`(`business-crud-page.vue:7356` 会 push `/business/loading-manage?detailId=id`,正好会污染运单列表实例)。
|
||||
|
||||
**次因(通道 B)**:`loading-manage.vue` 的 `restoreWaybillRows` 会用 `waybillIdsJson` 里的每个运单 id 并发 `getWaybillDetail(id)`,若历史运单已被删/失效,也会报同一句文案且并发多条。单条 `.catch(() => null)` 挡不住拦截器已弹出的提示。
|
||||
|
||||
**区分方法**:Network 里看报错请求的 `id` —— 等于 `2097607162718441474`(配载单 id)→ 通道 A;是别的数字(运单 id)→ 通道 B。或先关掉所有「运单管理详情」标签再复现,通道 A 会消失。
|
||||
|
||||
**待确认的修法**(用户要求先报不改):
|
||||
1. `waybill-manage-page.vue`:`created()` 锁 `routePathLocked = this.$route.path`,`detailId` watcher 首行加 `if (this.$route.path !== this.routePathLocked) return;`。
|
||||
2. `openDetail()` 的 push 分支加同样守卫(防其它调用路径)。
|
||||
3. 宿主 view 绑定加路径守卫(`waybill-manage-detail.vue` / `waybill-manage.vue` / `transport-plan.vue`)双保险。
|
||||
4. 可选:`getWaybillDetail` 支持静默模式,供 `restoreWaybillRows` 用,避免历史脏数据刷屏。
|
||||
|
||||
## 修复(已执行):「运单管理不存在」误报 + 误开标签
|
||||
|
||||
用户确认后按上方案落地,5 个文件:
|
||||
|
||||
1. `src/views/business/components/waybill-manage-page.vue`
|
||||
- `data()` 新增 `routePathLocked: this.$route.path` —— **必须放 data,不能放 created**:Vue Options API 顺序是 data → computed → watch(immediate) → created(已在 runtime-core `applyOptions` 源码中核实)。放 created 会让首个 immediate watcher 读到空值。
|
||||
- 新增 computed `isOwnedRouteActive() { return this.$route.path === this.routePathLocked; }`
|
||||
- `detailId` watcher 首行加 `if (!this.isOwnedRouteActive) return;`
|
||||
- `openDetail` 的 `$router.push` 分支同样加守卫。
|
||||
2. `src/views/business/components/transport-plan-page.vue`:完全同构的写法(`:detail-id` host 一致),加同样的 `routePathLocked` + `isOwnedRouteActive` + watcher 守卫。
|
||||
3. `src/views/business/waybill-manage-detail.vue`:`:detail-id="$route.query.id"` → `:detail-id="detailId"`,computed 做 `this.$route.path === '/business/waybill-manage/detail'` 守卫。
|
||||
4. `src/views/business/waybill-manage.vue`:同上,守卫 `/business/waybill-manage`(保留旧链接 `?detailId=` 兼容)。
|
||||
5. `src/views/business/transport-plan.vue`:同上,守卫 `/business/transport-plan`。
|
||||
|
||||
**验证**:
|
||||
- dev server curl 5 个 .vue + 2 个 scoped style 全部 HTTP 200。
|
||||
- 用 `@vue/server-renderer` 写了临时 mjs(已删)模拟带 `$route` 的 app,确认 **data() 里访问 `this.$route` 可行**:immediate watcher 触发时 `routePathLocked` 已是正确路径、首次加载不被拦截。
|
||||
- `$route` 是 vue-router 挂在 `app.config.globalProperties` 上的 getter(vue-router.mjs:1498),组件实例化时已就绪。
|
||||
|
||||
**未做(第 4 条可选)**:给 `getWaybillDetail` 加静默模式供 `restoreWaybillRows` 使用(历史失效运单仍会刷多条红字),待用户定夺。
|
||||
|
||||
**另注**:`business-crud-page.vue:6651` 有同构的 `detailId` immediate watcher,但全项目只有上述 3 个宿主 view 传 `:detail-id`,它暂无调用方,未动。
|
||||
|
||||
**仍未 commit。**
|
||||
|
||||
## 修复(已执行):合同详情页整页空白 —— 路由表重复定义
|
||||
|
||||
**现象**:主订单详情点合同编号 → `/business/contract-manage/detail?id=...&name=合同详情` 内容区空白(侧栏/标签正常)。
|
||||
|
||||
**定位过程(关键:真实浏览器复现,不猜)**
|
||||
1. 静态排查全部排除:`contract-manage.vue` 及其 26 个依赖 HTTP 200、模块 `import()` 成功、无循环依赖、详情分支引用的 24 个成员全部有定义。
|
||||
2. dev 环境登录态无法直接拿(验证码 + `admin/admin` 报 `error_code 2004 用户密码强度过低`),改为**注入式复现**:
|
||||
- `agent-browser open <目标 URL>`(落 `/login`);
|
||||
- eval 取 `document.getElementById('app').__vue_app__.config.globalProperties` 拿 `$router` / `$store`;
|
||||
- 写 cookie `saber3-access-token`(注意 token 走 **js-cookie**,不是 localStorage)+ `$store.state.user.token`;
|
||||
- 覆写 `XMLHttpRequest.prototype.open/send` 让所有 `/api/**` 返回 `{code:200,data:{...}}`(defineProperty 伪造 readyState/status/responseText 后手动触发 `onloadend`)。
|
||||
- 再 `$router.push(...)` 做 SPA 跳转。
|
||||
3. 复现出空白,并拿到决定性证据:
|
||||
- `/business/contract-manage/detail` → `$route.matched.length === 1`(只有 Layout,name undefined),`#avue-view.children.length === 0`,innerHTML 为 `<!---->`。
|
||||
- 对照 `/business/contract-manage/change`(结构相同但未重复定义)→ `matched.length === 2`(Layout + 合同变更),内容完整渲染。
|
||||
- `$router.getRoutes()` 里 detail 有 **3 条**记录:2 个 Layout 父记录 + 1 个子记录(`合同详情`)。
|
||||
|
||||
**根因**:`src/router/views/index.js` 中 `/business/contract-manage/detail` **被复制粘贴定义了两次**,两次的子路由都叫 `合同详情`。vue-router 的 `addRoute` 遇到重名会 `removeRoute(name)` 把先注册的子记录删掉,但**不会把它从父记录的 `children` 数组里摘除**;于是第一条 Layout 壳记录留在 matcher 列表里且排在前面,`resolve('/business/contract-manage/detail')` 命中这个"无有效子路由"的壳 → matched 只有 1 层 → `page/index/index.vue` 里 `#avue-view` 的二级 `<router-view>` 无匹配 → 内容区空白。
|
||||
(`getRoutes()` 计数是判据:form / change 都是 2 条 = 父 + 子(健康),detail 是 3 条 = 2 父 + 1 子(坏)。)
|
||||
|
||||
**修复**:删除重复块(原 314-325 行),只保留 159-169 行那一份。
|
||||
|
||||
**验证**:
|
||||
- 重复扫描脚本(正则匹配「顶层 path + `component: Layout` + 首个子路由 name」):views/index.js 27 条 Layout 子路由中仅 `合同详情` 重复;page/index.js 无重复 → 已清零。
|
||||
- 修复后浏览器复验:detail → `matched.length === 2`(Layout + 合同详情)、`#avue-view.children.length === 1`,正文完整渲染「基本信息 / 签约类型 / 合同编号 HT-2026-001 / … / 变更记录」。
|
||||
- 全量回归:48 条 business/vehicle/payment/settlement 路由 `resolve()` 后 `matched.length` 全部 ≥ 2,`bad: []`。
|
||||
- `curl` 校验:`views/index.js`、`contract-manage.vue` 均 200。
|
||||
- 残留报错均为 mock 数据形状不符所致(所有接口都返回同一个合同对象,导致 `tree.reduce` / `feeCategories.find` 之类失败),非真实缺陷。
|
||||
|
||||
**未 commit**(连同 1、2 轮改动一起待确认)。
|
||||
|
||||
+20
-33
@@ -1,40 +1,27 @@
|
||||
# 项目长期记忆(tms-erp-web-ws / Saber3)
|
||||
|
||||
配套后端:`/Users/gxwebsoft/JAVA/tms-api`(不是 tms-erp-api-ws,后者功能滞后)。
|
||||
后端:`/Users/gxwebsoft/JAVA/tms-api`(`tms-erp-api-ws` 滞后,别看错仓库)。
|
||||
|
||||
## 列表 + 独立表单页 + 独立详情页(同一组件分流)
|
||||
- 路由:`/xxx`(菜单)、`/xxx/form`、`/xxx/detail` 指向同一 .vue;form/detail 为 `component: Layout` + 空 children,`meta:{keepAlive:false}`,detail 带 `activeMenu:'/xxx'`。
|
||||
- 组件内按 `$route.path` 分流,列表部分统一 `v-if="!isStandalonePage"` 隐藏。
|
||||
- 路由记录不同但组件相同 → 必须 `watch:{$route}` 里重新 init;`mounted` 只覆盖直接打开/刷新。
|
||||
- 跳转 `push({path:'/xxx/form',query:{mode,id,name:'新增xxx'}})`,标签标题取 `query.name`。
|
||||
- 底栏:只读详情页「关闭」;表单页「关闭/暂存/确认」。
|
||||
## 列表 / 表单 / 详情三态同组件分流
|
||||
`/xxx`、`/xxx/form`、`/xxx/detail` 指向同一 `.vue`;form/detail 在 `src/router/views/index.js` 是 `component: Layout` + 空 `children.path` + `meta:{keepAlive:false}`(detail 另带 `activeMenu`)。组件内按 `$route.path` 分流,必须 `watch:{$route}` 重新 init;标签标题取 `query.name`;独立页容器固定 `<div v-if>`,不用 `el-dialog`。
|
||||
|
||||
## ⚠️ 独立页容器禁止用 `<component :is>` 在 div 与 el-dialog 间切换
|
||||
- **现象**:改成独立整页后,点左侧菜单又弹出旧弹窗,且永久盖在页面上。
|
||||
- **根因**:`src/router/tab.js` 按 fullPath 建 wrapper 组件,`layout.vue` 用 `<keep-alive :include="tagsKeep">` 缓存,每个标签一个实例。实例被 deactivate 后**仍会随 `$route` 重新渲染**;此时 `:is` 由 div 变回 `el-dialog`,`v-model` 仍为 true,`append-to-body` 的弹窗经 Teleport 渲染进 body——而 `KeepAlive.deactivate → Teleport.move` 只做 reorder,**不会把 teleport 出去的 DOM 搬回 storage container**,于是弹窗永久残留。
|
||||
- **正确写法**:独立页固定 `<div v-if="isXxxPage">` 普通容器,绝不按路由回退成 el-dialog;列表页分支另写或只保留弹窗一种形态。
|
||||
- **兜底**:页面内其它 `append-to-body` 的二级弹窗,加 `deactivated(){closeInnerDialogs()}` + `beforeUnmount(){closeInnerDialogs()}` 把 v-model 置 false。
|
||||
- 已按此修法落地:loading-manage、project-apply(2026-09-18)、waybill-manage-page(同日,锁 detailPage/formPage/formMode 三个标志)。
|
||||
- 未修同类风险:`settlement/components/{pre,formal}-settlement-editor.vue`、`transport-reconciliation-editor.vue`(`pageMode ? 'div':'el-dialog'`)、`waybill-import-dialog.vue`(`standalone`/`createPage`)。
|
||||
## ⚠️ keep-alive 三坑
|
||||
背景(`router/tab.js` + `page/index/index.vue`):按 `fullPath` 建 wrapper,`<keep-alive :include="tagsKeep">` 缓存,**每标签一实例、永不销毁**,deactivate 后**仍随全局 `$route` 重渲染**。
|
||||
|
||||
**坑一:独立页容器禁用 `<component :is>` 在 div / el-dialog 间切换。** `append-to-body` 走 Teleport,`TeleportImpl.move` 忽略 `moveType`,不会把 DOM 搬回缓存容器 → 切菜单冒出旧弹窗。修法:固定 `<div v-if>` + `deactivated(){closeInnerDialogs()}` + `beforeUnmount()`(写在子组件也生效)。已修 loading-manage / project-apply / waybill-manage-page;未修 `settlement/components/{pre,formal}-settlement-editor.vue`、`transport-reconciliation-editor.vue`、`waybill-import-dialog.vue`。
|
||||
|
||||
**坑二:宿主 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。
|
||||
|
||||
**坑三:路由表重复定义 → 内容区整页空白(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」重复项。
|
||||
|
||||
## 样式硬规则
|
||||
- 底栏:`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`(禁绿色、禁两个蓝实心)。顺序 `[辅助][取消][保存草稿][提交]`。
|
||||
- 独立页标题 `.archive-page-form__title`(18px/600 + 4px 主色竖条);分组用全局 `<section-card>`;弹窗灰底 `#f5f6fa`。
|
||||
- 上传证件区:`width:100%; max-width:240px` + `.el-upload{height:151px}`,禁写死 px。
|
||||
- ⚠️ `<style scoped lang="scss">` **顶层禁 `//` 注释**(Vite5+sass 报 `Unexpected '/'` → 样式模块 500),顶层用 `/* */`。
|
||||
- 其余全站细则见 `src/styles/element-ui.scss` 与 AGENTS.md 4.4。
|
||||
- 底栏 `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`(禁绿、禁两个蓝实心)。顺序 `[辅助][取消][保存草稿][提交]`。
|
||||
- 独立页标题 `.archive-page-form__title`;分组用全局 `<section-card>`;上传证件区 `width:100%; max-width:240px` + `.el-upload{height:151px}`。
|
||||
- ⚠️ `<style scoped lang="scss">` 顶层禁 `//` 注释(Vite5 + sass `Unexpected '/'` → 500),顶层用 `/* */`。其余见 `src/styles/element-ui.scss` 与 `AGENTS.md` 4.4。
|
||||
|
||||
## 表单文案
|
||||
- placeholder 只写 `请输入`/`请选择`;例外保留:`请输入或选择车辆`、日期区间 `起/止`、示例值(`如:京A12345`)。校验 message 必须带字段名。
|
||||
|
||||
## 复用与自检
|
||||
- `business/components/business-crud-page.vue`:option 经 `cloneOption` 克隆;独立表单页走 `PageAvueForm`;详情弹窗 `detailButton + detailSections`。
|
||||
- 自检:dev(2889) 时 `curl "localhost:2889/src/xxx.vue"` 与 `curl "...?vue&type=style&index=0&scoped=true&lang.scss"`,200 通过、500 返回带堆栈错误页。
|
||||
- ⚠️ `vite build` 会在 `src/page/login/facelogin.vue` 被沙箱敏感内容保护中断(环境问题),不能作为唯一校验手段。
|
||||
- dev:`VITE_APP_API=/api`,代理 `172.16.203.228:8000`。
|
||||
|
||||
## 导出(后端 FastExcel)
|
||||
- 前端 `exportColumns` 后端未使用,导出列以 `XxxExportExcel.java` 为准。
|
||||
- BladeX `BeanUtil` 类型不兼容时静默跳过(`Date createTime` → `LocalDateTime` 丢值),须 Service 内 `DateUtil.fromDate(...)` 赋值。
|
||||
- 排查导出文件用 Python `zipfile` 读 `xl/worksheets/sheet1.xml`。
|
||||
## 表单 / 自检 / 导出
|
||||
- placeholder 只写 `请输入`/`请选择`;例外:`请输入或选择车辆`、日期区间 `起`/`止`、示例值。校验 message 须带字段名。
|
||||
- `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`)。
|
||||
|
||||
@@ -33,6 +33,25 @@ export const getPunchRecords = waybillId =>
|
||||
method: 'get',
|
||||
params: { waybillId },
|
||||
});
|
||||
|
||||
/** 运单车辆实时定位 */
|
||||
export const locateVehicle = id =>
|
||||
request({
|
||||
url: `${baseUrl}/locate`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
/** 运单车辆历史轨迹 */
|
||||
export const trackVehicle = (id, startDate, endDate) =>
|
||||
request({
|
||||
url: `${baseUrl}/track`,
|
||||
method: 'post',
|
||||
params: { id, startDate, endDate },
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
export const roadLoading = ids =>
|
||||
request({
|
||||
url: `${baseUrl}/road-loading`,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import request from '@/axios';
|
||||
|
||||
/**
|
||||
* 调用 MK processSubmit 提交审核流
|
||||
* @param {Object} data
|
||||
* @param {string} data.templateCode 模板编码
|
||||
* @param {string} data.submitIdentity 提交人(手机号)
|
||||
* @param {string} data.loginName 登录账号(手机号)
|
||||
* @param {string} data.formInstanceId 业务表单实例 id
|
||||
* @param {string} [data.subject] 流程标题
|
||||
*/
|
||||
export const processSubmit = data => {
|
||||
return request({
|
||||
url: '/blade-system/businessProcess/processSubmit',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
};
|
||||
@@ -48,6 +48,32 @@ export const syncIamOrganizations = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const syncOaCompany = (current = 1, size = 20, signal) => {
|
||||
return request({
|
||||
url: '/blade-system/dept/sync-oa-company',
|
||||
method: 'post',
|
||||
params: {
|
||||
current,
|
||||
size,
|
||||
},
|
||||
timeout: 60000,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const syncOaDepartment = (current = 1, size = 20, signal) => {
|
||||
return request({
|
||||
url: '/blade-system/dept/sync-oa-department',
|
||||
method: 'post',
|
||||
params: {
|
||||
current,
|
||||
size,
|
||||
},
|
||||
timeout: 60000,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const update = row => {
|
||||
return request({
|
||||
url: '/blade-system/dept/submit',
|
||||
|
||||
@@ -31,10 +31,16 @@ export const add = row => {
|
||||
});
|
||||
};
|
||||
|
||||
export const syncIamAccounts = () => {
|
||||
export const syncIamAccounts = (current = 1, size = 50, signal) => {
|
||||
return request({
|
||||
url: '/blade-system/user/sync-iam-accounts',
|
||||
method: 'post',
|
||||
params: {
|
||||
current,
|
||||
size,
|
||||
},
|
||||
timeout: 60000,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,19 @@ export const getDetail = id => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getPublicDetail = id => {
|
||||
return request({
|
||||
url: '/blade-transport/customer-archive/public/detail',
|
||||
method: 'get',
|
||||
meta: {
|
||||
isToken: false,
|
||||
},
|
||||
params: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getChangeRecordList = (customerId, current, size) => {
|
||||
return request({
|
||||
url: '/blade-transport/customer-archive/change-record/list',
|
||||
@@ -34,6 +47,32 @@ export const getChangeRecordList = (customerId, current, size) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getPublicChangeRecordList = (customerId, current, size) => {
|
||||
return request({
|
||||
url: '/blade-transport/customer-archive/public/change-record/list',
|
||||
method: 'get',
|
||||
meta: {
|
||||
isToken: false,
|
||||
},
|
||||
params: {
|
||||
customerId,
|
||||
current,
|
||||
size,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const postPublicProcessMessage = data => {
|
||||
return request({
|
||||
url: '/blade-transport/customer-archive/public/process-message',
|
||||
method: 'post',
|
||||
meta: {
|
||||
isToken: false,
|
||||
},
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const submit = row => {
|
||||
return request({
|
||||
url: '/blade-transport/customer-archive/submit',
|
||||
|
||||
@@ -38,6 +38,20 @@ export const createOption = () => ({
|
||||
menuWidth: 240,
|
||||
menuFixed: 'right',
|
||||
column: [
|
||||
{
|
||||
label: '计量单位编码',
|
||||
prop: 'unitCode',
|
||||
minWidth: 150,
|
||||
search: true,
|
||||
searchOrder: 4,
|
||||
searchSpan: 6,
|
||||
maxlength: 50,
|
||||
showWordLimit: true,
|
||||
rules: [
|
||||
{ required: true, message: '请输入计量单位编码', trigger: 'blur' },
|
||||
{ max: 50, message: '计量单位编码不能超过50个字', trigger: 'blur' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '计量单位',
|
||||
prop: 'unitName',
|
||||
|
||||
@@ -3,6 +3,8 @@ import { getToken } from '@/utils/auth';
|
||||
import store from '@/store';
|
||||
import { generateIframePath, processUrlForQuery, isURL } from './router';
|
||||
import { wrapViewLoader } from '@/utils/chunk-reload';
|
||||
|
||||
// 保持懒加载,避免 eager 与 store 循环依赖(Cannot access 'store' before initialization)
|
||||
const modules = import.meta.glob('../**/**/*.vue');
|
||||
|
||||
// 将多级路由扁平化为二级路由,支持 keep-alive 跨层级缓存
|
||||
|
||||
@@ -74,6 +74,16 @@ export default [
|
||||
isAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/vehicle/customer-archive/public-view',
|
||||
name: '查看客商信息',
|
||||
component: () => import('@/views/vehicle/customer-archive-public-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
isTab: false,
|
||||
isAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: '主页',
|
||||
|
||||
+47
-51
@@ -1,5 +1,8 @@
|
||||
import Layout from '@/page/index/index.vue';
|
||||
import Store from '@/store/';
|
||||
import { wrapViewLoader } from '@/utils/chunk-reload';
|
||||
|
||||
const loadView = loader => wrapViewLoader(loader);
|
||||
|
||||
export default [
|
||||
{
|
||||
@@ -10,7 +13,7 @@ export default [
|
||||
path: '',
|
||||
name: '汇票付款表单',
|
||||
meta: { keepAlive: false, activeMenu: '/payment/bill-payment' },
|
||||
component: () => import('@/views/payment/bill-payment-form.vue'),
|
||||
component: loadView(() => import('@/views/payment/bill-payment-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -22,7 +25,7 @@ export default [
|
||||
path: '',
|
||||
name: '汇票台账表单',
|
||||
meta: { keepAlive: false, activeMenu: '/payment/bill-ledger' },
|
||||
component: () => import('@/views/payment/bill-ledger-form.vue'),
|
||||
component: loadView(() => import('@/views/payment/bill-ledger-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -34,7 +37,7 @@ export default [
|
||||
path: '',
|
||||
name: '认领记录详情',
|
||||
meta: { keepAlive: false, activeMenu: '/payment/receipt-claim-record' },
|
||||
component: () => import('@/views/payment/receipt-claim-record-form.vue'),
|
||||
component: loadView(() => import('@/views/payment/receipt-claim-record-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -46,7 +49,7 @@ export default [
|
||||
path: '',
|
||||
name: '收款流水认领',
|
||||
meta: { keepAlive: false, activeMenu: '/payment/receipt-flow' },
|
||||
component: () => import('@/views/payment/receipt-flow-form.vue'),
|
||||
component: loadView(() => import('@/views/payment/receipt-flow-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -58,7 +61,7 @@ export default [
|
||||
path: '',
|
||||
name: '收票登记',
|
||||
meta: { keepAlive: false, activeMenu: '/payment/invoice-receipt' },
|
||||
component: () => import('@/views/payment/invoice-receipt-form.vue'),
|
||||
component: loadView(() => import('@/views/payment/invoice-receipt-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -70,7 +73,7 @@ export default [
|
||||
path: '',
|
||||
name: '开票申请',
|
||||
meta: { keepAlive: false, activeMenu: '/payment/invoice-application' },
|
||||
component: () => import('@/views/payment/invoice-application-form.vue'),
|
||||
component: loadView(() => import('@/views/payment/invoice-application-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -82,7 +85,7 @@ export default [
|
||||
path: '',
|
||||
name: '付款申请',
|
||||
meta: { keepAlive: false, activeMenu: '/payment/payment-application' },
|
||||
component: () => import('@/views/payment/payment-application-form.vue'),
|
||||
component: loadView(() => import('@/views/payment/payment-application-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -97,7 +100,7 @@ export default [
|
||||
keepAlive: false,
|
||||
activeMenu: '/settlement/pre-settlement',
|
||||
},
|
||||
component: () => import('@/views/settlement/pre-settlement-form.vue'),
|
||||
component: loadView(() => import('@/views/settlement/pre-settlement-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -112,7 +115,7 @@ export default [
|
||||
keepAlive: false,
|
||||
activeMenu: '/settlement/formal-settlement',
|
||||
},
|
||||
component: () => import('@/views/settlement/formal-settlement-form.vue'),
|
||||
component: loadView(() => import('@/views/settlement/formal-settlement-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -127,7 +130,7 @@ export default [
|
||||
keepAlive: false,
|
||||
activeMenu: '/settlement/transport-reconciliation',
|
||||
},
|
||||
component: () => import('@/views/settlement/transport-reconciliation-form.vue'),
|
||||
component: loadView(() => import('@/views/settlement/transport-reconciliation-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -139,7 +142,7 @@ export default [
|
||||
path: '',
|
||||
name: '执行凭证批次详情',
|
||||
meta: { keepAlive: false, activeMenu: '/business/voucher-manage' },
|
||||
component: () => import('@/views/business/voucher-manage-detail.vue'),
|
||||
component: loadView(() => import('@/views/business/voucher-manage-detail.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -151,7 +154,7 @@ export default [
|
||||
path: '',
|
||||
name: '新增合同管理',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/contract-manage.vue'),
|
||||
component: loadView(() => import('@/views/business/contract-manage.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -163,7 +166,7 @@ export default [
|
||||
path: '',
|
||||
name: '合同详情',
|
||||
meta: { keepAlive: false, activeMenu: '/business/contract-manage' },
|
||||
component: () => import('@/views/business/contract-manage.vue'),
|
||||
component: loadView(() => import('@/views/business/contract-manage.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -175,7 +178,7 @@ export default [
|
||||
path: '',
|
||||
name: '新增项目申请',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/project-apply.vue'),
|
||||
component: loadView(() => import('@/views/business/project-apply.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -187,7 +190,7 @@ export default [
|
||||
path: '',
|
||||
name: '新增编辑运单管理',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/waybill-manage.vue'),
|
||||
component: loadView(() => import('@/views/business/waybill-manage.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -199,7 +202,7 @@ export default [
|
||||
path: '',
|
||||
name: '运单管理详情',
|
||||
meta: { keepAlive: false, activeMenu: '/business/waybill-manage' },
|
||||
component: () => import('@/views/business/waybill-manage-detail.vue'),
|
||||
component: loadView(() => import('@/views/business/waybill-manage-detail.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -211,7 +214,7 @@ export default [
|
||||
path: '',
|
||||
name: '导入运单',
|
||||
meta: { keepAlive: false, activeMenu: '/business/waybill-import' },
|
||||
component: () => import('@/views/business/waybill-import.vue'),
|
||||
component: loadView(() => import('@/views/business/waybill-import.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -223,7 +226,7 @@ export default [
|
||||
path: '',
|
||||
name: '新建导入运单',
|
||||
meta: { keepAlive: false, activeMenu: '/business/waybill-import' },
|
||||
component: () => import('@/views/business/waybill-import-form.vue'),
|
||||
component: loadView(() => import('@/views/business/waybill-import-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -235,7 +238,7 @@ export default [
|
||||
path: '',
|
||||
name: '新增编辑配载管理',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/loading-manage.vue'),
|
||||
component: loadView(() => import('@/views/business/loading-manage.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -259,7 +262,7 @@ export default [
|
||||
path: '',
|
||||
name: '新增编辑运输计划',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/transport-plan.vue'),
|
||||
component: loadView(() => import('@/views/business/transport-plan.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -271,7 +274,7 @@ export default [
|
||||
path: '',
|
||||
name: '导入运输计划',
|
||||
meta: { keepAlive: false, activeMenu: '/business/transport-plan' },
|
||||
component: () => import('@/views/business/transport-plan-import.vue'),
|
||||
component: loadView(() => import('@/views/business/transport-plan-import.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -283,7 +286,7 @@ export default [
|
||||
path: '',
|
||||
name: '计划调度',
|
||||
meta: { keepAlive: false, activeMenu: '/business/transport-plan' },
|
||||
component: () => import('@/views/business/transport-plan-dispatch.vue'),
|
||||
component: loadView(() => import('@/views/business/transport-plan-dispatch.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -295,7 +298,7 @@ export default [
|
||||
path: '',
|
||||
name: '新增编辑运单模板',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/shipping-template.vue'),
|
||||
component: loadView(() => import('@/views/business/shipping-template.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -307,19 +310,7 @@ export default [
|
||||
path: '',
|
||||
name: '新增客商档案',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/vehicle/customer-archive.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/business/contract-manage/detail',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: '合同详情',
|
||||
meta: { keepAlive: false, activeMenu: '/business/contract-manage' },
|
||||
component: () => import('@/views/business/contract-manage.vue'),
|
||||
component: loadView(() => import('@/views/vehicle/customer-archive.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -331,7 +322,7 @@ export default [
|
||||
path: '',
|
||||
name: '合同变更',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/contract-manage-change.vue'),
|
||||
component: loadView(() => import('@/views/business/contract-manage-change.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -347,7 +338,7 @@ export default [
|
||||
meta: {
|
||||
i18n: 'dashboard',
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "views" */ '@/views/wel/index.vue'),
|
||||
component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/wel/index.vue')),
|
||||
},
|
||||
{
|
||||
path: 'dashboard',
|
||||
@@ -356,7 +347,7 @@ export default [
|
||||
i18n: 'dashboard',
|
||||
menu: false,
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "views" */ '@/views/wel/dashboard.vue'),
|
||||
component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/wel/dashboard.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -371,7 +362,7 @@ export default [
|
||||
meta: {
|
||||
i18n: 'test',
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "views" */ '@/views/util/test.vue'),
|
||||
component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/util/test.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -386,8 +377,9 @@ export default [
|
||||
meta: {
|
||||
i18n: 'dict',
|
||||
},
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-horizontal.vue'),
|
||||
component: loadView(() =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-horizontal.vue')
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -402,8 +394,9 @@ export default [
|
||||
meta: {
|
||||
i18n: 'dict',
|
||||
},
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-vertical.vue'),
|
||||
component: loadView(() =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-vertical.vue')
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -418,7 +411,7 @@ export default [
|
||||
meta: {
|
||||
i18n: 'info',
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "views" */ '@/views/system/userinfo.vue'),
|
||||
component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/system/userinfo.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -433,8 +426,9 @@ export default [
|
||||
meta: {
|
||||
i18n: 'work',
|
||||
},
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/form.vue'),
|
||||
component: loadView(() =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/form.vue')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'handle/:taskId/:processInstanceId/:businessId',
|
||||
@@ -442,8 +436,9 @@ export default [
|
||||
meta: {
|
||||
i18n: 'work',
|
||||
},
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/handle.vue'),
|
||||
component: loadView(() =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/handle.vue')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'detail/:processInstanceId/:businessId',
|
||||
@@ -451,8 +446,9 @@ export default [
|
||||
meta: {
|
||||
i18n: 'work',
|
||||
},
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/detail.vue'),
|
||||
component: loadView(() =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/detail.vue')
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -119,6 +119,7 @@ export default {
|
||||
return this.isAdmin || this.permission?.[code] === true;
|
||||
},
|
||||
normalizeRow(row) {
|
||||
row.unitCode = String(row.unitCode || '').trim();
|
||||
row.unitName = String(row.unitName || '').trim();
|
||||
row.dimension = String(row.dimension || '').trim();
|
||||
row.remark = String(row.remark || '').trim();
|
||||
|
||||
@@ -157,12 +157,19 @@
|
||||
<el-table-column label="计费单位" width="150"
|
||||
><template #default="{ row }"
|
||||
><span v-if="readonly">{{ displayValue(row.billingUnit) }}</span
|
||||
><el-select v-else v-model="row.billingUnit" clearable filterable :loading="unitLoading"
|
||||
><el-select
|
||||
v-else
|
||||
v-model="row.billingUnit"
|
||||
clearable
|
||||
filterable
|
||||
:disabled="!row.billingElement"
|
||||
:loading="unitLoading"
|
||||
:placeholder="row.billingElement ? '请选择' : '请先选择计费要素'"
|
||||
><el-option
|
||||
v-for="item in unitOptions"
|
||||
:key="item.id || item.dictKey || item.dictValue"
|
||||
:label="item.dictValue"
|
||||
:value="item.dictValue" /></el-select></template
|
||||
v-for="item in unitOptionsFor(row)"
|
||||
:key="item.id || item.value"
|
||||
:label="item.label"
|
||||
:value="item.value" /></el-select></template
|
||||
></el-table-column>
|
||||
<el-table-column label="单价(元)" width="180"
|
||||
><template #default="{ row }"
|
||||
@@ -367,12 +374,19 @@
|
||||
<script>
|
||||
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||
import { getList as getFeeItemList } from '@/api/base/fee-item';
|
||||
import { getList as getMeasurementUnitList } from '@/api/base/measurement-unit';
|
||||
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';
|
||||
|
||||
const clone = value => JSON.parse(JSON.stringify(value));
|
||||
/** 计费要素与计量单位维度的对应关系 */
|
||||
const BILLING_ELEMENT_DIMENSION_MAP = {
|
||||
按重量: '重量',
|
||||
按体积: '体积',
|
||||
按车辆: '数量',
|
||||
};
|
||||
const defaultRule = () => ({
|
||||
feeType: '',
|
||||
feeItem: '',
|
||||
@@ -417,6 +431,7 @@ export default {
|
||||
feeItems: {},
|
||||
feeItemLoadingMap: {},
|
||||
unitOptions: [],
|
||||
measurementUnits: [],
|
||||
unitLoading: false,
|
||||
billingElements: [
|
||||
'按重量',
|
||||
@@ -614,15 +629,54 @@ export default {
|
||||
.finally(() => {
|
||||
this.feeCategoryLoading = false;
|
||||
});
|
||||
this.loadUnitOptions();
|
||||
},
|
||||
loadUnitOptions() {
|
||||
this.unitLoading = true;
|
||||
getDictionary({ code: 'unit_fee' })
|
||||
.then(res => {
|
||||
Promise.all([
|
||||
getDictionary({ code: 'unit_fee' }).then(res => {
|
||||
this.unitOptions = res.data?.data || [];
|
||||
})
|
||||
.finally(() => {
|
||||
}),
|
||||
getMeasurementUnitList(1, 9999, { status: 1 }).then(res => {
|
||||
const data = res?.data?.data || res?.data || {};
|
||||
const records = Array.isArray(data) ? data : data.records || [];
|
||||
this.measurementUnits = records.filter(
|
||||
item => item.status === undefined || item.status === null || Number(item.status) === 1
|
||||
);
|
||||
}),
|
||||
]).finally(() => {
|
||||
this.unitLoading = false;
|
||||
});
|
||||
},
|
||||
measurementDimension(billingElement) {
|
||||
return BILLING_ELEMENT_DIMENSION_MAP[billingElement] || '';
|
||||
},
|
||||
unitOptionsFor(row) {
|
||||
const dimension = this.measurementDimension(row?.billingElement);
|
||||
if (dimension) {
|
||||
return this.measurementUnits
|
||||
.filter(item => String(item.dimension || '').trim() === dimension)
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
label: item.unitName,
|
||||
value: item.unitName,
|
||||
}))
|
||||
.filter(item => item.value);
|
||||
}
|
||||
return (this.unitOptions || [])
|
||||
.map(item => ({
|
||||
id: item.id || item.dictKey || item.dictValue,
|
||||
label: item.dictValue,
|
||||
value: item.dictValue,
|
||||
}))
|
||||
.filter(item => item.value);
|
||||
},
|
||||
syncBillingUnit(row) {
|
||||
const options = this.unitOptionsFor(row);
|
||||
if (!options.some(item => String(item.value) === String(row.billingUnit || ''))) {
|
||||
row.billingUnit = '';
|
||||
}
|
||||
},
|
||||
feeTypeKey(row) {
|
||||
const option = this.feeCategories.find(
|
||||
item =>
|
||||
@@ -699,7 +753,14 @@ export default {
|
||||
return this.typeMap[row.billingElement] || [];
|
||||
},
|
||||
handleElementChange(row) {
|
||||
if (!this.billingTypes(row).includes(row.billingType)) row.billingType = '';
|
||||
const billingTypes = this.billingTypes(row);
|
||||
if (!billingTypes.includes(row.billingType)) {
|
||||
row.billingType =
|
||||
row.billingElement === '固定金额(整单一口价)' && billingTypes.includes('固定一口价')
|
||||
? '固定一口价'
|
||||
: '';
|
||||
}
|
||||
this.syncBillingUnit(row);
|
||||
if (!this.canEditMinimum(row)) {
|
||||
row.minimumBillingWeight = '';
|
||||
row.limitRanges = (row.limitRanges || []).map(item => ({
|
||||
|
||||
@@ -3020,6 +3020,10 @@ export default {
|
||||
Location,
|
||||
OfficeBuilding,
|
||||
Search,
|
||||
// 实例所属路由的路径快照,用于在 watcher / 跳转前判断当前路由是否还是自己的。
|
||||
// 必须放 data:Options API 的顺序是 data → computed → watch(immediate) → created,
|
||||
// 放 created 的话首个 immediate watcher 会读到空值,把首次加载也挡掉。
|
||||
routePathLocked: this.$route.path,
|
||||
form: {},
|
||||
suppressTransportTypeClear: false,
|
||||
query: {},
|
||||
@@ -3596,6 +3600,11 @@ export default {
|
||||
!this.dialogReadonly
|
||||
);
|
||||
},
|
||||
// 当前路由是否仍属于本实例(tab.js 按 fullPath 建实例,一个实例只对应一个 path)。
|
||||
// 实例被 keep-alive 缓存后 $route 已跑到别的页面时,不能用「当前页面的 id」去查本页数据。
|
||||
isOwnedRouteActive() {
|
||||
return this.$route.path === this.routePathLocked;
|
||||
},
|
||||
billingCargoTypeCascaderProps() {
|
||||
return {
|
||||
label: 'cargoName',
|
||||
@@ -3673,6 +3682,9 @@ export default {
|
||||
detailId: {
|
||||
immediate: true,
|
||||
handler(id) {
|
||||
// 宿主 view 传的是全局 $route.query.detailId,必须确认当前路由还是自己的,
|
||||
// 否则跳到别的页面后会把「别人的 id」灌进来触发详情请求。
|
||||
if (!this.isOwnedRouteActive) return;
|
||||
if (id !== undefined && id !== null && id !== '') {
|
||||
this.$nextTick(() => this.openDetail({ id }));
|
||||
}
|
||||
|
||||
@@ -199,13 +199,17 @@
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="detailVisible" title="导入运单详情" width="90%" append-to-body>
|
||||
<section-card class="waybill-search">
|
||||
<el-form :inline="true" :model="detailQuery"
|
||||
><el-form-item label="车牌号/船号"
|
||||
>
|
||||
<el-form-item label="车牌号/船号"
|
||||
><el-input v-model="detailQuery.vehicleNo" clearable /></el-form-item
|
||||
><el-form-item label="司机/船长姓名"
|
||||
><el-input v-model="detailQuery.driverName" clearable placeholder="请输入" /></el-form-item
|
||||
><el-button type="primary" @click="loadDetails">查询</el-button></el-form
|
||||
>
|
||||
</section-card>
|
||||
|
||||
<el-table :data="details" border
|
||||
><el-table-column type="index" label="序号" width="65" /><el-table-column
|
||||
prop="batchNo"
|
||||
@@ -593,6 +597,7 @@ import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import * as api from '@/api/business/waybill-manage';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import SectionCard from '@/components/section-card/main.vue';
|
||||
|
||||
const props = defineProps({ modelValue: Boolean, standalone: Boolean, createPage: Boolean });
|
||||
const emit = defineEmits(['update:modelValue', 'closed']);
|
||||
@@ -1617,4 +1622,7 @@ const confirmImport = async () => {
|
||||
:global(.avue-layout--horizontal .waybill-import-create__footer) {
|
||||
left: 0;
|
||||
}
|
||||
.waybill-search .el-form-item{
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2212,13 +2212,105 @@
|
||||
<section-card title="物流轨迹">
|
||||
<template #extra>
|
||||
<div class="waybill-manage-page__waybill-track-actions">
|
||||
<el-button plain @click="handleWaybillTrackAction('playback')"
|
||||
<el-button
|
||||
plain
|
||||
:type="waybillTrackMode === 'playback' ? 'primary' : undefined"
|
||||
:loading="waybillTrackLoading"
|
||||
@click="handleWaybillTrackAction('playback')"
|
||||
>轨迹回放</el-button
|
||||
>
|
||||
<el-button plain @click="handleWaybillTrackAction('locate')">实时定位</el-button>
|
||||
<el-button
|
||||
plain
|
||||
:type="waybillTrackMode === 'locate' ? 'primary' : undefined"
|
||||
:loading="waybillLocateLoading"
|
||||
@click="handleWaybillTrackAction('locate')"
|
||||
>实时定位</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<div class="waybill-manage-page__waybill-map-empty">暂无数据</div>
|
||||
<div
|
||||
v-if="waybillTrackMode === 'playback'"
|
||||
class="waybill-manage-page__waybill-track-toolbar"
|
||||
>
|
||||
<span>时间段</span>
|
||||
<el-date-picker
|
||||
v-model="waybillTrackDateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
:clearable="false"
|
||||
:disabled="waybillTrackLoading"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="waybillTrackLoading"
|
||||
@click="loadWaybillTrack"
|
||||
>查询</el-button
|
||||
>
|
||||
<span class="waybill-manage-page__waybill-track-total"
|
||||
>轨迹点 {{ waybillTrackInfo?.total || 0 }} 个</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="waybillTrackMode === 'locate'"
|
||||
class="waybill-manage-page__waybill-locate"
|
||||
>
|
||||
<div v-if="waybillLocateInfo" class="waybill-manage-page__waybill-locate-meta">
|
||||
<div>
|
||||
<span>车牌号</span><strong>{{ waybillLocateInfo.vehicleNo || '-' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>定位时间</span><strong>{{ waybillLocateInfo.locateTime || '-' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>速度</span><strong>{{ waybillLocateInfo.speed || '-' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>方向</span><strong>{{ waybillLocateInfo.direction || '-' }}</strong>
|
||||
</div>
|
||||
<div class="waybill-manage-page__waybill-locate-address">
|
||||
<span>地址</span><strong>{{ waybillLocateInfo.address || '-' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>坐标</span
|
||||
><strong
|
||||
>{{
|
||||
waybillLocateInfo.longitude != null && waybillLocateInfo.latitude != null
|
||||
? `${waybillLocateInfo.longitude}, ${waybillLocateInfo.latitude}`
|
||||
: '-'
|
||||
}}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="waybillLocateMap"
|
||||
class="waybill-manage-page__waybill-locate-map"
|
||||
></div>
|
||||
<div
|
||||
v-if="!waybillLocateInfo"
|
||||
class="waybill-manage-page__waybill-map-tip"
|
||||
>
|
||||
{{ waybillLocateHint || '暂无数据' }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="waybillTrackMode === 'playback'"
|
||||
class="waybill-manage-page__waybill-locate"
|
||||
>
|
||||
<div
|
||||
ref="waybillTrackMap"
|
||||
class="waybill-manage-page__waybill-locate-map waybill-manage-page__waybill-locate-map--track"
|
||||
></div>
|
||||
<div
|
||||
v-if="!(waybillTrackInfo?.points || []).length"
|
||||
class="waybill-manage-page__waybill-map-tip"
|
||||
>
|
||||
{{ waybillTrackHint || '暂无数据' }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="waybill-manage-page__waybill-map-empty">暂无数据</div>
|
||||
</section-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3029,7 +3121,7 @@ import {
|
||||
getList as getProcessConfigList,
|
||||
getVoucherImages as getProcessConfigVoucherImages,
|
||||
} from '@/api/business/process-config';
|
||||
import { getPunchRecords as getWaybillPunchRecords } from '@/api/business/waybill-manage';
|
||||
import { getPunchRecords as getWaybillPunchRecords, locateVehicle, trackVehicle } from '@/api/business/waybill-manage';
|
||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
@@ -3292,6 +3384,21 @@ export default {
|
||||
waybillRouteChangeRecords: [],
|
||||
waybillRouteChangeDragIndex: -1,
|
||||
waybillRouteChangeSaving: false,
|
||||
waybillLocateLoading: false,
|
||||
waybillLocateInfo: null,
|
||||
waybillLocateHint: '',
|
||||
waybillLocateMapInstance: null,
|
||||
waybillLocateMarker: null,
|
||||
waybillLocateInfoWindow: null,
|
||||
waybillTrackMode: '',
|
||||
waybillTrackLoading: false,
|
||||
waybillTrackInfo: null,
|
||||
waybillTrackHint: '',
|
||||
waybillTrackDateRange: [],
|
||||
waybillTrackMapInstance: null,
|
||||
waybillTrackPolyline: null,
|
||||
waybillTrackStartMarker: null,
|
||||
waybillTrackEndMarker: null,
|
||||
mileageDialog: {
|
||||
visible: false,
|
||||
submitting: false,
|
||||
@@ -3518,6 +3625,11 @@ export default {
|
||||
selectionList: [],
|
||||
attachmentUploadMode: false,
|
||||
standaloneFormKey: '',
|
||||
// 实例所属路由的路径快照:用于判断当前 $route 是否还是「本实例自己的路由」。
|
||||
// 必须在 data 里初始化,不能放 created —— Vue 的 Options API 执行顺序是
|
||||
// data → computed → watch(immediate) → created,放 created 会被首个
|
||||
// immediate watcher 读到空值,把本该执行的首次加载也挡掉。
|
||||
routePathLocked: this.$route.path,
|
||||
// 页面形态(列表 / 独立表单页 / 独立详情页)在实例创建时锁定一次,之后不再随 $route 变化。
|
||||
// 原因:标签页 keep-alive 只会让实例 deactivate,实例仍会随 $route(全局响应式)重新渲染;
|
||||
// 若形态跟着路由翻回列表/弹窗态,缓存实例会渲染出 append-to-body 的 el-dialog —— 弹窗被
|
||||
@@ -3600,6 +3712,12 @@ export default {
|
||||
return this.standaloneDetailPage && this.$route.path === standaloneWaybillDetailRoute;
|
||||
},
|
||||
// 容器形态读实例创建时锁定的标志,不随 $route 翻转(详见 data 中 formPageLocked 的说明)
|
||||
// 当前路由是否仍属于本实例(tab.js 按 fullPath 建实例,一个实例只对应一个 path)。
|
||||
// 被 keep-alive 缓存后如果 $route 已经跑到别的页面上,就不该再用「当前页面的 id」
|
||||
// 去触发本实例的请求或跳转,否则会拿别的单据 id 查自己的接口。
|
||||
isOwnedRouteActive() {
|
||||
return this.$route.path === this.routePathLocked;
|
||||
},
|
||||
detailContainer() {
|
||||
return this.detailPageLocked ? 'PageDetail' : 'el-dialog';
|
||||
},
|
||||
@@ -3867,6 +3985,10 @@ export default {
|
||||
detailId: {
|
||||
immediate: true,
|
||||
handler(id) {
|
||||
// 宿主 view 传的是全局 $route.query.id / detailId,实例被缓存后仍会被重渲染,
|
||||
// 必须确认当前路由还是自己的:否则跳到别的详情页时会把「别的单据 id」灌进来,
|
||||
// 触发 openDetail 用错 id 查运单 → 报「运单管理不存在」。
|
||||
if (!this.isOwnedRouteActive) return;
|
||||
if (id !== undefined && id !== null && id !== '') {
|
||||
this.$nextTick(() => this.openDetail({ id }));
|
||||
}
|
||||
@@ -4449,6 +4571,9 @@ export default {
|
||||
},
|
||||
openDetail(row) {
|
||||
if (!this.isStandaloneWaybillDetailPage) {
|
||||
// 已不在自己的路由上(被缓存后路由切走):此时 row.id 极可能是别的单据的 id,
|
||||
// 再 push 会生成一堆错误的「运单管理详情」标签
|
||||
if (!this.isOwnedRouteActive) return;
|
||||
this.$router.push({ path: standaloneWaybillDetailRoute, query: { id: row.id } });
|
||||
return;
|
||||
}
|
||||
@@ -4505,6 +4630,13 @@ export default {
|
||||
});
|
||||
},
|
||||
closeDetail() {
|
||||
this.destroyWaybillLocateMap();
|
||||
this.destroyWaybillTrackMap();
|
||||
this.waybillTrackMode = '';
|
||||
this.waybillLocateInfo = null;
|
||||
this.waybillLocateHint = '';
|
||||
this.waybillTrackInfo = null;
|
||||
this.waybillTrackHint = '';
|
||||
if (this.isStandaloneWaybillDetailPage) {
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push({ path: '/business/waybill-manage', query: {} });
|
||||
@@ -4849,7 +4981,262 @@ export default {
|
||||
}
|
||||
},
|
||||
handleWaybillTrackAction(action) {
|
||||
this.$message.info(action === 'playback' ? '暂无可回放的物流轨迹' : '暂无车辆实时定位数据');
|
||||
if (action === 'playback') {
|
||||
this.destroyWaybillLocateMap();
|
||||
this.waybillTrackMode = 'playback';
|
||||
this.waybillLocateInfo = null;
|
||||
this.waybillLocateHint = '';
|
||||
if (!Array.isArray(this.waybillTrackDateRange) || this.waybillTrackDateRange.length !== 2) {
|
||||
this.waybillTrackDateRange = this.buildDefaultTrackDateRange();
|
||||
}
|
||||
this.loadWaybillTrack();
|
||||
return;
|
||||
}
|
||||
this.destroyWaybillTrackMap();
|
||||
this.waybillTrackMode = 'locate';
|
||||
this.waybillTrackInfo = null;
|
||||
this.waybillTrackHint = '';
|
||||
this.loadWaybillLocate();
|
||||
},
|
||||
buildDefaultTrackDateRange() {
|
||||
const pad = value => String(value).padStart(2, '0');
|
||||
const formatDate = date =>
|
||||
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1);
|
||||
return [formatDate(yesterday), formatDate(today)];
|
||||
},
|
||||
destroyWaybillLocateMap() {
|
||||
if (this.waybillLocateMarker) {
|
||||
this.waybillLocateMarker.setMap(null);
|
||||
this.waybillLocateMarker = null;
|
||||
}
|
||||
if (this.waybillLocateInfoWindow) {
|
||||
this.waybillLocateInfoWindow.close();
|
||||
this.waybillLocateInfoWindow = null;
|
||||
}
|
||||
if (this.waybillLocateMapInstance) {
|
||||
this.waybillLocateMapInstance.destroy();
|
||||
this.waybillLocateMapInstance = null;
|
||||
}
|
||||
},
|
||||
destroyWaybillTrackMap() {
|
||||
if (this.waybillTrackPolyline) {
|
||||
this.waybillTrackPolyline.setMap(null);
|
||||
this.waybillTrackPolyline = null;
|
||||
}
|
||||
if (this.waybillTrackStartMarker) {
|
||||
this.waybillTrackStartMarker.setMap(null);
|
||||
this.waybillTrackStartMarker = null;
|
||||
}
|
||||
if (this.waybillTrackEndMarker) {
|
||||
this.waybillTrackEndMarker.setMap(null);
|
||||
this.waybillTrackEndMarker = null;
|
||||
}
|
||||
if (this.waybillTrackMapInstance) {
|
||||
this.waybillTrackMapInstance.destroy();
|
||||
this.waybillTrackMapInstance = null;
|
||||
}
|
||||
},
|
||||
async loadWaybillLocate() {
|
||||
const waybillId = this.detailRow?.id;
|
||||
if (!waybillId) {
|
||||
this.$message.warning('运单信息不完整');
|
||||
return;
|
||||
}
|
||||
if (!this.detailRow.vehicleNo) {
|
||||
this.$message.warning('运单未绑定车牌号,无法实时定位');
|
||||
return;
|
||||
}
|
||||
this.waybillLocateLoading = true;
|
||||
this.waybillLocateHint = '正在获取车辆实时定位...';
|
||||
this.waybillLocateInfo = null;
|
||||
try {
|
||||
const locateApi =
|
||||
typeof this.api.locateVehicle === 'function' ? this.api.locateVehicle : locateVehicle;
|
||||
const res = await locateApi(waybillId);
|
||||
const data = res?.data?.data || null;
|
||||
if (!data || data.longitude == null || data.latitude == null) {
|
||||
this.waybillLocateInfo = null;
|
||||
this.waybillLocateHint = '暂无车辆实时定位数据';
|
||||
this.$message.warning('暂无车辆实时定位数据');
|
||||
return;
|
||||
}
|
||||
this.waybillLocateInfo = data;
|
||||
this.waybillLocateHint = '';
|
||||
await this.$nextTick();
|
||||
await this.renderWaybillLocateMap(data);
|
||||
} catch (error) {
|
||||
this.waybillLocateInfo = null;
|
||||
this.waybillLocateHint = '实时定位获取失败';
|
||||
this.$message.error(error?.message || '实时定位获取失败');
|
||||
} finally {
|
||||
this.waybillLocateLoading = false;
|
||||
}
|
||||
},
|
||||
async loadWaybillTrack() {
|
||||
const waybillId = this.detailRow?.id;
|
||||
if (!waybillId) {
|
||||
this.$message.warning('运单信息不完整');
|
||||
return;
|
||||
}
|
||||
if (!this.detailRow.vehicleNo) {
|
||||
this.$message.warning('运单未绑定车牌号,无法查询历史轨迹');
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(this.waybillTrackDateRange) || this.waybillTrackDateRange.length !== 2) {
|
||||
this.waybillTrackDateRange = this.buildDefaultTrackDateRange();
|
||||
}
|
||||
const [startDate, endDate] = this.waybillTrackDateRange;
|
||||
this.waybillTrackLoading = true;
|
||||
this.waybillTrackHint = '正在获取历史轨迹...';
|
||||
this.destroyWaybillTrackMap();
|
||||
try {
|
||||
const trackApi =
|
||||
typeof this.api.trackVehicle === 'function' ? this.api.trackVehicle : trackVehicle;
|
||||
const res = await trackApi(waybillId, startDate, endDate);
|
||||
const data = res?.data?.data || null;
|
||||
const points = Array.isArray(data?.points) ? data.points : [];
|
||||
if (!data || points.length === 0) {
|
||||
this.waybillTrackInfo = data || { total: 0, points: [] };
|
||||
this.waybillTrackHint = '暂无可回放的物流轨迹';
|
||||
this.$message.warning('暂无可回放的物流轨迹');
|
||||
return;
|
||||
}
|
||||
this.waybillTrackInfo = data;
|
||||
this.waybillTrackHint = '';
|
||||
await this.$nextTick();
|
||||
await this.renderWaybillTrackMap(points);
|
||||
} catch (error) {
|
||||
this.waybillTrackInfo = null;
|
||||
this.waybillTrackHint = '历史轨迹获取失败';
|
||||
this.$message.error(error?.message || '历史轨迹获取失败');
|
||||
} finally {
|
||||
this.waybillTrackLoading = false;
|
||||
}
|
||||
},
|
||||
async renderWaybillLocateMap(locateInfo) {
|
||||
const longitude = Number(locateInfo?.longitude);
|
||||
const latitude = Number(locateInfo?.latitude);
|
||||
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
|
||||
this.$message.warning('定位坐标无效,无法在地图上展示');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.loadAmap();
|
||||
await this.$nextTick();
|
||||
const container = this.$refs.waybillLocateMap;
|
||||
if (!container || !window.AMap) {
|
||||
this.$message.warning('高德地图容器未就绪');
|
||||
return;
|
||||
}
|
||||
this.destroyWaybillLocateMap();
|
||||
this.waybillLocateMapInstance = new window.AMap.Map(container, {
|
||||
zoom: 15,
|
||||
center: [longitude, latitude],
|
||||
viewMode: '2D',
|
||||
resizeEnable: true,
|
||||
});
|
||||
const content = [
|
||||
`<div style="padding:4px 2px;line-height:1.6;font-size:12px;">`,
|
||||
`<div><b>${locateInfo.vehicleNo || '车辆位置'}</b></div>`,
|
||||
locateInfo.locateTime ? `<div>时间:${locateInfo.locateTime}</div>` : '',
|
||||
locateInfo.address ? `<div>地址:${locateInfo.address}</div>` : '',
|
||||
`<div>坐标:${longitude}, ${latitude}</div>`,
|
||||
`</div>`,
|
||||
].join('');
|
||||
this.waybillLocateInfoWindow = new window.AMap.InfoWindow({
|
||||
content,
|
||||
offset: new window.AMap.Pixel(0, -30),
|
||||
});
|
||||
this.waybillLocateMarker = new window.AMap.Marker({
|
||||
position: [longitude, latitude],
|
||||
title: locateInfo.vehicleNo || '车辆位置',
|
||||
anchor: 'bottom-center',
|
||||
});
|
||||
this.waybillLocateMarker.on('click', () => {
|
||||
this.waybillLocateInfoWindow.open(this.waybillLocateMapInstance, [longitude, latitude]);
|
||||
});
|
||||
this.waybillLocateMapInstance.add(this.waybillLocateMarker);
|
||||
this.waybillLocateInfoWindow.open(this.waybillLocateMapInstance, [longitude, latitude]);
|
||||
this.waybillLocateMapInstance.setFitView([this.waybillLocateMarker], false, [40, 40, 40, 40]);
|
||||
setTimeout(() => {
|
||||
this.waybillLocateMapInstance?.resize?.();
|
||||
}, 80);
|
||||
} catch (error) {
|
||||
window.console.warn('实时定位地图渲染失败', error);
|
||||
this.$message.error('高德地图渲染失败,请稍后重试');
|
||||
}
|
||||
},
|
||||
async renderWaybillTrackMap(points) {
|
||||
const path = (points || [])
|
||||
.map(item => [Number(item.longitude), Number(item.latitude)])
|
||||
.filter(item => Number.isFinite(item[0]) && Number.isFinite(item[1]));
|
||||
if (!path.length) {
|
||||
this.$message.warning('轨迹坐标无效,无法在地图上展示');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.loadAmap();
|
||||
await this.$nextTick();
|
||||
const container = this.$refs.waybillTrackMap;
|
||||
if (!container || !window.AMap) {
|
||||
this.$message.warning('高德地图容器未就绪');
|
||||
return;
|
||||
}
|
||||
this.destroyWaybillTrackMap();
|
||||
this.waybillTrackMapInstance = new window.AMap.Map(container, {
|
||||
zoom: 12,
|
||||
center: path[0],
|
||||
viewMode: '2D',
|
||||
resizeEnable: true,
|
||||
});
|
||||
this.waybillTrackPolyline = new window.AMap.Polyline({
|
||||
path,
|
||||
strokeColor: '#409eff',
|
||||
strokeWeight: 6,
|
||||
strokeOpacity: 0.9,
|
||||
lineJoin: 'round',
|
||||
lineCap: 'round',
|
||||
showDir: path.length > 1,
|
||||
});
|
||||
this.waybillTrackStartMarker = new window.AMap.Marker({
|
||||
position: path[0],
|
||||
title: '起点',
|
||||
anchor: 'bottom-center',
|
||||
label: {
|
||||
content: '起',
|
||||
direction: 'top',
|
||||
offset: new window.AMap.Pixel(0, -6),
|
||||
},
|
||||
});
|
||||
this.waybillTrackEndMarker = new window.AMap.Marker({
|
||||
position: path[path.length - 1],
|
||||
title: '终点',
|
||||
anchor: 'bottom-center',
|
||||
label: {
|
||||
content: '终',
|
||||
direction: 'top',
|
||||
offset: new window.AMap.Pixel(0, -6),
|
||||
},
|
||||
});
|
||||
this.waybillTrackMapInstance.add([
|
||||
this.waybillTrackPolyline,
|
||||
this.waybillTrackStartMarker,
|
||||
this.waybillTrackEndMarker,
|
||||
]);
|
||||
this.waybillTrackMapInstance.setFitView(
|
||||
[this.waybillTrackPolyline, this.waybillTrackStartMarker, this.waybillTrackEndMarker],
|
||||
false,
|
||||
[48, 48, 48, 48]
|
||||
);
|
||||
setTimeout(() => {
|
||||
this.waybillTrackMapInstance?.resize?.();
|
||||
}, 80);
|
||||
} catch (error) {
|
||||
window.console.warn('历史轨迹地图渲染失败', error);
|
||||
this.$message.error('高德地图渲染失败,请稍后重试');
|
||||
}
|
||||
},
|
||||
displayStatus(row, prop) {
|
||||
const formatStatus = this.config.formatStatus;
|
||||
@@ -9535,6 +9922,81 @@ export default {
|
||||
border: 1px solid #eff1f7;
|
||||
}
|
||||
|
||||
&__waybill-locate {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__waybill-locate-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px 16px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #eff1f7;
|
||||
background: #fafafa;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
|
||||
span {
|
||||
margin-right: 8px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&__waybill-locate-address {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
&__waybill-locate-map {
|
||||
width: 100%;
|
||||
min-height: 320px;
|
||||
height: 320px;
|
||||
border: 1px solid #eff1f7;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
&__waybill-locate-map--track {
|
||||
min-height: 420px;
|
||||
height: 420px;
|
||||
}
|
||||
|
||||
&__waybill-map-tip {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
z-index: 2;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 8px 14px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__waybill-track-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&__waybill-track-total {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
&__waybill-track-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
@@ -140,15 +140,14 @@
|
||||
@load="onLoad(page, query)"
|
||||
/>
|
||||
|
||||
<!--
|
||||
项目新增/编辑/查看/变更/补录全部是独立整页(openProjectDialog 一律 push /business/project-apply/form),
|
||||
这里固定渲染普通容器,不再按 $route 在 div 与 el-dialog 之间切换。
|
||||
原因:标签页 keep-alive 会缓存本页实例,若容器在离开本页后又变回 el-dialog,
|
||||
缓存实例会把 append-to-body 的弹窗渲染进 body 并永久停留(Vue 的 KeepAlive.deactivate →
|
||||
Teleport.move 不会搬走 append-to-body 的弹窗 DOM),表现为“点左侧菜单又弹出新增项目管理弹窗”。
|
||||
-->
|
||||
<div v-if="isProjectFormPage" class="project-apply-page-form">
|
||||
<div class="archive-page-form__title">
|
||||
<component
|
||||
:is="projectFormContainer"
|
||||
v-if="projectBox"
|
||||
v-bind="projectFormContainerProps"
|
||||
@update:model-value="projectBox = $event"
|
||||
@closed="resetProjectDialog"
|
||||
>
|
||||
<div v-if="isProjectFormPage" class="archive-page-form__title">
|
||||
{{ projectDialogTitle }}
|
||||
</div>
|
||||
<el-form
|
||||
@@ -319,7 +318,7 @@
|
||||
<el-form-item prop="fundLimit" class="project-apply-form__tip-label">
|
||||
<template #label>
|
||||
<span>项目资金使用额度</span>
|
||||
<el-tooltip content="实际业务回款周期内付款额度" placement="top">
|
||||
<el-tooltip content="实际业务回款周期内付款" placement="top">
|
||||
<el-icon class="project-apply-form__label-tip"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
@@ -789,7 +788,8 @@
|
||||
:class="{ 'project-apply-dialog__footer--change': isChangeDialog }"
|
||||
>
|
||||
<template v-if="isChangeDialog">
|
||||
<el-button @click="closeProjectForm">取消</el-button>
|
||||
<el-button v-if="!isProjectFormPage" @click="handleCancelProject">取消</el-button>
|
||||
<el-button v-if="isProjectFormPage" @click="closeProjectForm">取消</el-button>
|
||||
<el-button type="primary" plain :loading="submitLoading" @click="saveChangeProject">
|
||||
保存
|
||||
</el-button>
|
||||
@@ -798,7 +798,8 @@
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button @click="closeProjectForm">取消</el-button>
|
||||
<el-button v-if="!isProjectFormPage" @click="handleCancelProject">取消</el-button>
|
||||
<el-button v-if="isProjectFormPage" @click="closeProjectForm">取消</el-button>
|
||||
<el-button
|
||||
v-if="!dialogReadonly"
|
||||
type="primary"
|
||||
@@ -818,7 +819,7 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
|
||||
<el-dialog
|
||||
v-model="attachmentDocumentPreviewVisible"
|
||||
@@ -1115,6 +1116,11 @@ export default {
|
||||
total: 0,
|
||||
},
|
||||
selectionList: [],
|
||||
// 表单页实例标记:创建时按路由落定,不随 keep-alive 切走后的 $route 变化翻转。
|
||||
// 否则 isProjectFormPage 变 false 后容器会从 div 切成 el-dialog(append-to-body),
|
||||
// 盖住后续打开的页面(如客商档案)。
|
||||
isFormPageInstance: false,
|
||||
projectBox: false,
|
||||
dialogType: 'add',
|
||||
dialogReadonly: false,
|
||||
submitLoading: false,
|
||||
@@ -1306,7 +1312,23 @@ export default {
|
||||
return ['add', 'majorSupplement'].includes(this.dialogType);
|
||||
},
|
||||
isProjectFormPage() {
|
||||
return this.$route.path === '/business/project-apply/form';
|
||||
return this.isFormPageInstance;
|
||||
},
|
||||
projectFormContainer() {
|
||||
// 表单页实例始终用页面容器,避免 keep-alive 失活时误切弹窗
|
||||
return this.isFormPageInstance ? 'div' : 'el-dialog';
|
||||
},
|
||||
projectFormContainerProps() {
|
||||
if (this.isFormPageInstance) return { class: 'project-apply-page-form' };
|
||||
return {
|
||||
modelValue: this.projectBox,
|
||||
title: this.projectDialogTitle,
|
||||
appendToBody: true,
|
||||
destroyOnClose: true,
|
||||
width: '1440px',
|
||||
top: '4vh',
|
||||
class: 'project-apply-dialog',
|
||||
};
|
||||
},
|
||||
isBasicInfoReadonly() {
|
||||
return this.dialogReadonly || this.isChangeDialog;
|
||||
@@ -1342,10 +1364,13 @@ export default {
|
||||
this.loadCargoTypeOptions();
|
||||
this.loadTransportTypeOptions();
|
||||
this.loadSettlementModeOptions();
|
||||
if (this.isProjectFormPage) {
|
||||
if (this.$route.path === '/business/project-apply/form') {
|
||||
this.isFormPageInstance = true;
|
||||
this.openProjectFormPage();
|
||||
}
|
||||
},
|
||||
// 标签切走(keep-alive 缓存)或销毁时,收起 append-to-body 的内层弹窗,
|
||||
// 避免 Teleport 出去的 DOM 继续盖住后续页面。
|
||||
deactivated() {
|
||||
this.closeInnerDialogs();
|
||||
},
|
||||
@@ -1353,6 +1378,12 @@ export default {
|
||||
this.closeInnerDialogs();
|
||||
},
|
||||
methods: {
|
||||
closeInnerDialogs() {
|
||||
this.changeRecordDetailVisible = false;
|
||||
this.attachmentDocumentPreviewVisible = false;
|
||||
this.attachmentImagePreviewVisible = false;
|
||||
this.userBox = false;
|
||||
},
|
||||
buildTableOption() {
|
||||
return {
|
||||
...option,
|
||||
@@ -1379,6 +1410,7 @@ export default {
|
||||
const id = this.$route.query.id;
|
||||
this.dialogType = type;
|
||||
this.dialogReadonly = type === 'view';
|
||||
this.projectBox = true;
|
||||
if (['add', 'majorSupplement'].includes(type)) {
|
||||
this.applyProjectDetail({
|
||||
...emptyForm(),
|
||||
@@ -1399,15 +1431,11 @@ export default {
|
||||
});
|
||||
},
|
||||
closeProjectForm() {
|
||||
if (this.isProjectFormPage) {
|
||||
this.$router.push('/business/project-apply');
|
||||
},
|
||||
// 页面被 keep-alive 缓存/卸载时,关闭本页所有 append-to-body 的二级弹窗。
|
||||
// 若不关闭,Teleport 出去的弹窗 DOM 在实例失活时不会被搬离 body,会残留盖在后续页面上。
|
||||
closeInnerDialogs() {
|
||||
this.changeRecordDetailVisible = false;
|
||||
this.attachmentDocumentPreviewVisible = false;
|
||||
this.attachmentImagePreviewVisible = false;
|
||||
this.userBox = false;
|
||||
} else {
|
||||
this.projectBox = false;
|
||||
}
|
||||
},
|
||||
statusValue(row) {
|
||||
return row[this.config.statusProp || 'status'];
|
||||
@@ -1626,6 +1654,17 @@ export default {
|
||||
else if (type === 'change') query.name = '项目变更';
|
||||
this.$router.push({ path: '/business/project-apply/form', query });
|
||||
},
|
||||
resetProjectDialog() {
|
||||
this.$refs.projectForm?.clearValidate();
|
||||
this.form = emptyForm();
|
||||
this.customerRows = [];
|
||||
this.carrierRows = [];
|
||||
this.attachmentRows = [];
|
||||
this.selectedAttachmentRows = [];
|
||||
this.changeRows = [];
|
||||
this.selectedCustomerId = [];
|
||||
this.selectedCarrierIds = [];
|
||||
},
|
||||
applyProjectDetail(row) {
|
||||
const displayRow = {
|
||||
...row,
|
||||
@@ -1748,6 +1787,23 @@ export default {
|
||||
this.submitLoading = false;
|
||||
});
|
||||
},
|
||||
handleCancelProject() {
|
||||
if (this.isProjectFormPage) {
|
||||
this.closeProjectForm();
|
||||
return;
|
||||
}
|
||||
if (!['add', 'majorSupplement', 'change'].includes(this.dialogType)) {
|
||||
this.closeProjectForm();
|
||||
return;
|
||||
}
|
||||
this.$confirm('确认后直接关闭弹窗,列表无数据变更,是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
this.closeProjectForm();
|
||||
});
|
||||
},
|
||||
saveChangeProject() {
|
||||
this.submitChangeForm(false);
|
||||
},
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<!-- 同 waybill-manage-detail.vue:只在当前路由属于本页时才透传 detailId,
|
||||
避免标签页缓存的实例被其它页面的 detailId 污染后误开详情。 -->
|
||||
<transport-plan-page
|
||||
:api="api"
|
||||
:config="config"
|
||||
:crud-option="option"
|
||||
:detail-id="$route.query.detailId"
|
||||
:detail-id="detailId"
|
||||
:menu-width="220"
|
||||
standalone-form-page
|
||||
/>
|
||||
@@ -14,6 +16,8 @@ import TransportPlanPage from './components/transport-plan-page.vue';
|
||||
import * as api from '@/api/business/transport-plan';
|
||||
import { config, option } from '@/option/business/transport-plan';
|
||||
|
||||
const listRoutePath = '/business/transport-plan';
|
||||
|
||||
export default {
|
||||
components: { TransportPlanPage },
|
||||
data() {
|
||||
@@ -23,5 +27,10 @@ export default {
|
||||
option,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
detailId() {
|
||||
return this.$route.path === listRoutePath ? this.$route.query.detailId : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
<template>
|
||||
<!--
|
||||
detailId 必须做路由守卫:本组件对应 /business/waybill-manage/detail,
|
||||
而 $route 是全局响应式的。标签页 keep-alive 会缓存本实例,路由切到别的详情页后
|
||||
实例仍会重渲染,若无守卫就会把「当前页面的 id」灌进子组件,导致它拿别的单据 id
|
||||
去查运单 → 报「运单管理不存在」,并误生成一个又一个运单详情标签。
|
||||
-->
|
||||
<waybill-manage-page
|
||||
:api="api"
|
||||
:config="config"
|
||||
:crud-option="option"
|
||||
:detail-id="$route.query.id"
|
||||
:detail-id="detailId"
|
||||
:menu-width="250"
|
||||
standalone-detail-page
|
||||
/>
|
||||
@@ -14,10 +20,17 @@ import WaybillManagePage from './components/waybill-manage-page.vue';
|
||||
import * as api from '@/api/business/waybill-manage';
|
||||
import { config, option } from '@/option/business/waybill-manage';
|
||||
|
||||
const detailRoutePath = '/business/waybill-manage/detail';
|
||||
|
||||
export default {
|
||||
components: { WaybillManagePage },
|
||||
data() {
|
||||
return { api, config, option };
|
||||
},
|
||||
computed: {
|
||||
detailId() {
|
||||
return this.$route.path === detailRoutePath ? this.$route.query.id : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<!-- 同 waybill-manage-detail.vue:保留旧链接 /business/waybill-manage?detailId=xxx 的能力,
|
||||
但只在当前路由确实属于本页时才透传,避免被其它页面的 detailId 污染。 -->
|
||||
<waybill-manage-page
|
||||
:api="api"
|
||||
:config="config"
|
||||
:crud-option="option"
|
||||
:detail-id="$route.query.detailId"
|
||||
:detail-id="detailId"
|
||||
:menu-width="250"
|
||||
standalone-form-page
|
||||
/>
|
||||
@@ -14,6 +16,8 @@ import WaybillManagePage from './components/waybill-manage-page.vue';
|
||||
import * as api from '@/api/business/waybill-manage';
|
||||
import { config, option } from '@/option/business/waybill-manage';
|
||||
|
||||
const listRoutePath = '/business/waybill-manage';
|
||||
|
||||
export default {
|
||||
components: { WaybillManagePage },
|
||||
data() {
|
||||
@@ -23,5 +27,10 @@ export default {
|
||||
option,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
detailId() {
|
||||
return this.$route.path === listRoutePath ? this.$route.query.detailId : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
+272
-30
@@ -22,14 +22,6 @@
|
||||
@tree-load="treeLoad"
|
||||
>
|
||||
<template #menu-left>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:loading="iamSyncLoading"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
@click="handleIamOrganizationSync"
|
||||
>同步组织
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="el-icon-delete"
|
||||
@@ -38,6 +30,14 @@
|
||||
@click="handleDelete"
|
||||
>删除
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:loading="oaSyncLoading"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
@click="handleOaOrgSync"
|
||||
>自动同步组织
|
||||
</el-button>
|
||||
</template>
|
||||
<template #menu="scope">
|
||||
<el-link
|
||||
@@ -124,6 +124,46 @@
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="oaSyncVisible"
|
||||
width="480px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="!oaSyncRunning"
|
||||
:show-close="!oaSyncRunning"
|
||||
class="oa-sync-dialog"
|
||||
@close="closeOaSyncDialog"
|
||||
>
|
||||
<template #header>
|
||||
<span class="dialog-title">{{ oaSyncDialogTitle }}</span>
|
||||
</template>
|
||||
<div class="oa-sync-body">
|
||||
<el-progress
|
||||
:percentage="oaSyncPercent"
|
||||
:status="oaSyncProgressStatus"
|
||||
:stroke-width="12"
|
||||
/>
|
||||
<div class="oa-sync-meta">
|
||||
当前阶段:{{ oaSyncStageLabel }},第 {{ oaSyncProgress.current || 0 }} / {{ oaSyncTotalPage }} 页
|
||||
</div>
|
||||
<div class="oa-sync-stats">
|
||||
<div class="oa-sync-stat">
|
||||
<span class="oa-sync-stat__label">同步成功</span>
|
||||
<span class="oa-sync-stat__value oa-sync-stat__value--success">{{ oaSyncProgress.synced }}</span>
|
||||
</div>
|
||||
<div class="oa-sync-stat">
|
||||
<span class="oa-sync-stat__label">跳过</span>
|
||||
<span class="oa-sync-stat__value oa-sync-stat__value--skip">{{ oaSyncProgress.skipped }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button v-if="oaSyncRunning" @click="cancelOaSync">取消</el-button>
|
||||
<el-button v-else type="primary" @click="closeOaSyncDialog">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
@@ -135,7 +175,8 @@ import {
|
||||
add,
|
||||
getDept,
|
||||
getDeptTree,
|
||||
syncIamOrganizations,
|
||||
syncOaCompany,
|
||||
syncOaDepartment,
|
||||
} from '@/api/system/dept';
|
||||
import { getLeaderList } from '@/api/system/user';
|
||||
import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive';
|
||||
@@ -168,7 +209,20 @@ export default {
|
||||
selectionList: [],
|
||||
query: {},
|
||||
loading: true,
|
||||
iamSyncLoading: false,
|
||||
oaSyncLoading: false,
|
||||
oaSyncVisible: false,
|
||||
oaSyncRunning: false,
|
||||
oaSyncCancelled: false,
|
||||
oaSyncStatus: 'running',
|
||||
oaSyncStage: 'company',
|
||||
oaSyncController: null,
|
||||
oaSyncProgress: {
|
||||
current: 0,
|
||||
size: 20,
|
||||
total: 0,
|
||||
synced: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
parentId: 0,
|
||||
page: {
|
||||
pageSize: 10,
|
||||
@@ -453,8 +507,153 @@ export default {
|
||||
});
|
||||
return ids.join(',');
|
||||
},
|
||||
oaSyncDialogTitle() {
|
||||
if (this.oaSyncStatus === 'done') {
|
||||
return '同步完成';
|
||||
}
|
||||
if (this.oaSyncStatus === 'cancelled') {
|
||||
return '已取消同步';
|
||||
}
|
||||
if (this.oaSyncStatus === 'error') {
|
||||
return '同步失败';
|
||||
}
|
||||
return '自动同步组织';
|
||||
},
|
||||
oaSyncStageLabel() {
|
||||
return this.oaSyncStage === 'department' ? '同步部门' : '同步公司';
|
||||
},
|
||||
oaSyncTotalPage() {
|
||||
const total = Number(this.oaSyncProgress.total) || 0;
|
||||
const size = Number(this.oaSyncProgress.size) || 20;
|
||||
if (total <= 0) {
|
||||
return this.oaSyncProgress.current || 0;
|
||||
}
|
||||
return Math.max(1, Math.ceil(total / size));
|
||||
},
|
||||
oaSyncPercent() {
|
||||
if (this.oaSyncStatus === 'done') {
|
||||
return 100;
|
||||
}
|
||||
const totalPage = this.oaSyncTotalPage;
|
||||
const current = Number(this.oaSyncProgress.current) || 0;
|
||||
const stageBase = this.oaSyncStage === 'department' ? 50 : 0;
|
||||
if (totalPage <= 0) {
|
||||
return stageBase;
|
||||
}
|
||||
const stagePercent = Math.min(50, Math.round((current / totalPage) * 50));
|
||||
return Math.min(99, stageBase + stagePercent);
|
||||
},
|
||||
oaSyncProgressStatus() {
|
||||
if (this.oaSyncStatus === 'done') {
|
||||
return 'success';
|
||||
}
|
||||
if (this.oaSyncStatus === 'error') {
|
||||
return 'exception';
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleOaOrgSync() {
|
||||
this.$confirm('确定从OA先同步公司、再同步部门?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
this.startOaOrgSync();
|
||||
});
|
||||
},
|
||||
startOaOrgSync() {
|
||||
this.oaSyncLoading = true;
|
||||
this.oaSyncVisible = true;
|
||||
this.oaSyncRunning = true;
|
||||
this.oaSyncCancelled = false;
|
||||
this.oaSyncStatus = 'running';
|
||||
this.oaSyncStage = 'company';
|
||||
this.oaSyncController = new AbortController();
|
||||
this.oaSyncProgress = {
|
||||
current: 0,
|
||||
size: 20,
|
||||
total: 0,
|
||||
synced: 0,
|
||||
skipped: 0,
|
||||
};
|
||||
this.runOaOrgSyncPages();
|
||||
},
|
||||
isOaSyncCanceledError(error) {
|
||||
return (
|
||||
this.oaSyncCancelled ||
|
||||
error?.code === 'ERR_CANCELED' ||
|
||||
error?.name === 'CanceledError' ||
|
||||
error?.name === 'AbortError'
|
||||
);
|
||||
},
|
||||
async runOaOrgSyncStage(syncApi) {
|
||||
const size = 20;
|
||||
let current = 1;
|
||||
while (!this.oaSyncCancelled) {
|
||||
const res = await syncApi(current, size, this.oaSyncController?.signal);
|
||||
const page = res?.data?.data || {};
|
||||
this.oaSyncProgress.current = page.current || current;
|
||||
this.oaSyncProgress.size = page.size || size;
|
||||
this.oaSyncProgress.total = page.total || 0;
|
||||
this.oaSyncProgress.synced += page.syncedCount || 0;
|
||||
this.oaSyncProgress.skipped += page.skippedCount || 0;
|
||||
if (page.finished) {
|
||||
break;
|
||||
}
|
||||
current += 1;
|
||||
}
|
||||
},
|
||||
async runOaOrgSyncPages() {
|
||||
try {
|
||||
this.oaSyncStage = 'company';
|
||||
await this.runOaOrgSyncStage(syncOaCompany);
|
||||
if (this.oaSyncCancelled) {
|
||||
this.oaSyncStatus = 'cancelled';
|
||||
return;
|
||||
}
|
||||
this.oaSyncStage = 'department';
|
||||
this.oaSyncProgress.current = 0;
|
||||
this.oaSyncProgress.total = 0;
|
||||
await this.runOaOrgSyncStage(syncOaDepartment);
|
||||
if (this.oaSyncCancelled) {
|
||||
this.oaSyncStatus = 'cancelled';
|
||||
return;
|
||||
}
|
||||
this.oaSyncStatus = 'done';
|
||||
} catch (error) {
|
||||
if (this.isOaSyncCanceledError(error)) {
|
||||
this.oaSyncStatus = 'cancelled';
|
||||
} else {
|
||||
this.oaSyncStatus = 'error';
|
||||
this.$message.error(error?.message || 'OA组织同步失败');
|
||||
}
|
||||
} finally {
|
||||
this.oaSyncRunning = false;
|
||||
this.oaSyncLoading = false;
|
||||
if (this.oaSyncStatus === 'done' || this.oaSyncStatus === 'cancelled') {
|
||||
this.parentId = 0;
|
||||
this.data = [];
|
||||
this.$refs.crud?.refreshTable?.();
|
||||
this.onLoad(this.page, this.query);
|
||||
}
|
||||
}
|
||||
},
|
||||
cancelOaSync() {
|
||||
if (!this.oaSyncRunning) {
|
||||
return;
|
||||
}
|
||||
this.oaSyncCancelled = true;
|
||||
this.oaSyncController?.abort();
|
||||
},
|
||||
closeOaSyncDialog() {
|
||||
if (this.oaSyncRunning) {
|
||||
this.cancelOaSync();
|
||||
return;
|
||||
}
|
||||
this.oaSyncVisible = false;
|
||||
},
|
||||
initData(tenantId) {
|
||||
getDeptTree(tenantId).then(res => {
|
||||
const column = this.findColumn(this.option.column, 'parentId');
|
||||
@@ -708,26 +907,6 @@ export default {
|
||||
done(row);
|
||||
});
|
||||
},
|
||||
handleIamOrganizationSync() {
|
||||
this.$confirm('确定从IAM同步组织信息?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
this.iamSyncLoading = true;
|
||||
return syncIamOrganizations();
|
||||
})
|
||||
.then(res => {
|
||||
const count = res?.data?.data ?? 0;
|
||||
this.$message.success(`IAM组织同步完成,共处理${count}条`);
|
||||
this.parentId = 0;
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.finally(() => {
|
||||
this.iamSyncLoading = false;
|
||||
});
|
||||
},
|
||||
handleDelete() {
|
||||
if (this.selectionList.length === 0) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
@@ -895,3 +1074,66 @@ export default {
|
||||
color: var(--el-color-danger-light-3);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.oa-sync-dialog .dialog-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.oa-sync-dialog .dialog-title::before {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
margin-right: 8px;
|
||||
background: #409eff;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.oa-sync-body {
|
||||
padding: 8px 4px 0;
|
||||
}
|
||||
|
||||
.oa-sync-meta {
|
||||
margin-top: 12px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.oa-sync-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.oa-sync-stat {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #eff1f7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.oa-sync-stat__label {
|
||||
display: block;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.oa-sync-stat__value {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.oa-sync-stat__value--success {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.oa-sync-stat__value--skip {
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
|
||||
+218
-15
@@ -340,6 +340,45 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="oaSyncVisible"
|
||||
width="480px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="!oaSyncRunning"
|
||||
:show-close="!oaSyncRunning"
|
||||
class="oa-sync-dialog"
|
||||
@close="closeOaSyncDialog"
|
||||
>
|
||||
<template #header>
|
||||
<span class="dialog-title">{{ oaSyncDialogTitle }}</span>
|
||||
</template>
|
||||
<div class="oa-sync-body">
|
||||
<el-progress
|
||||
:percentage="oaSyncPercent"
|
||||
:status="oaSyncProgressStatus"
|
||||
:stroke-width="12"
|
||||
/>
|
||||
<div class="oa-sync-meta">当前进度:第 {{ oaSyncProgress.current || 0 }} / {{ oaSyncTotalPage }} 页</div>
|
||||
<div class="oa-sync-stats">
|
||||
<div class="oa-sync-stat">
|
||||
<span class="oa-sync-stat__label">同步成功</span>
|
||||
<span class="oa-sync-stat__value oa-sync-stat__value--success">{{ oaSyncProgress.synced }}</span>
|
||||
</div>
|
||||
<div class="oa-sync-stat">
|
||||
<span class="oa-sync-stat__label">跳过</span>
|
||||
<span class="oa-sync-stat__value oa-sync-stat__value--skip">{{ oaSyncProgress.skipped }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button v-if="oaSyncRunning" @click="cancelOaSync">取消</el-button>
|
||||
<el-button v-else type="primary" @click="closeOaSyncDialog">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 认证日志组件 -->
|
||||
<auth-log ref="authLog" />
|
||||
<!-- 认证锁定配置组件 -->
|
||||
@@ -403,6 +442,18 @@ export default {
|
||||
query: {},
|
||||
loading: true,
|
||||
iamSyncLoading: false,
|
||||
oaSyncVisible: false,
|
||||
oaSyncRunning: false,
|
||||
oaSyncCancelled: false,
|
||||
oaSyncStatus: 'running',
|
||||
oaSyncController: null,
|
||||
oaSyncProgress: {
|
||||
current: 0,
|
||||
size: 50,
|
||||
total: 0,
|
||||
synced: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
page: {
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
@@ -453,6 +504,46 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo', 'permission']),
|
||||
oaSyncDialogTitle() {
|
||||
if (this.oaSyncStatus === 'done') {
|
||||
return '同步完成';
|
||||
}
|
||||
if (this.oaSyncStatus === 'cancelled') {
|
||||
return '已取消同步';
|
||||
}
|
||||
if (this.oaSyncStatus === 'error') {
|
||||
return '同步失败';
|
||||
}
|
||||
return '同步人员';
|
||||
},
|
||||
oaSyncTotalPage() {
|
||||
const total = Number(this.oaSyncProgress.total) || 0;
|
||||
const size = Number(this.oaSyncProgress.size) || 50;
|
||||
if (total <= 0) {
|
||||
return this.oaSyncProgress.current || 0;
|
||||
}
|
||||
return Math.max(1, Math.ceil(total / size));
|
||||
},
|
||||
oaSyncPercent() {
|
||||
if (this.oaSyncStatus === 'done') {
|
||||
return 100;
|
||||
}
|
||||
const totalPage = this.oaSyncTotalPage;
|
||||
const current = Number(this.oaSyncProgress.current) || 0;
|
||||
if (totalPage <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(100, Math.round((current / totalPage) * 100));
|
||||
},
|
||||
oaSyncProgressStatus() {
|
||||
if (this.oaSyncStatus === 'done') {
|
||||
return 'success';
|
||||
}
|
||||
if (this.oaSyncStatus === 'error') {
|
||||
return 'exception';
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
// 所属组织级联配置:checkStrictly 为 false 时只能选择最后一级(叶子节点)
|
||||
deptCascaderProps() {
|
||||
return {
|
||||
@@ -564,24 +655,88 @@ export default {
|
||||
}
|
||||
},
|
||||
handleIamSync() {
|
||||
this.$confirm('确定从IAM同步人员信息?', '提示', {
|
||||
this.$confirm('确定从OA同步人员及组织信息?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
this.iamSyncLoading = true;
|
||||
return syncIamAccounts();
|
||||
})
|
||||
.then(res => {
|
||||
const count = res?.data?.data ?? 0;
|
||||
this.$message.success(`IAM人员同步完成,共处理${count}条`);
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.finally(() => {
|
||||
this.iamSyncLoading = false;
|
||||
}).then(() => {
|
||||
this.startOaSync();
|
||||
});
|
||||
},
|
||||
startOaSync() {
|
||||
this.iamSyncLoading = true;
|
||||
this.oaSyncVisible = true;
|
||||
this.oaSyncRunning = true;
|
||||
this.oaSyncCancelled = false;
|
||||
this.oaSyncStatus = 'running';
|
||||
this.oaSyncController = new AbortController();
|
||||
this.oaSyncProgress = {
|
||||
current: 0,
|
||||
size: 50,
|
||||
total: 0,
|
||||
synced: 0,
|
||||
skipped: 0,
|
||||
};
|
||||
this.runOaSyncPages();
|
||||
},
|
||||
isOaSyncCanceledError(error) {
|
||||
return (
|
||||
this.oaSyncCancelled ||
|
||||
error?.code === 'ERR_CANCELED' ||
|
||||
error?.name === 'CanceledError' ||
|
||||
error?.name === 'AbortError'
|
||||
);
|
||||
},
|
||||
async runOaSyncPages() {
|
||||
const size = 50;
|
||||
let current = 1;
|
||||
try {
|
||||
while (!this.oaSyncCancelled) {
|
||||
const res = await syncIamAccounts(current, size, this.oaSyncController?.signal);
|
||||
const page = res?.data?.data || {};
|
||||
this.oaSyncProgress.current = page.current || current;
|
||||
this.oaSyncProgress.size = page.size || size;
|
||||
this.oaSyncProgress.total = page.total || 0;
|
||||
this.oaSyncProgress.synced += page.syncedCount || 0;
|
||||
this.oaSyncProgress.skipped += page.skippedCount || 0;
|
||||
if (page.finished) {
|
||||
this.oaSyncStatus = 'done';
|
||||
break;
|
||||
}
|
||||
current += 1;
|
||||
}
|
||||
if (this.oaSyncCancelled && this.oaSyncStatus === 'running') {
|
||||
this.oaSyncStatus = 'cancelled';
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.isOaSyncCanceledError(error)) {
|
||||
this.oaSyncStatus = 'cancelled';
|
||||
} else {
|
||||
this.oaSyncStatus = 'error';
|
||||
this.$message.error(error?.message || 'OA人员同步失败');
|
||||
}
|
||||
} finally {
|
||||
this.oaSyncRunning = false;
|
||||
this.iamSyncLoading = false;
|
||||
if (this.oaSyncStatus === 'done' || this.oaSyncStatus === 'cancelled') {
|
||||
this.onLoad(this.page, this.query);
|
||||
}
|
||||
}
|
||||
},
|
||||
cancelOaSync() {
|
||||
if (!this.oaSyncRunning) {
|
||||
return;
|
||||
}
|
||||
this.oaSyncCancelled = true;
|
||||
this.oaSyncController?.abort();
|
||||
},
|
||||
closeOaSyncDialog() {
|
||||
if (this.oaSyncRunning) {
|
||||
this.cancelOaSync();
|
||||
return;
|
||||
}
|
||||
this.oaSyncVisible = false;
|
||||
},
|
||||
handleSetLeader(row) {
|
||||
const tip = row.isLeader === 1 ? '确定取消用户的主管职务?' : '确定设置用户为主管职务?';
|
||||
const message = row.isLeader === 1 ? '取消主管成功!' : '设置主管成功!';
|
||||
@@ -1150,14 +1305,16 @@ export default {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.user-edit-dialog .dialog-title {
|
||||
.user-edit-dialog .dialog-title,
|
||||
.oa-sync-dialog .dialog-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-edit-dialog .dialog-title::before {
|
||||
.user-edit-dialog .dialog-title::before,
|
||||
.oa-sync-dialog .dialog-title::before {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
margin-right: 8px;
|
||||
@@ -1165,6 +1322,52 @@ export default {
|
||||
content: '';
|
||||
}
|
||||
|
||||
.oa-sync-body {
|
||||
padding: 8px 4px 0;
|
||||
}
|
||||
|
||||
.oa-sync-meta {
|
||||
margin-top: 12px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.oa-sync-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.oa-sync-stat {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #eff1f7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.oa-sync-stat__label {
|
||||
display: block;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.oa-sync-stat__value {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.oa-sync-stat__value--success {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.oa-sync-stat__value--skip {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.user-edit-dialog .el-form-item {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div ref="page" class="customer-archive-public-view">
|
||||
<customer-archive ref="archive" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomerArchive from './customer-archive.vue';
|
||||
import { postPublicProcessMessage } from '@/api/vehicle/customer-archive';
|
||||
|
||||
export default {
|
||||
name: 'CustomerArchivePublicView',
|
||||
components: {
|
||||
CustomerArchive,
|
||||
},
|
||||
created() {
|
||||
this.handleIframeHeight = () => this.sendIframeHeight();
|
||||
this.handleProcessMessage = event => this.onProcessMessage(event);
|
||||
document.addEventListener('DOMContentLoaded', this.handleIframeHeight, false);
|
||||
window.addEventListener('message', this.handleProcessMessage);
|
||||
},
|
||||
mounted() {
|
||||
document.documentElement.classList.add('mk-iframe-page');
|
||||
document.body.classList.add('mk-iframe-page');
|
||||
const app = document.getElementById('app');
|
||||
if (app) app.classList.add('mk-iframe-page');
|
||||
this.handleIframeHeight();
|
||||
window.addEventListener('load', this.handleIframeHeight);
|
||||
this.mkHeightTimers = [300, 800, 1600].map(delay =>
|
||||
setTimeout(this.handleIframeHeight, delay)
|
||||
);
|
||||
if (typeof ResizeObserver === 'undefined') return;
|
||||
this.mkHeightObserver = new ResizeObserver(() => this.handleIframeHeight());
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.page) this.mkHeightObserver.observe(this.$refs.page);
|
||||
this.mkHeightObserver.observe(document.body);
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('DOMContentLoaded', this.handleIframeHeight, false);
|
||||
window.removeEventListener('load', this.handleIframeHeight);
|
||||
window.removeEventListener('message', this.handleProcessMessage);
|
||||
(this.mkHeightTimers || []).forEach(timer => clearTimeout(timer));
|
||||
if (this.mkHeightObserver) {
|
||||
this.mkHeightObserver.disconnect();
|
||||
this.mkHeightObserver = null;
|
||||
}
|
||||
document.documentElement.classList.remove('mk-iframe-page');
|
||||
document.body.classList.remove('mk-iframe-page');
|
||||
const app = document.getElementById('app');
|
||||
if (app) app.classList.remove('mk-iframe-page');
|
||||
},
|
||||
methods: {
|
||||
sendIframeHeight() {
|
||||
this.$nextTick(() => {
|
||||
var tbody = document.body;
|
||||
var height = tbody.clientHeight;
|
||||
// 如需动态改变表单高度,可以直接发送此postMessage
|
||||
window.parent.postMessage({ height: height }, '*');
|
||||
});
|
||||
},
|
||||
onProcessMessage(event) {
|
||||
const data = event && event.data;
|
||||
if (!data || typeof data !== 'object') return;
|
||||
if (data.height && !data.status && !data.type) return;
|
||||
if (data.type === 'formValues' || data.type === 'afterSubmit') return;
|
||||
console.log('客商公开页收到流程消息:', data);
|
||||
const formValues = data.formValues;
|
||||
if (data.status === 'submit') {
|
||||
this.submitData(formValues);
|
||||
} else if (data.status === 'save') {
|
||||
this.saveData(formValues);
|
||||
}
|
||||
if (data.type === 'getFormValues') {
|
||||
window.parent.postMessage({ type: 'formValues', formData: this.buildFormData() }, '*');
|
||||
}
|
||||
},
|
||||
submitData(lbpmFormValues) {
|
||||
const formData = this.buildFormData();
|
||||
this.postFormData('submit', lbpmFormValues, formData);
|
||||
if (!lbpmFormValues) return;
|
||||
const parameters = Object.assign({}, lbpmFormValues, {
|
||||
loginName: this.getLoginName(lbpmFormValues),
|
||||
formInstanceId: this.getFormId(),
|
||||
subject: formData.subject,
|
||||
});
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'afterSubmit',
|
||||
success: true,
|
||||
parameters,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
},
|
||||
saveData(lbpmFormValues) {
|
||||
this.postFormData('save', lbpmFormValues, this.buildFormData());
|
||||
},
|
||||
postFormData(status, formValues, formData) {
|
||||
const payload = {
|
||||
status,
|
||||
formValues: formValues || {},
|
||||
formData,
|
||||
};
|
||||
console.log('客商公开页提交流程数据:', payload);
|
||||
postPublicProcessMessage(payload).catch(error => {
|
||||
console.error('客商公开页提交流程数据失败:', error);
|
||||
});
|
||||
},
|
||||
buildFormData() {
|
||||
const archive = this.getArchiveForm();
|
||||
return {
|
||||
...archive,
|
||||
subject: archive.fullName || archive.shortName || '',
|
||||
formInstanceId: this.getFormId(),
|
||||
};
|
||||
},
|
||||
getArchiveForm() {
|
||||
const archive = this.$refs.archive && this.$refs.archive.archiveForm;
|
||||
if (!archive) return {};
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(archive));
|
||||
} catch (error) {
|
||||
return { ...archive };
|
||||
}
|
||||
},
|
||||
getFormId() {
|
||||
return this.$route.query.id || this.getArchiveForm().id || '';
|
||||
},
|
||||
getLoginName(formValues = {}) {
|
||||
return (
|
||||
formValues.loginName ||
|
||||
this.$route.query.loginName ||
|
||||
this.$route.query.submitIdentity ||
|
||||
''
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
html.mk-iframe-page,
|
||||
html.mk-iframe-page body,
|
||||
html.mk-iframe-page #app,
|
||||
html.mk-iframe-page #app.mk-iframe-page {
|
||||
height: auto !important;
|
||||
min-height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.customer-archive-public-view {
|
||||
min-height: 100%;
|
||||
padding: 12px 0 24px;
|
||||
box-sizing: border-box;
|
||||
background: #f0f2f5;
|
||||
|
||||
:deep(.basic-container) {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
:deep(.archive-page-form .archive-form__footer) {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<basic-container class="customer-archive-page">
|
||||
<basic-container class="customer-archive-page" :class="{ 'is-public-view': isPublicViewPage }">
|
||||
<avue-crud
|
||||
v-if="!isArchivePage"
|
||||
:option="option"
|
||||
@@ -913,7 +913,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<div class="archive-form__footer">
|
||||
<div class="archive-form__footer" v-if="!isPublicViewPage">
|
||||
<!-- 次要:独立页返回 / 只读关闭 / 弹窗取消 -->
|
||||
<el-button @click="closeArchive">{{ readonly ? '关闭' : '取消' }}</el-button>
|
||||
<el-button type="primary" plain v-if="!readonly" @click="saveArchive">保存</el-button>
|
||||
@@ -1482,7 +1482,9 @@ import { ElCascader } from 'element-plus';
|
||||
import {
|
||||
getList,
|
||||
getDetail,
|
||||
getPublicDetail,
|
||||
getChangeRecordList,
|
||||
getPublicChangeRecordList,
|
||||
submit,
|
||||
submitApproval,
|
||||
withdrawApproval,
|
||||
@@ -1498,6 +1500,7 @@ import {
|
||||
getDetail as getCreditScoreQuantificationDetail,
|
||||
} from '@/api/vehicle/credit-score-quantification';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { processSubmit } from '@/api/system/business-process';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import { getLazyTree } from '@/api/base/region';
|
||||
import { exportBlob } from '@/api/common';
|
||||
@@ -1989,6 +1992,17 @@ export default {
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (this.isPublicViewPage) {
|
||||
this.readonly = true;
|
||||
const id = this.$route.query.id;
|
||||
if (!id) {
|
||||
this.archiveBox = true;
|
||||
this.$message.error('缺少客商ID');
|
||||
return;
|
||||
}
|
||||
this.openArchive({ id }, true);
|
||||
return;
|
||||
}
|
||||
this.initDeptTree();
|
||||
this.initRegionOptions();
|
||||
this.initBusinessDictionaries();
|
||||
@@ -2010,7 +2024,12 @@ export default {
|
||||
};
|
||||
},
|
||||
isArchivePage() {
|
||||
return this.$route.path === '/vehicle/customer-archive/form';
|
||||
return (
|
||||
this.$route.path === '/vehicle/customer-archive/form' || this.isPublicViewPage
|
||||
);
|
||||
},
|
||||
isPublicViewPage() {
|
||||
return this.$route.path === '/vehicle/customer-archive/public-view';
|
||||
},
|
||||
archiveContainer() {
|
||||
return 'div';
|
||||
@@ -2190,6 +2209,45 @@ export default {
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission[code], false);
|
||||
},
|
||||
mergeSelectOptions(target, values) {
|
||||
const list = Array.isArray(values) ? values : values ? [values] : [];
|
||||
list.forEach(value => {
|
||||
if (value === undefined || value === null || value === '') return;
|
||||
if (!this[target].some(item => String(item.value) === String(value))) {
|
||||
this[target].push({ label: String(value), value });
|
||||
}
|
||||
});
|
||||
},
|
||||
ensurePublicViewOptions(archive = {}) {
|
||||
this.mergeSelectOptions('customerNatureOptions', archive.customerNature);
|
||||
this.mergeSelectOptions('customerTypeOptions', archive.customerType);
|
||||
this.mergeSelectOptions('businessScopeOptions', archive.businessScope);
|
||||
this.mergeSelectOptions(
|
||||
'qualificationTypeOptions',
|
||||
(this.qualificationFiles || []).map(item => item.type)
|
||||
);
|
||||
const deptId = Array.isArray(archive.deptId) ? archive.deptId[0] : archive.deptId;
|
||||
if (deptId && archive.deptName) {
|
||||
this.deptTree = [
|
||||
{
|
||||
label: archive.deptName,
|
||||
value: String(deptId),
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
this.deptOptions = this.flattenDept(this.deptTree);
|
||||
}
|
||||
(archive.scores || []).forEach(score => {
|
||||
if (!score.quantificationId) return;
|
||||
const id = String(score.quantificationId);
|
||||
if (!this.scoreQuantificationOptions.some(item => String(item.id) === id)) {
|
||||
this.scoreQuantificationOptions.push({
|
||||
id,
|
||||
name: score.quantificationName || score.name || id,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
// 检测输入框文本是否溢出,仅溢出时才允许 tooltip 显示完整地址
|
||||
checkOverflow(refName, flag) {
|
||||
const inst = this.$refs[refName];
|
||||
@@ -2278,7 +2336,8 @@ export default {
|
||||
loadChangeRecords() {
|
||||
if (!this.archiveForm.id) return;
|
||||
const page = this.changeRecordPage;
|
||||
getChangeRecordList(this.archiveForm.id, page.currentPage, page.pageSize).then(res => {
|
||||
const requestList = this.isPublicViewPage ? getPublicChangeRecordList : getChangeRecordList;
|
||||
requestList(this.archiveForm.id, page.currentPage, page.pageSize).then(res => {
|
||||
const data = res.data.data || {};
|
||||
page.records = this.replaceNegativeOneWithBlank(data.records || []);
|
||||
page.total = Number(data.total || 0);
|
||||
@@ -3890,6 +3949,9 @@ export default {
|
||||
});
|
||||
},
|
||||
closeArchive() {
|
||||
if (this.isPublicViewPage) return;
|
||||
this.archiveBox = false;
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push('/vehicle/customer-archive');
|
||||
},
|
||||
openArchive(row, readonly = false) {
|
||||
@@ -3898,7 +3960,9 @@ export default {
|
||||
this.resetDetailPagination();
|
||||
this.resetChangeRecordPage();
|
||||
if (row && row.id) {
|
||||
getDetail(row.id).then(res => {
|
||||
const requestDetail = this.isPublicViewPage ? getPublicDetail : getDetail;
|
||||
requestDetail(row.id)
|
||||
.then(res => {
|
||||
const archive = this.normalizeDetail(res.data.data);
|
||||
this.archiveForm = readonly ? this.replaceNegativeOneWithBlank(archive) : archive;
|
||||
this.originalAccessType = this.archiveForm.accessType || '';
|
||||
@@ -3912,8 +3976,13 @@ export default {
|
||||
this.qualificationUploadFiles = [];
|
||||
this.ocrQualificationUploadFiles = [];
|
||||
this.selectedQualificationFiles = [];
|
||||
if (this.isPublicViewPage) this.ensurePublicViewOptions(this.archiveForm);
|
||||
this.archiveBox = true;
|
||||
this.loadChangeRecords();
|
||||
})
|
||||
.catch(() => {
|
||||
this.archiveBox = true;
|
||||
if (this.isPublicViewPage) this.$message.error('客商信息加载失败');
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -4078,7 +4147,12 @@ export default {
|
||||
this.$message({ type: 'success', message: '保存成功,请在列表提交审批' });
|
||||
return;
|
||||
}
|
||||
submitApproval(id).then(() => {
|
||||
const fullName =
|
||||
(data && typeof data === 'object' ? data.fullName : '') ||
|
||||
archive.fullName ||
|
||||
this.archiveForm.fullName ||
|
||||
'';
|
||||
this.submitCustomerApproval(id, fullName).then(() => {
|
||||
this.closeArchive();
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '提交成功!' });
|
||||
@@ -4086,6 +4160,38 @@ export default {
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 列表 / 新增 / 编辑提交共用:提交 MK 审核流并更新客商审批状态
|
||||
*/
|
||||
submitCustomerApproval(id, fullName = '') {
|
||||
return this.submitMkApprovalFlow(id, fullName).then(() => submitApproval(id));
|
||||
},
|
||||
/**
|
||||
* 提交 MK 审核流:templateCode 取业务字典 mk_template 中「提交审核流」的键值
|
||||
* 提交人手机号由后端从用户表读取真实值(前端接口会脱敏)
|
||||
*/
|
||||
async submitMkApprovalFlow(formInstanceId, subjectName = '') {
|
||||
const templateCode = await this.resolveMkTemplateCode('提交审核流');
|
||||
const subject = subjectName
|
||||
? `客商准入审批:${subjectName}`
|
||||
: `客商准入审批:${formInstanceId}`;
|
||||
return processSubmit({
|
||||
templateCode,
|
||||
formInstanceId: String(formInstanceId),
|
||||
subject,
|
||||
});
|
||||
},
|
||||
async resolveMkTemplateCode(dictName = '提交审核流') {
|
||||
const res = await getDictionary({ code: 'mk_template' });
|
||||
const list = res?.data?.data || [];
|
||||
const matched = list.find(item => String(item.dictValue || '').trim() === dictName);
|
||||
const templateCode = matched?.dictKey;
|
||||
if (!templateCode) {
|
||||
this.$message.warning(`未配置业务字典 mk_template「${dictName}」,无法提交审核流`);
|
||||
return Promise.reject(new Error(`未配置业务字典 mk_template「${dictName}」`));
|
||||
}
|
||||
return String(templateCode);
|
||||
},
|
||||
addScore() {
|
||||
this.scoreRecordIndex = -1;
|
||||
this.currentScore = this.normalizeScore({});
|
||||
@@ -4676,7 +4782,7 @@ export default {
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => submitApproval(row.id))
|
||||
.then(() => this.submitCustomerApproval(row.id, archive.fullName || row.fullName))
|
||||
.then(() => {
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '提交成功!' });
|
||||
@@ -5348,6 +5454,12 @@ export default {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.customer-archive-page.is-public-view {
|
||||
:deep(.archive-page-form .archive-form__footer) {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.archive-page-form {
|
||||
min-height: 100%;
|
||||
margin-bottom: 60px;
|
||||
|
||||
+13
-13
@@ -47,26 +47,26 @@ export default ({ mode, command }) => {
|
||||
__VUE_I18N_LEGACY_API__: true,
|
||||
__INTLIFY_PROD_DEVTOOLS__: false,
|
||||
},
|
||||
// server: {
|
||||
// port: 2888,
|
||||
// proxy: {
|
||||
// '/api': {
|
||||
// target: 'http://localhost',
|
||||
// //target: 'https://saber3.bladex.cn/api',
|
||||
// changeOrigin: true,
|
||||
// rewrite: path => path.replace(/^\/api/, ''),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
server: {
|
||||
port: 2889,
|
||||
port: 2888,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://172.16.203.228:8000',
|
||||
target: 'http://localhost',
|
||||
//target: 'https://saber3.bladex.cn/api',
|
||||
changeOrigin: true,
|
||||
rewrite: path => path.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
// server: {
|
||||
// port: 2889,
|
||||
// proxy: {
|
||||
// '/api': {
|
||||
// target: 'http://172.16.203.228:8000',
|
||||
// changeOrigin: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': resolve(__dirname, './'),
|
||||
|
||||
Reference in New Issue
Block a user