Compare commits

..

19 Commits

Author SHA1 Message Date
刘泉佳 f49f36fb60 ✨ 金蝶附件下载(A801-2)前端:票池弹窗/正式结算/付款申请三入口附件查看下载与类型分流 2026-09-27 21:17:59 +08:00
刘泉佳 aabe67e4d6 ✨ 金蝶附件同步(A902)前端:四页面附件同步列、重推补传提示与收票推送超时放宽 2026-09-24 13:27:41 +08:00
刘泉佳 e63c7fdd42 Merge remote-tracking branch 'origin/feature/bfi-api-rate' into feature/bfi-api-rate
# Conflicts:
#	src/api/payment/invoiceReceipt.js
2026-09-24 11:56:47 +08:00
刘泉佳 aa0aec3704 feat: 批量转结算弹窗新增单据号和合同编码查询条件 2026-09-23 15:39:32 +08:00
刘泉佳 622705325b feat: 选票弹窗新增剩余可用金额列并支持游标翻页与上一页缓存回显
- 下一页回传上页nextCursor续扫,新查询/换页容量/重新打开自动重置游标与页缓存
- 上一页直接回显已浏览页缓存,不再发起请求
- 查询表单删除单据使用状态条件(后端固定查未用),新增剩余可用金额展示列
2026-09-23 15:06:33 +08:00
刘泉佳 4657fc29bd ♻️ 收票管理仅保留推送金蝶收票单并统一命名为批量同步 2026-09-23 11:05:19 +08:00
刘泉佳 884de893db ✨ 收票单推送金蝶与票池实时游标分页前端适配:选票弹窗hasMore分页,下线镜像补查与票池同步入口,保存时按票号重查回填 2026-09-23 10:22:51 +08:00
刘泉佳 cc73d544ff 🔧 引入vue-tsc与typecheck脚本支持类型检查,pnpm-lock.yaml改为忽略不跟踪 2026-09-23 10:22:46 +08:00
刘泉佳 10fecb5a54 ✨ 金蝶票池选票统一查镜像前端接线(新增统一选票弹窗组件,替换收票与结算认领弹窗并删除Mock) 2026-09-22 21:26:50 +08:00
刘泉佳 bab6bdba27 Merge remote-tracking branch 'origin/feature/bfi-api-rate' into feature/bfi-api-rate 2026-09-22 16:36:33 +08:00
刘泉佳 a554ccf38e 🐛 axios网络级失败(502/504/超时)补充统一错误提示,修复接口报错页面无感知 2026-09-21 21:20:57 +08:00
刘泉佳 a748e52551 ✨ 开票申请商品行选择开票内容时快照税收分类编码并随保存提交 2026-09-21 19:14:57 +08:00
刘泉佳 e89e78b527 ✨ 正式结算单认领查询对接发票云全票池(多条件搜索+服务端分页) 2026-09-20 19:41:37 +08:00
刘泉佳 2a03991297 ⚡ 客商档案金蝶回填优化:放宽锁定字段、信用代码可重新输入并修复查询误判 2026-09-20 16:06:03 +08:00
刘泉佳 884cc4355f fix: 同步金蝶接口放宽请求超时,修复长耗时推送成功但页面无提示 2026-09-18 20:42:52 +08:00
刘泉佳 58aaf79445 ✨ 客商档案新增金蝶客商查询回填与同步金蝶重推入口 2026-09-18 20:01:29 +08:00
刘泉佳 f067518c0e fix: 付款申请批量同步金蝶失败时展示失败原因
- 批量同步后解析返回结果,按"同步失败"标识区分成功与失败条目
- 部分失败时以 warning 提示成功/失败条数及具体失败原因(停留8秒),全部成功仍提示已同步条数
2026-09-18 11:58:00 +08:00
刘泉佳 11460f3378 ✨ 付款管理新增同步付款结果按钮并扩展金蝶单据状态选项 2026-09-18 11:57:59 +08:00
刘泉佳 7f543937fb ✨ 结算单新增时自动查询BFI汇率
- currency.js: 新增getBfiExchangeRate接口调用
- pre-settlement-editor.vue:
  - 选择合同后同步结算币种并自动查询当天BFI汇率
  - 汇率日期变更时自动查询BFI汇率并回填结算汇率
  - 新增时自动设置当天日期并查询汇率
- formal-settlement-editor.vue:
  - 选择合同后同步结算币种并自动查询当天BFI汇率
  - 汇率日期变更时自动查询BFI汇率并回填结算汇率
  - 新增时自动查询当天BFI汇率
2026-09-16 14:14:53 +08:00
249 changed files with 8219 additions and 30395 deletions
+2 -3
View File
@@ -3,9 +3,8 @@
VITE_APP_ENV = 'development'
#接口地址
# 开发环境建议填 [/api]:由 vite.config.mjs 的 proxy 同源转发到本地网关 http://localhost(80)
# 本地需先启动:Nacos + blade-gateway(80) + blade-auth + blade-system 等;8080 端口一般是 Nacos 控制台不是网关
# 如需临时联调远程,把 vite proxy target 改为 http://172.16.203.228:8000 并去掉 rewrite
# 开发环境建议填 [/api]:由下方 vite.config.mjs 的 proxy 同源转发到后端(172.16.203.228:8000),避免跨域(CORS)被浏览器拦截
# 如需直连线上绝对地址(如 http://172.16.203.228:8000/api),必须让后端 CORS 把 Allow-Origin 改为具体前端域名,不能用 `*`(配合 credentials 会被浏览器拒绝)
VITE_APP_API=/api
#调试参数
+1
View File
@@ -1,5 +1,6 @@
.DS_Store
node_modules
pnpm-lock.yaml
/dist
/tests/e2e/videos/
-6
View File
@@ -1,6 +0,0 @@
# 2026-09-10
- 移除 project-apply 的 fund-risk-stats 请求(后端未实现端点,每次列表加载都弹 No endpoint 报错):删除 onLoad 内 refreshFundRiskStats 调用与该方法、data 中 fundRiskStats;头部风险标签去掉「(数量)」只留「高风险/中风险」;api/business/project-apply.js 保留 getFundRiskStats 定义并注释「后端就绪后恢复」。
EOF2
}
cat >> /Users/gxwebsoft/VUE/tms-erp-web-ws/.workbuddy/memory/2026-09-10.md <<'EOF'
-28
View File
@@ -1,28 +0,0 @@
# 2026-09-11
## 上传证件区域固定宽度导致跨机器错位(transportCapacity)
- 现象:driver.vue 新增弹窗的身份证/大头照上传框在本机对齐,他人电脑不对齐。
- 根因:`--large` 上传框写死 `width:240px`(ship 写 248px),而弹窗 `width:90%` + `el-col :span=6` 列宽是弹性的、上方输入框 `width:100%`;列宽随分辨率 / Windows 系统缩放 / 浏览器缩放变化,240px 只在特定宽度下恰好等于列宽。叠加 `labelWidth:auto` 靠 JS 实测字体(苹方 vs 微软雅黑)宽度,label 列宽也不同 → 起始位置漂移。
- 修复:三个文件改为 `width:100%; max-width:240px; flex:none`(ship 保留 248px 上限),窄屏跟随列宽、宽屏保持比例上限,左边缘恒与输入框对齐。
- `src/views/transportCapacity/driver.vue`(.driver-uploader--large)
- `src/views/transportCapacity/vehicle.vue`(.vehicle-uploader--large)
- `src/views/transportCapacity/ship.vue`(.ship-uploader--large)
- 用 @vue/compiler-sfc(脚本放项目根目录)验证三个 SFC 编译通过。
- 教训:本项目所有「固定 px 宽度的上传/控件」都要配 `max-width` + `width:100%`,否则高分屏/低分屏下必然错位。
## 合同详情由弹窗改为独立页面(contract-manage)
- 路由:新增 `/business/contract-manage/detail`(`src/router/views/index.js`,name『合同详情』,meta `keepAlive:false` + `activeMenu:'/business/contract-manage'`),组件仍复用 `contract-manage.vue`(与 form 页同一套路)。
- 页面内三分支:`v-if="!isFormPage && !isDetailPage"`(列表)/ `v-else-if="isFormPage"`(表单)/ `v-else`(详情)。**v-else 必须紧邻 form 分支的 `</template>`**,中间不能插其它组件,否则 Vue 编译报「v-else 无相邻 v-if」——弹窗类组件(billing-plan-editor / 附件预览 / 变更记录详情 / 流程)放在三个分支之后。
- 关键改动:`openDetail` 改 `router.push({ path:'/business/contract-manage/detail', query:{ id, name } })`;新增 `initDetailPage()`(按 query.id 拉详情,`resetDetailState()` 先清空)、`closeDetailPage()`(closeTag + 回列表)、`isDetailPage` / `detailPageTitle` computed;`created` 与 `$route.fullPath` watch 都要加 `else if (this.isDetailPage)`。
- 样式:`.contract-manage-detail--page { max-height:none; overflow:visible }`,去掉弹窗的 74vh 限制;底栏复用 `.contract-manage-page__footer`(fixed 浮动),只读详情页按钮保留「关闭」。
- 校验方式:dev server 已运行时 `curl "http://localhost:2889/src/views/business/contract-manage.vue"` 返回 200 即 Vite 编译通过(比整包 build 快)。
## 只给「查看弹窗」加宽 label(temporary-credit-limit)
- 场景:详情页长 label(「剩余项目资金使用额度(万元)」)换行超 2 行撑坏详情表格,但又不能影响新增/编辑页。
- **关键发现:Avue 的 `avue-form` 在 detail 模式下根节点会挂 `.avue--detail` 类**(源码 `node_modules/@smallwei/avue/lib/packages/element-plus/form/index3.js` 中 `class: normalizeClass([b(), { 'avue--detail': isDetail }])`)。所以「只针对详情态」的选择器就是:
```scss
.temporary-credit-limit-dialog .avue--detail .el-form-item__label { width:210px !important; flex:0 0 210px !important; }
```
新增/编辑弹窗无 `.avue--detail`,天然不受影响,不需要在 option 里做 mode 判断。
- 校验样式块编译:dev server 请求 `?vue&type=style&index=1&lang.scss`(**lang 必须写 scss,写 css 会 500**),grep 规则确认产物。
- 结论备忘:长 label 优先「加宽 + 换行 ≤2 行」,不要用单行省略 + tooltip —— 详情是纯阅读场景,hover 才能看全字段名的可发现性差。
-63
View File
@@ -1,63 +0,0 @@
# 2026-09-17 工作日志
## 配载管理(loading-manage):弹窗全部改为独立表单页/详情页
- 需求:`/business/loading-manage` 不再用弹窗,改成与新增合同(`/business/contract-manage/form?mode=add&name=新增合同管理`)一致的独立页面。
- 现状:新增/编辑**已经**是独立页(组件内 `isStandaloneFormPage` + `/business/loading-manage/form` 路由),但「详情」和「重新派单」仍是 el-dialog(用户截图即详情弹窗)。
- 改动(3 个文件):
1. `src/router/views/index.js`:新增 `/business/loading-manage/detail`(name 配载单详情,`meta.keepAlive:false, activeMenu:'/business/loading-manage'`),组件仍是 `loading-manage.vue`。
2. `src/views/business/loading-manage.vue`:
- computed 新增 `isStandaloneDetailPage`(path==='/business/loading-manage/detail')、`isStandalonePage`(form|detail 并集,作为隐藏列表容器的统一开关)、`detailPageTitle`、`formPageDefaultTitle`(add/edit/reassign 映射);`isStandaloneFormPage` 放行 `mode=reassign`。
- 模板中所有控制「是否独立页/是否弹窗」的 `isStandaloneFormPage` 改为 `isStandalonePage`;顶部标题按详情/表单取 `detailPageTitle`/`formPageTitle`。
- 新增 `gotoLoadingDetail(row)`(push `/detail?id=&name=配载单详情`,替换「配载单号」链接与操作列「详情」)、`gotoLoadingReassign(row)`(push `/form?mode=reassign&id=&name=重新派单`)、`initStandaloneDetailPage()`;`initStandaloneFormPage` 模式透传 edit/reassign;`openLoadingDialog` 对 add/edit/reassign 一律转独立页;`closeLoadingDialog` 判据改 `isStandalonePage`。
- `mounted` / `watch $route` 增加详情页分支(直接打开与页内切换都要 init);detail 分支除 id 变化外,`dialogMode !== 'view'` 也重新 init(防止从表单页切详情时残留表单态)。
- 兼容旧链接:`/business/loading-manage?detailId=x` 仍可用,会 replace 到详情页。
- 样式:底栏去掉写死的 `gap:8px`(全站统一 12px);页面模式的内容区解除 `max-height:78vh` 内嵌滚动盒 —— 原 `:global(.loading-manage-form-page .loading-manage-dialog__body)` 权重 (0,2,0) 低于 scoped 规则 (0,3,0) 一直没生效,改为 `.loading-manage-page .loading-manage-form-dialog.loading-manage-form-page .loading-manage-dialog__body` (0,4,0),内容改随 `#avue-view` 自然滚动(与合同页一致)。
3. `src/views/business/components/waybill-manage-page.vue`:`openLoadingDetail` 由 `/business/loading-manage?detailId=` 改为 push `/business/loading-manage/detail?id=&name=配载单详情`。
- 校验:dev server(2889)单独编译 4 个模块均 HTTP 200(含 scoped scss);`vite build` 仍会在无关文件 `src/page/login/facelogin.vue` 被沙箱敏感内容保护中断。
- 踩坑:**`<style scoped lang="scss">` 顶层写 `//` 注释**会让该样式模块编译 500(`Unexpected '/'. Escaping special characters with \ may help.`),已实测确认;顶层必须用 `/* */`。
- 未做:登录态无法在浏览器实测(无测试账号),需用户点 `/business/loading-manage` → 详情/重新派单 自测;`voucher-manage-detail` 跳配载时带的 `?loadingNo=` 参数本来就未被列表消费(历史遗留)。
## 运单管理详情:打卡时间轴改为按打卡顺序展示
- 文件:`src/views/business/components/waybill-manage-page.vue`(详情页「执行详情 → 打卡详情」tab)
- 需求:时间轴不再固定按流程节点顺序(发货/在途/到货/卸货/签收/回单),改为按打卡时间先后展示。
- 实现(不改后端、不改原始数据):
1. 新增 computed `orderedWaybillPunchRecords`:映射出 `punchedAt`(毫秒时间戳)后排序 —— **已打卡记录按打卡时间升序排在前**,未打卡记录保持后端返回的节点顺序排在其后;时间相同用原索引稳定排序。
2. 新增 method `waybillPunchRecordTimestamp(record)`:`punched === false` 或 `statusName === '未打卡'` 直接返回 null(防止后端返回计划时间造成误序);优先 `this.$dayjs(text).valueOf()`,失败兜底 `new Date(text.replace(/-/g,'/'))`(兼容 Safari)。
3. 模板 `el-timeline` 的 `v-for` 改用 `orderedWaybillPunchRecords`;`key` 由 `record.nodeCode || record.id || index` 改为 `${nodeCode||id||'punch'}-${index}`,避免同节点多次打卡(在途)时 key 重复。
- 校验:项目根目录临时脚本 `_verify-sfc.mjs`(@vue/compiler-sfc parse + compileScript + compileTemplate)通过,已删除脚本。
- 注意:`getPunchRecords`(`src/api/business/waybill-manage.js` → `/punch-records`)后端未提供节点排序字段,排序只能在前端做。
## 保险记录批量导入:前端归一化日期格式
- 文件:`src/views/vehicle/insurance-record.vue`(仅此一个文件)
- 背景:保险记录批量导入是**后端驱动**——前端 `handleImport` 经 `importBlob` 把原始 Excel 直接 POST 到 `/blade-transport/insurance-record/import-insurance-record`,前端不解析行。后端只认 `2026-09-01`,`2026-9-1` 导入失败。
- 决策:用户选择**前端归一化**(不动后端)。把 `openImportDialog` 改为自写 `httpRequest`:用 `xlsx` 读取 Excel → 把日期列(开始日期/结束日期/开票日期)`2026-9-1` 归一为 `2026-09-01` → 重写成新 File 再交给 `handleImportExcel` 上传;复用其失败明细下载与成功刷新逻辑。无日期列时回退为原文件(行为不变)。
- 关键方法:`normalizeExcelDates` / `readExcelRows` / `writeExcelFile` / `normalizeDateString`(正则 `^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$`,用 `new Date` 校验合法性后补零;非法/已标准格式返回 null 保持原值)。
- 校验:本机 dev(localhost:2889)待用户部署自测。注意所有单元格经 `sheet_to_json(raw:false)` 后均为字符串,与 transport-plan-import 的客户端解析模式一致。
## 车/船务模块导出 xlsx「创建时间/更新时间」为空(后端 tms-api 修复)
- 现象:`/vehicle/insurance-record` 批量导出的 xlsx 里「创建时间」「更新时间」两列空白,「更新人」有值。用户要求车/船务模块所有页面统一修。
- 定位:导出由**后端**生成(前端只传查询参数,`exportColumns` 参数后端未使用)。实测 `~/Downloads/保险记录2026-09-17 20_46_08.xlsx`:K/M 列单元格无 `<v>` 节点(值 null),E/F/J 日期列是数值 46266.0(带 numFmt `yyyy-MM-dd`,WPS 显示为日期)。
- 根因:Service 用 BladeX `org.springblade.core.tool.utils.BeanUtil`(**继承 Spring `BeanUtils`**)做 `copyProperties`。实体 `TenantEntity.createTime/updateTime` 是 `java.util.Date`,导出类字段是 `java.time.LocalDateTime` → Spring BeanUtils **类型不兼容静默跳过**,值为 null。而「更新人」是手动 `UserCache.getUserRealName(...)` 赋值的,所以有值。(Hutool 的 BeanUtil 能转 Date→LocalDateTime,但本项目用的是 BladeX 版本,不能。)
- 佐证:项目里正常的导出(Waybill / LoadingManage / CustomerArchive / CommonCargo)时间字段一律用 `java.util.Date`,并在 Service 里手动 `excel.setCreateTime(record.getCreateTime())`。
- 修复(后端仓库 `/Users/gxwebsoft/JAVA/tms-api`,blade-transport 模块):在 13 个导出方法里补显式赋值(Date→LocalDateTime 用 BladeX `DateUtil.fromDate`,`Func.isEmpty` 判空),不改 Excel 类字段类型(避免影响自定义 converter 与导入模板)。
```java
excel.setCreateTime(Func.isEmpty(x.getCreateTime()) ? null : DateUtil.fromDate(x.getCreateTime()));
excel.setUpdateTime(Func.isEmpty(x.getUpdateTime()) ? null : DateUtil.fromDate(x.getUpdateTime()));
```
- 涉及文件(13 个 Service + 1 新建 Excel 类 + 设备台账 controller/接口):InsuranceRecord / ViolationRecord / MaintenanceRecord / MaintenancePlan / TireReplacementRecord / AccidentRecord / AnnualInspectionRecord / MileageRecord / TransportChangeRecord / EtcRecord / OilElectricRecord / OtherExpenseRecord / EquipmentLedger。
- 设备台账特殊:原 `EquipmentLedgerExcel` 根本没有审计列(前端列表有),新建 `EquipmentLedgerExportExcel extends EquipmentLedgerExcel`(创建时间/更新人/更新时间),导出链路改用新类,**导入模板仍用旧类不受影响**。
- 未验证:本机无 mvn(仅 IDEA 内置),且离线模式缺 `blade-bom` 无法编译;用 javap 确认 `DateUtil.fromDate(Date)` 存在 + 逐行 grep 复核代替。需用户在 IDEA 里编译并重新部署后自测。
- 同类隐患(本次未改,其他模块):TemporaryCreditLimit、ProcessConfig、CommonRoute、CommonAddress、ContractManage、ProjectApply、ShippingTemplate 的导出 Excel 也是 LocalDateTime 且未手动 set 时间。
## 保险记录保额 -1 问题排查(未结案,待用户验证 DB 环境)
- 症状:新增弹窗保额不填 → 再开编辑弹窗显示 -1,触发「不能小于 0」校验。
- 已排除:前端(源码/dist 构建/git 全历史)、后端 tms-api + tms-erp-api-ws(源码/编译产物 javap/git 历史)均无任何 -1 赋值逻辑;保额为 formslot el-input,空值传 ''。
- 关键链路:编辑弹窗 beforeOpen 走 `/detail` 接口(getOne 原生查询),**不经过**列表 SQL 的 `CASE WHEN insured_amount<0 THEN 0` 兜底——所以表格显示 0、弹窗显示原始值。
- 数据库截图(用户 Navicat):该行 insured_amount=NULL,全表无负数 → 说明用户浏览的库与后端实际连接的库**不是同一个/DDL 不一致**。系统存在「空数值=-1」惯例(MaintenanceRecordMapper 里程 `CASE WHEN mileage=-1 THEN NULL`;前端 waybill-manage.js/process-config.js 均有 `Number(v)===-1?'':v` 掩码)→ 后端真实库的 insured_amount 列默认值很可能是 -1,MP 插入排除 null 列 → 默认值 -1 入库。
- 待验证:在后端 Nacos 数据源指向的库执行 `SHOW CREATE TABLE blade_insurance_record` + `SELECT insured_amount FROM blade_insurance_record WHERE policy_no='11111' AND vehicle_no='沪AE31312'`。
- 修复方案(待确认):1) ALTER 列默认值改 NULL + UPDATE 存量负数为 NULL;2) 前端 beforeOpen 对 insuredAmount/premium 做 null/-1 → '' 归一(参照 waybill-manage normalizeNumericDisplayValue);3) 列表 SQL 的 CASE 兜底与 detail 不一致,可顺手统一。
-122
View File
@@ -1,122 +0,0 @@
# 2026-09-18
## 修复:改成独立整页后点左侧菜单仍弹出旧弹窗(loading-manage / project-apply)
**现象**:配载单管理、项目管理已改为「新增/编辑/详情走独立整页」,但从独立页返回或直接点左侧菜单时,页面上仍会盖出一个老的 `el-dialog`(新增项目管理 / 配载单)。
**根因**(已读 Vue runtime-core 源码确认):
- `src/router/tab.js` 按 `tabKey`(默认 fullPath)为每个标签建一个具名 wrapper 组件放进 `wrapperMap`,`src/page/index/layout.vue` 用 `<keep-alive :include="$store.getters.tagsKeep">` 缓存 —— 每个标签页一个实例,全部缓存不销毁。
- 组件被 deactivate 后**仍会随 `$route`(全局响应式)重新渲染**;此时 `isStandalonePage` 变 false,`<component :is="isStandalonePage ? 'div' : 'el-dialog'">` 由 div 翻回 `el-dialog`,而 `v-model`(`projectBox`/`dialogVisible`)仍是 true,`append-to-body` 使弹窗 Teleport 到 body。
- `KeepAlive.deactivate` 调 `move(vnode, storageContainer, ..., moveType=OUT)`,但 `TeleportImpl.move`(`moveTeleport`)忽略传入的 moveType、一律按 REORDER 处理 → **teleport 出去的 DOM 不会被搬回 storage container**,永久残留在 `document.body` 上并显示出来。
**修法**(不回退 tab.js 的 `return wrapper` 提交 c1c4fa1,改修根因):
- 独立页容器固定为 `<div v-if="isXxxPage">`,不再按路由回退成 `el-dialog`。
- 页内其它 `append-to-body` 二级弹窗加 `deactivated()` / `beforeUnmount()` → `closeInnerDialogs()` 把 v-model 置 false。
**改动文件**:
- `src/views/business/loading-manage.vue`:容器改 `<div v-if="isStandalonePage">`;删 `dialogVisible`、`dialogTitle`、`resetLoadingDialog`、`clearLoadingDialog`、页脚弹窗分支(含「清空」);`watch.$route` 简化;新增 `closeInnerDialogs()`(routeChange / commonAddress / candidateSearch)+ `deactivated`/`beforeUnmount`。
- `src/views/business/project-apply.vue`:容器改 `<div v-if="isProjectFormPage">`;删 `projectFormContainer`/`projectFormContainerProps`、`projectBox`、`resetProjectDialog`、`handleCancelProject`;`closeProjectForm` 简化为直接 push 列表;新增 `closeInnerDialogs()`(changeRecordDetail / 附件文档预览 / 图片预览 / 选人)+ `deactivated`/`beforeUnmount`。
**校验**:dev server(2889) curl 两个 .vue 及其 scoped style 模块,均 HTTP 200(500 才是编译失败)。`vite build` 因沙箱敏感内容保护会在 facelogin.vue 中断,未用。
**遗留同类风险(未改,已告知用户)**:`business/components/waybill-manage-page.vue`(`detailContainer` 在 PageDetail / el-dialog 间切)、`settlement/components/{pre,formal}-settlement-editor.vue`、`transport-reconciliation-editor.vue`(`pageMode ? 'div':'el-dialog'`)、`business/components/waybill-import-dialog.vue`(`standalone`/`createPage`)。统一修法:容器类型在 `created()` 锁定,不随路由/prop 翻转。
---
## 追加修复:运单详情弹窗残留(waybill-manage-page.vue)
用户复现路径:打开 `/business/waybill-manage/detail?id=xxx` → 点「配载管理」菜单 → 页面上又冒出运单详情弹窗。与上面同一根因:`detailContainer()` 随 `isStandaloneWaybillDetailPage` 在 PageDetail / el-dialog 间翻转,`:append-to-body="!isStandaloneWaybillDetailPage"` 随之为 true,`v-model="detailBox"` 仍 true → Teleport 进 body 搬不回来。
**改动**(`src/views/business/components/waybill-manage-page.vue`):
- data 新增三个锁定标志 `formPageLocked` / `detailPageLocked` / `formModeLocked`,在 `created()` 一次性从 `isStandaloneWaybillFormPage` / `isStandaloneWaybillDetailPage` / `$route.query.mode` 取值。
- 模板里所有形态判定(根 class、`v-if="!detailPageLocked"` 的列表容器、`form-page-title`、`status`、独立表单页按钮块、分页 `v-show`、`:append-to-body`、detail-page class、内容 `v-if="detailBox || detailPageLocked"`、独立页 footer / 弹窗 footer slot)全部改读锁定标志。
- `detailContainer()` / `crudContainer()` / `pageFormOption()`(含 `boxType`)改读锁定标志,避免缓存实例的 option 变形。
- 新增 `closeInnerDialogs()`(关 detailBox、里程补录、附件文档/图片预览、变更路线、常用地址、流程图、运输路线/地址/站点/地图、常用货物、货物导入、过程配置、Excel 导入)+ `deactivated()` / `beforeUnmount()`。
**关键验证**:`onDeactivated` 在子组件里也会触发 —— apiLifecycle 的 `injectHook` 会向上遍历父链,发现 KeepAlive 父级就把钩子注入到根实例(runtime-core 的 `injectToKeepAliveRoot`),所以写在子组件 waybill-manage-page 上的 `deactivated` 有效。
**校验**: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 轮改动一起待确认)。
-11
View File
@@ -1,11 +0,0 @@
# 2026-09-30
## /wel/index 首页重构为运营数据看板(按蓝湖效果图)
- 新增依赖:echarts ^6.1.0(corepack pnpm add echarts)。
- 重构 `src/views/wel/index.vue`:改为「核心经营指标 / 运输结构 / 业务单元情况 / 项目情况 / 财务情况 / 车队与线路 / 车务管理 / 底部占比与趋势」8 大板块 + 版权页脚,删除原欢迎横幅/快捷入口。
- 新增文件:
- `src/views/wel/chart-options.js`:排行条形/盈亏双向/环形/分组柱/面积折线/堆叠柱 6 类 option 生成器(fmt 千分位、红涨绿跌 rich label)。
- `src/views/wel/dashboard.scss`:看板公共样式,各组件 scoped `@import '../dashboard.scss'`(子目录注意路径)。
- `src/views/wel/components/`:wel-chart.vue(ECharts 封装:ResizeObserver + activated resize + dispose)、wel-filters.vue(统计时间/条件筛选 mock)、rank-panel.vue(排行/盈亏通用面板)、section-kpi / section-structure / section-business / section-project / section-finance / section-fleet / section-vehicle / section-summary.vue。
- 数据全部为 Mock,组件内已注释标注对接点;蓝湖红字标注的指标口径未定,待后端统计接口(估 12~18 个)确定后替换。
- 自检:dev server(2889) 下所有新文件及 `?vue&type=style&...&lang.scss` 均 200;`/node_modules/.vite/deps/echarts.js` 200。vite build 因 facelogin.vue 沙箱中断不可用,未跑。
-18
View File
@@ -1,18 +0,0 @@
# 2026-10-13
## 车辆调度弹窗:使用部门级联选择器宽度对齐
- 现象:`/transportCapacity/vehicle-dispatch` 新增/编辑弹窗中,使用部门 `el-cascader` 明显窄于同行车牌号 `el-input`。
- 根因:`el-cascader` 是 `display: inline-block` 宽度按内容收缩(不像 `el-autocomplete` / `el-input` 有 `--el-input-width: 100%` 兜底);而页面里写的 `.vehicle-dispatch-page :deep(.vehicle-dispatch-form-control){width:100%}` 命中不到 —— avue-crud 弹窗默认 `append-to-body: true`(`append-to-body": validData(crud.tableOption.dialogAppendToBody, true)`),DOM Teleport 到 body,已不在 `.vehicle-dispatch-page` 之下。
- 修法(走项目既有约定):
1. `src/option/transportCapacity/vehicle-dispatch.js` 增加 `dialogCustomClass: 'vehicle-dispatch-dialog'`;
2. `src/styles/element-ui.scss` 增加全局规则 `.vehicle-dispatch-dialog .el-form-item__content > .el-cascader { width:100%; max-width:100% }`。
- 自检:dev(2889) `curl /src/styles/element-ui.scss` 与 `/src/option/transportCapacity/vehicle-dispatch.js` 均 200,编译产物含新规则。
- 遗留观察:同文件里 `.vehicle-dispatch-page :deep(.avue--detail ...)` 那一批查看态禁用样式,同样因 Teleport 属死规则(未改,待确认是否需要迁到 `.vehicle-dispatch-dialog` 作用域)。
## 追加修正(同日 20:xx):第一次改完用户反馈「还是没对齐」
- 原因一:选择器写成了 `.el-form-item__content > .el-cascader`(直接子级)。**avue 表单项在 `el-form-item__content` 与控件之间还有两层包裹 div**(`packages/element-plus/form/index3.js`:content > div > [div|elTooltip] > 控件),必须用后代选择器。已改成 `.vehicle-dispatch-dialog .el-form-item__content .el-cascader`。
- 原因二(更隐蔽):option 用 `labelWidth: 'auto'`,**EP 的 auto 是逐项计算**(实测:3 字 54/64px、2 字 40px、4 字无星号 68px、4 字带星号 78px),label 越宽控件可用宽度越小 → 车牌号 input 486px、使用部门 cascader 472px,仍差 14px。已加 `.vehicle-dispatch-dialog .el-form-item__label { flex: 0 0 calc(4em + 24px) !important; width: calc(4em + 24px) !important }`(本弹窗左右两列最长 label 均为 4 汉字)。
- 验证方式(可复用):临时在项目根建 `cascader-width-check.html`,Vite 直接服务 → `import './src/option/...'` 用真实 option + `import './src/styles/element-ui.scss'`,挂 avue-crud 并 `mounted(){ this.$refs.crud.rowAdd() }` 弹出真实弹窗;用 agent-browser(`/Users/gxwebsoft/.workbuddy/binaries/node/workspace/node_modules/.bin/agent-browser`)`open` + `eval` 读 DOM 链路与 `getBoundingClientRect`。修完已删除该临时文件。
- 结果:全部 label 80px,车牌号 input 与使用部门 cascader 均为 left=732 / width=470 / right=1202,完全一致。
+94 -57
View File
@@ -1,71 +1,108 @@
# 项目长期记忆(tms-erp-web-ws / Saber3)
# 项目长期记忆(tms-erp-web-ws / Saber3 客商模块)
后端:`/Users/gxwebsoft/JAVA/tms-api`(`tms-erp-api-ws` 滞后,别看错仓库)。
## 设计约定(已确认)
## 列表 / 表单 / 详情三态同组件分流
`/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`。
### 表单 placeholder 统一为「请输入 / 请选择」(不带字段名)
- **规范**:输入框 placeholder 只写动作词 `请输入` / `请选择`,**后面不跟字段名称**(label 已说明是什么字段,重复无意义)。
- 已生效页面:waybill-manage、transport-plan、shipping-template、temporary-credit-limit(搜索栏 + 弹窗表单)、**driver.vue**(2026-09-09 补齐 18 处)。
- **3 类例外必须保留,不要误简化**:
1. 复合操作:如 `请输入或选择车辆`(既可键盘输入又可点按钮弹窗选车),丢掉会损失语义。
2. 日期区间的 `起` / `止`:是起止标记,不是字段提示。
3. 默认值说明:如资格证号 `默认身份证号`、车牌号 `如:京A12345`,属于示例/默认值提示,非「请输入」格式。
- **校验错误提示 message 保持带字段名**(如「请选择准驾车型」「请输入档案编号」),与 placeholder 定位不同——报错必须指明具体哪个字段,不能只说「请选择」。
- Avue 配置化表单:在 option 各列加 `placeholder`;搜索栏可用通用工具 `withSearchPlaceholders([...])`(见 temporary-credit-limit.js)。
- 批量改法:Python 脚本按**完整 placeholder 字符串**做精确替换,每规则校验 `count > 0` 否则中断,避免误伤;改完用 `@vue/compiler-sfc` 的 parse + compileTemplate + compileScript 验证(脚本须放项目根目录下跑,放 /tmp 解析不到 node_modules)。
## ⚠️ keep-alive 三坑
背景(`router/tab.js` + `page/index/index.vue`):按 `fullPath` 建 wrapper,`<keep-alive :include="tagsKeep">` 缓存,**每标签一实例、永不销毁**,deactivate 后**仍随全局 `$route` 重渲染**。
### 弹窗分组:统一用全局 `<section-card>`
- 弹窗 body 背景由 `src/styles/element-ui.scss` 全局 `.el-dialog__body { background:#f5f6fa; padding:16px }` 统一灰底。
- 每组 = 白底卡片:4px 主色竖条 + 8px 间距 + 圆角 6px + 淡阴影。组件全局注册 `<section-card title="...">`,支持 `#title` / `#extra` slot。
- 仅 3 项的小分组用 `el-col :span="8"` 均分一行。
**坑一:独立页容器禁用 `<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`。
### Avue 内置弹窗用 section-card:dialogCustomClass 透明化 avue-form
- option 顶层加 `dialogCustomClass:'xxx-dialog'`,再在 `element-ui.scss` 写三条:
1. `.xxx-dialog .avue-form { background:transparent; box-shadow:none; padding:0 }`
2. `.xxx-dialog .avue-form__group > .el-col > .el-form-item { margin-bottom:0 }`
3. `.xxx-dialog .avue-dialog__footer, .avue-crud__dialog .xxx-dialog .avue-dialog__footer { margin-top:0 !important }`
**坑二:宿主 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。
### 弹窗底部操作栏浮动(全局 CSS)
- `.el-dialog__body { max-height:calc(100vh - 200px); overflow-y:auto; overflow-x:hidden }`
- `.el-dialog__footer { margin:0; padding:12px 20px; background:#fff; border-top:1px solid #ebeef5; box-shadow:0 -2px 8px rgba(0,0,0,.04) }`
- Avue 内置弹窗 footer 真实 class 是 `.avue-dialog__footer`,须 `position:sticky; bottom:0; flex:none; background:#fff` 贴底。
**坑四:avue-crud 弹窗 `append-to-body` 默认 true(`dialog-form3.js`:`validData(tableOption.dialogAppendToBody, true)`),DOM Teleport 到 body。** 于是页面里以页面容器为祖先的 scoped 选择器(`.xxx-page :deep(.yyy)`)**命中不到弹窗内容**,尤其 `el-cascader`(inline-block、宽度按内容收缩,不像 `el-input`/`el-autocomplete` 有 `--el-input-width:100%` 兜底)会明显比同行 input 窄。修法:option 里配 `dialogCustomClass: 'xxx-dialog'`,规则写到 `src/styles/element-ui.scss` 的 `.xxx-dialog ...`(项目既有约定,10+ 处同款);或把基准类放在 **Teleport 内部元素**上(如 ship.vue 的 `.ship-form` 挂在 el-form 上,自身带 scope 属性,照样生效)。
### 上传证件区域尺寸(transportCapacity)
- 身份证/行驶证/道路运输证等固定 **240×151px**(1.586:1)。
- scoped 覆盖 `.xxx-uploader--large { width:240px }` 与 `--large .el-upload { width:100%; height:151px }`。
**坑五:avue 表单项控件不是 `.el-form-item__content` 的直接子级。** `form/index3.js` 渲染为 `content > div > [div|elTooltip] > 控件`,所以 `.el-form-item__content > .el-cascader` 这类**直接子级选择器对 avue 表单全部失效**(手写 `<el-form-item>` 才生效),一律用后代选择器。
### 上传图片点击放大预览
- `src/components/image-upload-field/main.vue` 用 `<el-image preview-src-list fit="contain" preview-teleported :z-index="3000" @click.stop>`,阻止冒泡到 el-upload。
**坑六:`labelWidth: 'auto'` 在 Element Plus 里逐项计算,不是全表单共享。** 实测同一弹窗:3 字 54/64px、2 字 40px、4 字 68px、4 字带必填星号 78px;label 越宽 → 控件可用宽度越小,同级控件宽度就不一致(车牌号 input 486 vs 使用部门 cascader 472)。要让同一行控件等宽必须统一 label 宽度:`.xxx-dialog .el-form-item__label { flex: 0 0 calc(Nem + 24px) !important; width: calc(Nem + 24px) !important }`(N=本列最长汉字数,与 ship.vue、`.dialog-form--carded` 同款)。
### 独立表单页标题 `archive-page-form__title`
- 样式全局化到 `src/styles/element-ui.scss`:18px/600/#303133 + margin-bottom:20px + 4px 主色竖条 + 8px 间距。
- business-crud-page 系列通过 `formPageTitle` prop 注入;loading-manage / master-order 手写前置标题。
**验证 avue 弹窗 DOM/宽度的可复用办法:** 项目根建临时 html(Vite 直接服务)→ 引真实 option 与 `src/styles/element-ui.scss`,挂 avue-crud 并 `mounted(){ this.$refs.crud.rowAdd() }` 弹出真弹窗;用 `agent-browser`(`/Users/gxwebsoft/.workbuddy/binaries/node/workspace/node_modules/.bin/agent-browser`)`open` + `eval` 读 DOM 链路与 `getBoundingClientRect`,用完删文件。
### 表格列内嵌输入控件全宽
- `element-ui.scss` 全局兜底:`.el-table .el-table__body td.el-table__cell .cell > .el-input-number/.el-input/.el-select/.el-date-editor/.el-cascader { width:100% }`。
**坑三:路由表重复定义 → 内容区整页空白(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」重复项。
### 业务状态标签文字化
- 全站"状态语义"的 `el-tag` 加 `class="status-text"`,转为普通文字,不带颜色/背景/边框/圆点。
- 实现:`element-ui.scss` 末尾 `.el-tag.status-text { ... }`。
## 列表页右上角按钮组(table-header-tools 标准件)
⚠️ **2026-10-14 修正:最终方向是手写列表页改用 `avue-crud`**。用户要求与其它模块"完全一致",而 avue 的列显隐抽屉(`el-drawer`+表格,隐藏/冻结/过滤/排序,行拖拽)是 avue-crud 内部子组件(`inject: ['crud']`),脱离 avue-crud 拿不出来;第 4 键也是 avue 的"表格↔卡片视图"切换。所以 `table-header-tools` 只是过渡仿制件,**新页面不要再接**,直接写 avue-crud。
### 弹窗 / 独立表单页底栏按钮统一规则
- **排序**:从左到右按次→主排(次要居左、主操作居右),主操作永远放最后。
- 顺序模板:`[可选辅助工具(如同步/导入)][取消][保存草稿][提交/确认]`
- **容器布局(2026-08-27 改为「去 gap」)**:`display:flex; justify-content:flex-end;`(**不写 `gap`**)。按钮间距统一由 Element Plus 全局默认的相邻按钮 `.el-button+.el-button{margin-left:12px}` 提供,全站恒为 12px。原各 footer 写死的 `gap:8/12px` 已全部移除。`src/styles/element-ui.scss` 里 `.el-dialog__footer .el-button + .el-button` 也已从 `8px` 改 `12px`,与之一致。弹窗关闭按钮继承 Element Plus 自带 `.el-dialog__footer` 全局规则(见上条),独立页用 `.archive-form__footer` 配 `.archive-page-form & { position:fixed; right:0; left:230px; bottom:0; z-index:10; margin:0; padding:12px 24px; border-top:1px solid #eff1f7; background:#fff; box-shadow:0 -2px 8px rgba(0,0,0,.06); }`。
- **⚠️ 浮动底部必须用 `position:fixed`,不能用 `position:sticky`**:本项目布局 `.app-main` / `.basic-container` 祖先带 `overflow:hidden`,会导致 `sticky` 永不触发(footer 不吸底)。固定写法照搬 customer-archive:
```scss
.xxx-actions {
position: fixed; right: 0; left: 230px; bottom: 0; margin: 0; z-index: 10;
/* flex-end + padding + border-top + 白底 + 上投影 */
}
:global(.avue--collapse .xxx-actions) { left: 60px; } /* 侧栏折叠 */
:global(.avue-layout--horizontal .xxx-actions) { left: 0; } /* 横向布局 */
```
注意 `:global()` 要把「整个选择器」包进去(customer-archive 写法),不要只包前缀,否则 scoped 会在中间选择器上追加 data 属性导致不匹配。
- **颜色层级 3 档**:
| 语义 | type | 备注 |
| --- | --- | --- |
| 次要(取消 / 关闭) | 不写 type(默认) | 灰描边 |
- **全站底部操作栏「返回」已统一改为「取消」**:独立表单页返回列表、弹窗关闭、主从编辑器返回等场景一律写「取消」;仅只读查看 / 纯关闭场景保留「关闭」。
| 中间步骤(保存草稿 / 按匹配结果更新) | `type="primary" plain` | 蓝描边 |
| 主操作(提交 / 完成对账 / 复评确认) | `type="primary"` | 蓝实心 |
- **禁区**:
- 保存草稿和主操作不允许同为 `type="primary"`,否则两个蓝实心无视觉层级。
- 主操作禁止用 `type="success"`(绿)。全站统一深蓝实心。
- **生效页面(已按规则统一)**:
- `business/project-apply.vue`(footer + 变更 footer)
- `vehicle/customer-archive.vue` 全部 5 处 footer(`archive-form__footer` 主表单页 + `contact-dialog`/`receipt-dialog`/`invoice-dialog` 子弹窗 + `score-detail-dialog` 含总分合计)
- `business/components/business-crud-page.vue` 的 PageAvueForm 独立表单页(`#menu-form-before` slot 新增 `取消` 按钮,shipping-template 模式验证通过;影响 `/business/waybill-manage/form`、`/business/transport-plan/form`、`/business/shipping-template/form` 三处独立表单页)
- `business/components/master-order-editor.vue` 主 `<footer>`(取消 default + 暂存/确认创建 plain + 创建并调度 primary;`position:fixed` 浮动,含 `avue--collapse`/`avue-layout--horizontal` 左偏移)
- **payment 模块**表单页(`__actions` 均 `flex-end` + `position:fixed` 浮动,含 `avue--collapse`/`avue-layout--horizontal` 左偏移):
- `payment-application-form.vue`(`[取消][保存 plain][提交]`)
- `invoice-application-form.vue` / `invoice-receipt-form.vue`(`[取消][同步 plain][保存 plain][提交]`)
- `bill-ledger-form.vue`(`[取消][确认 primary]`)
- `bill-payment-form.vue`(`[取消][保存 plain][提交]`)
- `receipt-flow-form.vue`(`[取消][确认 primary]`)
- `receipt-claim-record-form.vue`(单「关闭」按钮,居中→右对齐浮动)
- **settlement 模块**:
- `settlement/components/pre-settlement-editor.vue`(弹窗 + pageMode 两处 footer:保存加 `plain`;`&__page-actions` 加 `position:fixed` 浮动,含 `avue--collapse`/`avue-layout--horizontal` 左偏移)
- `settlement/components/formal-settlement-editor.vue`(`.formal-editor__page-actions` 加 `position:fixed` 浮动,含 `avue--collapse`/`avue-layout--horizontal` 左偏移;按钮已 `[取消][提交]` 合规)
- `settlement/receivable-payable-detail.vue`(生成费用弹窗「上一步」改 `plain`)
- **已合规未动**:`settlement/components/transport-reconciliation-editor.vue`(`[取消][保存草稿 plain][按匹配结果更新账单 plain][完成对账 primary]`)、`settlement/components/settlement-adjustment-editor.vue`(弹窗 `[取消][保存 primary]`)、`business/temporary-credit-limit.vue`。
- **Avue-form footer 结构注意**:`#menu-form-before` slot 内容在 Avue 内置按钮之**左**;`#menu-form` slot 在 Avue 内置按钮之**右**。要实现 `[次要][Avue 主操作]` 顺序,把次要按钮放进 `#menu-form-before`,主操作保留 Avue 内置即可。
- **Avue 表单 footer 按钮去图标(统一纯文字)**:Avue 的 `avue-form` 提交/清空按钮内置图标(`submitIcon: 'el-icon-check'`、`emptyIcon: 'el-icon-delete'`),且 Avue 源码用 `|| 'el-icon-xxx'` 兜底,无法通过 option 配置去掉。已在 `business-crud-page.vue` 样式里用 `:global(.business-crud-form-page-dialog .avue-form__menu .el-icon){display:none}` 统一隐藏,仅作用于独立表单页(PageAvueForm 容器),不影响其它弹窗。新增/编辑弹窗内的 Avue 表单若也要去图标,复用此选择器即可。
- **复杂 footer 例外**:当 footer 需要在按钮组左侧展示「总分合计 / 数量统计」等聚合信息时,块用 `flex:1` 推自己到最左,按钮组按上述规则排在右半边(参考 `score-detail-dialog`)。
手写 `el-table` 列表页若暂时保留,右上角统一用 `src/components/table-header-tools/main.vue`,复刻 avue-crud 默认 4 键:刷新 `el-icon-refresh` / 列设置 `el-icon-operation` / 搜索显隐 `el-icon-search` / 表格尺寸 `el-icon-grid`(default→large→small 循环)。
## 关键文件
- `src/components/section-card/main.vue`
- `src/styles/element-ui.scss`
- `src/views/vehicle/customer-archive.vue`
- `src/views/transportCapacity/{driver,vehicle,ship}.vue`
- `src/views/business/project-apply.vue`
- 接入范式:`<table-header-tools :columns="columns" v-model:visible-column-keys="visibleColumnKeys" v-model:table-size="tableSize" v-model:search-visible="searchVisible" @refresh="loadTable" />`;配套:搜索区 `v-if="searchVisible"`、el-table `:size="tableSize"`、列用 computed `visibleColumns = columns.filter(c => visibleColumnKeys.includes(c.prop))`。⚠️ 不能 `v-for` + `v-if` 同元素(Vue3 v-if 优先级更高,取不到 column 变量)。
- 表格在子组件时(如 `payment/components/bill-*-table.vue`):子组件加 props `searchVisible` + emits `refresh`/`update:searchVisible`,父页面 `v-model:search-visible` + `@refresh`,搜索子组件加 `v-if="searchVisible"`。
- `icon="el-icon-xxx"` 字符串**依赖 Avue 的全局 el-icon 映射**(`main.js` 只注册 EP 的 PascalCase 图标),临时验证页没 `app.use(Avue)` 会渲染成空圆按钮 —— 别误判成组件 bug。
- 组件已加 `deactivated()` 关列设置弹窗(防 keep-alive 下 Teleport 弹窗残留)。
- 已接入(过渡态,待逐页改 avue):business/{master-order,loading-manage,voucher-manage}、settlement、payment 模块手写页。
## 环境注意
- Vite 代理跨域:`axios.withCredentials=true`,改 `.env`/`vite.config.mjs` 须重启 dev。
## avue-crud 列表页改造要点(payment 模块样板,2026-10-14)
- **列显隐抽屉**:点第 2 键弹出,标题"列显隐"(语言包 `crud.showTitle`),表头 列名/隐藏/冻结/过滤/排序。直接读写 avue crud 的列配置,页面无需实现。
- **第 4 键**:`handleGridShow()` = 表格 ↔ 卡片视图(`tableCard` 按列通用渲染),页面无需写卡片模板。
- `searchOrder` 是**降序**排序(值越大越靠前):`list.sort((a,b) => (b.searchOrder||0) - (a.searchOrder||0))`。首次改造踩过坑。
- `searchIndex: 4` + `searchIcon: true` → 默认只显示前 4 个搜索项 + "展开/收起"(columnLen > searchIndex 时才出现该按钮)。
- **搜索项 label 与表格列 label 不同**(列「收款方」/搜索「客户名称」):单独定义 hide 列 + `searchProp` 指向后端字段(avue 在 search-change 里自动改名)。
- 搜索专用列加 `hide: true` + `showColumn: false`,否则会混进列显隐抽屉。
- ⚠️ **column 数组顺序 = 表格列顺序**:搜索专用列别插在表格列中间(会把表格列顶到前面);deptName 这种"既搜索又展示"的列要放回表格列的正确位置。
- 分页:`page: { currentPage, pageSize, pageSizes, total }`;`on-load` 由 avue 内部(table-page)在 created / 翻页 / 改页容量时触发,`current-change`/`size-change` 里**只改 page 不要再调 onLoad**(否则双请求)。
- `onLoad(page)` 只用 `this.query`(avue 传的 page 对象含 total/pageSizes 等分页字段,别 spread 进查询参数)。
- 搜索参数转换放 `searchChange(params, done)`:日期区间数组拆成 `xxxStartDate/xxxEndDate`;所属组织级联用 `normalizeOrganizationSearch(params, 'deptName')`(见 `mixins/organization-search.js`,option 里该列加 `searchslot: true`)。
- 单元格空值统一 `-`:列级 `formatter: (row, value) => ...`;金额 `Number(v||0).toFixed(2)`;状态/链接用 `slot: true` + `#prop` 插槽。
- 操作列用 `#menu` 插槽(`menuWidth: 320`),`addBtn/viewBtn/delBtn/editBtn: false` 关掉 avue 自带按钮。
## 地址/行政区划展示(全站既有规则)
「省/市/区县」都要空格分段,但**省与自治区不重复展示**:`上海市浦东新区`→`上海市 浦东新区`,`广西壮族自治区南宁市邕宁区`→`南宁市 邕宁区`(同 `formatRoadLocation` / `formatRouteCityDistrict` / 总单列表 `formatRoadAddress` 一致)。
- 统一入口:`src/utils/region-address.js` 的 `formatRegionAddress(value)`(按后缀切分行政区划,`州+市`、`县+区` 连写合并;**只有整串都是行政区划**才重排,站点名「南宁站/广州南站」与含门牌的详细地址原样返回)。v-for 列表里用它做展示时,直接把函数挂进 `methods` 用 ES6 简写(`formatRegionAddress,`)。
- ⚠️ 路线节点的名称字段来自后端 `routeJson`:`routes[i].departureName` = 第 i 段**到货地/途经地**(不是本段发货地),末段 = 最终收货地;`master.departureName/arrivalName` 才是总发货/总收货。总单调度/详情都按「上一个节点的 departureName」拼分段起点。
## 样式硬规则
- 底栏 `flex; justify-content:flex-end`,**不写 gap**(靠 EP 默认 `.el-button+.el-button{margin-left:12px}`);浮动底栏必须 `position:fixed`(祖先 overflow:hidden 使 sticky 失效)+ `:global(.avue--collapse .x){left:60px}`、`:global(.avue-layout--horizontal .x){left:0}`,`:global()` 要包整个选择器。
- 按钮层级:次要=无 type;中间步骤=`primary plain`;主操作=`primary`(禁绿、禁两个蓝实心)。顺序 `[辅助][取消][保存草稿][提交]`。
- 独立页标题 `.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 只写 `请输入`/`请选择`;例外:`请输入或选择车辆`、日期区间 `起`/`止`、示例值。校验 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`)。
## Element Plus 图标两种用法的机制差异(易误判)
`main.js` 里 `for (...ElementPlusIconsVue) app.component(key, component)` 全局注册了全部图标,因此模板里可以写标签形式 `<Location />`(靠全局注册解析)。
但 `:icon="OfficeBuilding"` 是**表达式**,编译成 `_ctx.OfficeBuilding`,只会从 data / props / computed / setup 返回值里取 —— **组件对象必须放进 `data()`**;放进 `components: {}` 注册里模板取不到,会静默渲染不出图标(不报错,只是空白)。
项目既有惯例:需要 `:icon` 的图标一律 `import` 后在 `data()` 里 `return { Xxx }`(transport-plan-page、waybill-manage-page、business-crud-page 都是这么写的)。
注:标签形式(如 `<Location />`)不依赖 data,放不放 data 都能显示;仅 `:icon` 必须放 data。
## business-crud-page 复用模式
- option 由 prop `crudOption` 经 `cloneOption` 克隆。
- 独立表单页走 `PageAvueForm`,不包 el-dialog。
- 自定义详情弹窗:`config.detailButton=true` + `detailSections:[{title, fields}]`。
-2
View File
@@ -13,8 +13,6 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="format-detection" content="telephone=no" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<link rel="stylesheet" href="/iconfont/index.css" />
<link rel="stylesheet" href="/iconfont/avue/iconfont.css" />
<link rel="stylesheet" href="/iconfont/saber/iconfont.css" />
-52
View File
@@ -1,52 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>oa登录</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.10.2/jquery.min.js">
</script>
<script>
// 获取URL查询字符串
const queryString = window.location.search;
// 使用URLSearchParams解析查询字符串
const urlParams = new URLSearchParams(queryString);
// 获取特定的参数值
var userid = urlParams.get('userid'); // 用户id
var username = urlParams.get('username'); // 用户名
var redirectUrl = urlParams.get('redirectUrl'); // 重定向地址
if(userid==null){
userid="";
}
if(username==null){
username="";
}
$.get("/oaApi/getToken?userid="+userid+"&username="+username,function(result){
debugger
//成功返回token
if(result.data.token){
//设置系统的cookie
document.cookie = "X-AUTH-TOKEN="+result.data.token;
if(redirectUrl==null){
//如果是登录就跳转到系统首页
window.location.href = "./";
}else {
//如果是待办有重定向就跳转
window.location.href = redirectUrl;
}
}else {
alert("该业务系统未同步当前oa用户");
}
});
</script>
</head>
<body>
</body>
</html>
+5 -3
View File
@@ -6,7 +6,8 @@
"prod": "vite --mode production",
"build": "vite build",
"build:prod": "vite build --mode production",
"serve": "vite preview --host"
"serve": "vite preview --host",
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
@@ -23,7 +24,6 @@
"crypto-js": "^4.1.1",
"dayjs": "^1.10.6",
"disable-devtool": "^0.3.8",
"echarts": "^6.1.0",
"element-plus": "^2.14.2",
"exceljs": "^4.4.0",
"highlight.js": "^11.9.0",
@@ -47,9 +47,11 @@
"prettier": "^2.8.7",
"sass": "^1.85.1",
"terser": "^5.31.1",
"typescript": "^7.0.2",
"unplugin-auto-import": "^0.11.2",
"vite": "^5.4.19",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-setup-extend": "^0.4.0"
"vite-plugin-vue-setup-extend": "^0.4.0",
"vue-tsc": "^3.3.11"
}
}
-4233
View File
File diff suppressed because it is too large Load Diff
-5
View File
@@ -1,10 +1,5 @@
packages:
- '.'
allowBuilds:
'@parcel/watcher': true
es5-ext: true
esbuild: true
patchedDependencies:
'@smallwei/avue': patches/@smallwei__avue.patch
+11
View File
@@ -58,3 +58,14 @@ export const changeStatus = (id, status) => {
},
});
};
export const getBfiExchangeRate = (currencyCode, effectdate) => {
return request({
url: '/blade-transport/bfi/exchange-rate',
method: 'get',
params: {
currencyCode,
effectdate,
},
});
};
-8
View File
@@ -90,11 +90,3 @@ export const expireStats = params =>
method: 'get',
params,
});
/** 按合同负责部门预览下一合同编号 */
export const nextContractNo = organizationId =>
request({
url: `${baseUrl}/next-contract-no`,
method: 'get',
params: { organizationId },
});
-9
View File
@@ -79,15 +79,6 @@ export const cancel = id =>
},
});
export const start = id =>
request({
url: `${baseUrl}/start`,
method: 'post',
params: {
id,
},
});
export const complete = id =>
request({
url: `${baseUrl}/complete`,
-12
View File
@@ -22,18 +22,6 @@ export const getVoucherImages = (waybillId, params = {}) =>
...params,
},
});
export const getVoucherFolders = waybillId =>
request({
url: '/blade-transport/process-config/voucher-folders',
method: 'get',
params: { waybillId },
});
export const getVoucherFolderDetail = (voucherId, plateNo) =>
request({
url: '/blade-transport/process-config/voucher-folder-detail',
method: 'get',
params: { voucherId, plateNo },
});
export const submit = api.submit;
export const remove = api.remove;
export const copy = api.copy;
-1
View File
@@ -16,7 +16,6 @@ export const submit = api.submit;
export const remove = api.remove;
// 项目资金使用风险统计(高风险 / 中风险数量),供列表头部快速筛选标签使用
// 暂未启用:后端 /fund-risk-stats 端点尚未实现,列表页已移除调用,等后端就绪后恢复
export const getFundRiskStats = params =>
request({
url: `${baseUrl}/fund-risk-stats`,
-11
View File
@@ -52,17 +52,6 @@ export const createFileTask = data =>
request({ url: `${fileTaskBaseUrl}/createFileTask`, method: 'post', data });
export const getPartUploadUrl = (id, partNumber) =>
request({ url: `${fileTaskBaseUrl}/getUploadUrl`, method: 'get', params: { id, partNumber } });
export const uploadPartByProxy = (id, partNumber, partBlob) => {
const formData = new FormData();
formData.append('file', partBlob, `part-${partNumber}.bin`);
return request({
url: `${fileTaskBaseUrl}/uploadPart`,
method: 'post',
params: { id, partNumber },
data: formData,
timeout: 600000,
});
};
export const updateFileTask = data =>
request({ url: `${fileTaskBaseUrl}/updateFileTask`, method: 'post', data });
export const pauseFileTask = ids =>
+3 -40
View File
@@ -33,33 +33,6 @@ export const getPunchRecords = waybillId =>
method: 'get',
params: { waybillId },
});
/** 编辑打卡记录(含打卡状态;punched=false 表示取消打卡) */
export const savePunchRecord = data =>
request({
url: `${baseUrl}/punch-record/save`,
method: 'post',
data,
});
/** 运单车辆实时定位 */
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`,
@@ -75,16 +48,6 @@ export const getImportDetails = params => request({ url: `${baseUrl}/import-batc
export const removeImportBatches = ids => request({ url: `${baseUrl}/import-batch/remove`, method: 'post', params: { ids } });
export const getImportOptions = () => request({ url: `${baseUrl}/import-batch/options`, method: 'get' });
export const exportImportTemplate = () => request({ url: `${baseUrl}/import-batch/export-template`, method: 'get', responseType: 'blob' });
export const saveImportDraft = data => request({ url: `${baseUrl}/import-batch/draft`, method: 'post', data, timeout: 120000 });
export const validateImport = data =>
request({ url: `${baseUrl}/import-batch/validate`, method: 'post', data, timeout: 120000 });
export const exportImportFailures = data =>
request({
url: `${baseUrl}/import-batch/export-failures`,
method: 'post',
data,
responseType: 'blob',
timeout: 120000,
});
export const confirmImport = data =>
request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data, timeout: 120000 });
export const saveImportDraft = data => request({ url: `${baseUrl}/import-batch/draft`, method: 'post', data });
export const validateImport = data => request({ url: `${baseUrl}/import-batch/validate`, method: 'post', data, responseType: 'blob' });
export const confirmImport = data => request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data });
-23
View File
@@ -1,23 +0,0 @@
import request from '@/axios';
export const getMkPublicDetail = (bizType, id) => {
return request({
url: `/blade-transport/mk-process/public/${bizType}/detail`,
method: 'get',
meta: {
isToken: false,
},
params: { id },
});
};
export const postMkPublicProcessMessage = (bizType, data) => {
return request({
url: `/blade-transport/mk-process/public/${bizType}/process-message`,
method: 'post',
meta: {
isToken: false,
},
data,
});
};
+1 -1
View File
@@ -22,7 +22,7 @@ export const approve = data => request({ url: `${baseUrl}/approve`, method: 'pos
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
export const syncKingdee = id =>
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id }, timeout: 120000 });
export const invoiceTypeOptions = [
{ label: '电子专票', value: 'electronic_special' },
+19 -8
View File
@@ -5,8 +5,17 @@ const baseUrl = '/blade-transport/invoice-receipt';
export const getList = (current, size, params) =>
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
export const getInvoicePool = keyword =>
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params: { keyword } });
export const queryInvoicePool = params =>
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params });
export const downloadInvoicePoolAttachment = invoiceNo =>
// 金蝶票池发票文件下载(后端代理 A801-2 流式回源),耗时较普通接口长,放宽到 120s
request({
url: `${baseUrl}/invoice-pool/attachment/download`,
method: 'get',
params: { invoiceNo },
responseType: 'blob',
timeout: 120000,
});
export const getSettlementCandidates = (keyword, receiptId) =>
request({
url: `${baseUrl}/settlement-candidates`,
@@ -25,8 +34,9 @@ export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post'
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
export const syncKingdee = id =>
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
export const pushKingdee = ids =>
// 推送联带 A902 附件上传(登记附件 + 镜像发票文件下载回传),耗时较普通接口长,放宽到 120s
request({ url: `${baseUrl}/push-kingdee`, method: 'post', data: ids, timeout: 120000 });
export const approvalStatusOptions = [
{ label: '草稿', value: 'draft' },
@@ -36,8 +46,9 @@ export const approvalStatusOptions = [
{ label: '已作废', value: 'voided' },
];
export const kingdeeStatusOptions = [
{ label: '未同步', value: 'unsynced' },
{ label: '已同步', value: 'synced' },
{ label: '同步失败', value: 'failed' },
export const kingdeePushStatusOptions = [
{ label: '未推送', value: 'unsynced' },
{ label: '已保存收票单', value: 'pushed' },
{ label: '已指定应付', value: 'assigned' },
{ label: '推送失败', value: 'failed' },
];
+11 -3
View File
@@ -16,15 +16,20 @@ export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post'
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
// 同步金蝶涉及多次金蝶远程接口调用(A103-2 预检 + A103-1/A103-3 推送),单笔可达 20s+,覆盖全局 10s 超时
export const syncKingdee = id =>
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id }, timeout: 120000 });
// 批量同步逐单单据级事务串行推送,耗时随单量线性增长
export const syncKingdeeBatch = ids =>
request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', data: ids });
request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', data: ids, timeout: 300000 });
// 回写任务按 paymentNo 分批远程查询 A103-2/A103-4,同样可能超过全局超时
export const syncKingdeeResult = () =>
request({ url: `${baseUrl}/sync-kingdee-result`, method: 'post', timeout: 300000 });
export const paymentTypeOptions = [
{ label: '项目预付', value: 'project_advance' },
{ label: '进度预付', value: 'progress_advance' },
{ label: '尾款付款', value: 'settlement_payment' },
{ label: '结算付款', value: 'settlement_payment' },
];
export const approvalStatusOptions = [
{ label: '草稿', value: 'draft' },
@@ -36,5 +41,8 @@ export const approvalStatusOptions = [
export const kingdeeStatusOptions = [
{ label: '未生成', value: 'unsynced' },
{ label: '已生成', value: 'synced' },
{ label: '付款中', value: 'paying' },
{ label: '已付款', value: 'paid' },
{ label: '已关闭', value: 'closed' },
{ label: '生成失败', value: 'failed' },
];
+4 -1
View File
@@ -30,8 +30,9 @@ export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post'
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
// 同步金蝶涉及多次金蝶远程接口调用(A102-4 预检 + A102-2 保存),可达 20s+,覆盖全局 10s 超时
export const syncKingdee = id =>
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id }, timeout: 120000 });
export const getDetailFees = detailId =>
request({ url: `${baseUrl}/detail-fees`, method: 'get', params: { detailId } });
export const getReceiptClaims = formalSettlementId =>
@@ -48,3 +49,5 @@ export const applyPayments = data =>
request({ url: `${baseUrl}/apply-payments`, method: 'post', data });
export const claimInvoices = data =>
request({ url: `${baseUrl}/claim-invoices`, method: 'post', data });
export const queryInvoicePool = params =>
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params });
-59
View File
@@ -1,59 +0,0 @@
import request from '@/axios';
/**
* 调用 MK processSubmit 提交审核流
* @param {Object} data
* @param {string} data.templateCode 模板编码
* @param {string} data.submitIdentity 提交人(手机号)
* @param {string} data.loginName 登录账号(手机号)
* @param {string} data.bizId 业务主键(后端生成/复用 formInstanceId)
* @param {string} [data.subject] 流程标题
* @param {string} [data.bizType] 业务类型
*/
export const processSubmit = data => {
return request({
url: '/blade-system/businessProcess/processSubmit',
method: 'post',
data,
});
};
/**
* 调用 MK processDelete 删除审核流(驳回后重新提交前使用)
* @param {Object} data
* @param {string} data.bizId 业务主键
*/
export const processDelete = data => {
return request({
url: '/blade-system/businessProcess/processDelete',
method: 'post',
data,
});
};
/**
* 查询 MK 流程实例是否仍存在(有数据则无需删除/重新提交)
* @param {Object} params
* @param {string} params.bizId 业务主键
*/
export const hasMkProcessInstance = params => {
return request({
url: '/blade-system/businessProcess/hasMkProcessInstance',
method: 'get',
params,
});
};
/**
* 获取 MK 审批页跳转链接(后端按 thirdParty.mk 配置拼接,含 SSO)
* @param {Object} params
* @param {string} [params.bizId] 业务 id(如客商 id)
* @param {string} [params.processInstanceId] 流程实例 id
*/
export const getMKApprovalUrl = params => {
return request({
url: '/blade-system/businessProcess/getMKApprovalUrl',
method: 'get',
params,
});
};
-52
View File
@@ -41,58 +41,6 @@ export const add = row => {
});
};
export const syncIamOrganizations = () => {
return request({
url: '/blade-system/dept/sync-iam-organizations',
method: 'post',
});
};
export const clearNonTopDept = signal => {
return request({
url: '/blade-system/dept/clear-non-top',
method: 'post',
timeout: 60000,
signal,
});
};
export const syncOaCompany = (current = 1, size = 20, signal, incremental = false, modifiedSince) => {
return request({
url: '/blade-system/dept/sync-oa-company',
method: 'post',
params: {
current,
size,
incremental: !!incremental,
modifiedSince: modifiedSince || undefined,
},
timeout: 60000,
signal,
});
};
export const syncOaDepartment = (
current = 1,
size = 20,
signal,
incremental = false,
modifiedSince
) => {
return request({
url: '/blade-system/dept/sync-oa-department',
method: 'post',
params: {
current,
size,
incremental: !!incremental,
modifiedSince: modifiedSince || undefined,
},
timeout: 60000,
signal,
});
};
export const update = row => {
return request({
url: '/blade-system/dept/submit',
-62
View File
@@ -1,62 +0,0 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/mp-user/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-system/mp-user/detail',
method: 'get',
params: {
id,
},
});
};
export const submit = row => {
return request({
url: '/blade-system/mp-user/submit',
method: 'post',
data: row,
});
};
export const remove = ids => {
return request({
url: '/blade-system/mp-user/remove',
method: 'post',
params: {
ids,
},
});
};
export const changeStatus = (id, status) => {
return request({
url: '/blade-system/mp-user/change-status',
method: 'post',
params: {
id,
status,
},
});
};
export const resetPassword = userIds => {
return request({
url: '/blade-system/mp-user/reset-password',
method: 'post',
params: {
userIds,
},
});
};
-26
View File
@@ -31,21 +31,6 @@ export const add = row => {
});
};
export const syncIamAccounts = (current = 1, size = 50, signal, incremental = false, modifiedSince) => {
return request({
url: '/blade-system/user/sync-iam-accounts',
method: 'post',
params: {
current,
size,
incremental: !!incremental,
modifiedSince: modifiedSince || undefined,
},
timeout: 60000,
signal,
});
};
export const update = row => {
return request({
url: '/blade-system/user/update',
@@ -54,17 +39,6 @@ export const update = row => {
});
};
export const changeStatus = (id, status) => {
return request({
url: '/blade-system/user/change-status',
method: 'post',
params: {
id,
status,
},
});
};
export const updatePlatform = (userId, userType, userExt) => {
return request({
url: '/blade-system/user/update-platform',
-2
View File
@@ -9,8 +9,6 @@ export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get'
export const follow = data => request({ url: `${baseUrl}/follow`, method: 'post', data });
export const update = data => request({ url: `${baseUrl}/update`, method: 'post', data });
export const complete = data => request({ url: `${baseUrl}/complete`, method: 'post', data });
export const batchComplete = ids =>
@@ -1,49 +0,0 @@
import request from '@/axios';
/**
* 认证记录列表:按审核状态分页,按提交时间倒序
* 审核对象是**一条认证记录**,不是一辆车或一名司机,故必须带 bizType 区分
* status 为审核状态(0审核中 1已通过 2已驳回 3已作废);不传时后端按 0 处理
*/
export const getPendingPage = (bizType, current, size, params) => {
return request({
url: '/blade-transport/certification-audit/pending-page',
method: 'get',
params: {
...params,
bizType,
current,
size,
},
});
};
/**
* 审核详情:同时返回本次待审快照与该对象当前生效快照,供前端做字段级对比
* 从未认证通过时 effectiveSnapshotJson 为空串
*/
export const getAuditDetail = id => {
return request({
url: '/blade-transport/certification-audit/detail',
method: 'get',
params: {
id,
},
});
};
/**
* 审核一条认证记录
* 通过时不传驳回原因;驳回时必须传,且原因不超过 200 字
*/
export const auditCertification = (id, pass, rejectReason) => {
return request({
url: '/blade-transport/certification-audit/audit',
method: 'post',
params: {
id,
pass,
...(rejectReason ? { rejectReason } : {}),
},
});
};
@@ -51,6 +51,14 @@ export const changeStatus = (id, status) => {
});
};
export const auditCertification = (id, certificationStatus, rejectReason) => {
return request({
url: '/blade-transport/transport-vehicle/audit-certification',
method: 'post',
params: { id, certificationStatus, rejectReason },
});
};
export const getExpiryStat = params => {
return request({
url: '/blade-transport/transport-vehicle/expiry-stat',
+20 -39
View File
@@ -22,19 +22,6 @@ 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',
@@ -47,32 +34,6 @@ 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',
@@ -152,6 +113,26 @@ export const getScoreTemplate = quantificationId => {
});
};
export const queryKingdeeCustomer = creditCode => {
return request({
url: '/blade-transport/bfi/customer/query-by-credit-code',
method: 'get',
params: {
creditCode,
},
});
};
export const syncKingdee = id => {
return request({
url: '/blade-transport/customer-archive/sync-kingdee',
method: 'post',
params: {
id,
},
});
};
export const recognizeBusinessLicenseOcr = imageUrl => {
return request({
url: '/blade-transport/baidu-ocr/recognize-url',
+20 -63
View File
@@ -70,44 +70,11 @@ const redirectToLogin = () => {
store.dispatch('FedLogOut').then(() => router.push({ path: '/login' }));
};
/**
* 统一弹出错误提示并返回带标记的 Error。
* `_notified` 标记用于让业务层(批量导入等)识别「拦截器已经提示过」,
* 避免同一条错误被弹两次。
*/
const notifiedError = message => {
ElMessage({
message: message,
type: 'error',
});
const error = new Error(message);
error._notified = true;
return error;
};
axios.defaults.timeout = 10000;
//返回其他状态码
axios.defaults.validateStatus = function (status) {
return status >= 200 && status <= 500; // 默认的
};
/**
* 判断响应是否为 JSON。
* 用于 blob 请求(导入/导出)的兜底:后端在业务校验失败时返回的是 JSON
* (Content-Type: application/json),而不是文件流,需要解析后才能取到 msg。
*/
const isJsonContentType = res => {
const headers = res.headers || {};
const contentType = String(headers['content-type'] || res.data?.type || '').toLowerCase();
return contentType.includes('application/json') || contentType.includes('text/json');
};
/**
* 判断响应体是否已是解析好的对象(无需再解析)。
* 注意 Blob 也是 object,必须排除,否则会把待解析的文件流误判为已完成。
*/
const isPlainObject = value =>
value !== null && typeof value === 'object' && !(value instanceof Blob);
//跨域请求,允许保存cookie
axios.defaults.withCredentials = true;
// NProgress Configuration
@@ -173,7 +140,7 @@ axios.interceptors.request.use(
//http response拦截
axios.interceptors.response.use(
async res => {
res => {
NProgress.done();
//获取配置信息
const config = res.config;
@@ -182,29 +149,6 @@ axios.interceptors.response.use(
if (cryptoData) {
res.data = JSON.parse(crypto.decryptAES(res.data, crypto.aesKey));
}
// blob 请求(文件下载、导入)的响应体是 Blob,直接读 res.data.code / res.data.msg
// 拿不到任何字段:状态码会退回 res.status、提示文案会落到「系统错误」兜底,
// 用户就会以为系统崩了——而后端其实返回了明确的业务提示(如「未读取到数据行…」)。
// 这里按 Content-Type 判断:JSON 才解析回对象;真·文件流(Excel 等)保持原样不动。
// 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。
if (isJsonContentType(res) && !isPlainObject(res.data)) {
try {
let text = '';
if (typeof res.data?.text === 'function') {
text = await res.data.text();
} else if (typeof res.data === 'string') {
text = res.data;
} else if (typeof res.data?.arrayBuffer === 'function') {
text = new TextDecoder().decode(await res.data.arrayBuffer());
}
const parsed = typeof text === 'string' && text ? JSON.parse(text) : null;
if (parsed && typeof parsed === 'object') {
res.data = parsed;
}
} catch (ignored) {
// 声明是 JSON 却解析失败:保持原样,交给下游按文件处理
}
}
//获取状态信息
const status = res.data.error_code || res.data.code || res.status;
const statusWhiteList = website.statusWhiteList || [];
@@ -260,21 +204,34 @@ axios.interceptors.response.use(
// OAuth2 业务错误码提示
if (status > 2000 && !validatenull(res.data.error_description)) {
return Promise.reject(notifiedError(message));
ElMessage({
message: message,
type: 'error',
});
return Promise.reject(new Error(message));
}
// 其余非 200 响应统一提示
if (status !== 200) {
return Promise.reject(notifiedError(message));
ElMessage({
message: message,
type: 'error',
});
return Promise.reject(new Error(message));
}
return res;
},
error => {
NProgress.done();
// 保留原始 axios 错误对象:下游(如导入失败明细解析)需要 error.response
// 读取响应体。若包装成 new Error(error),response 会丢失、message 退化成
// "[object Object]",用户只能看到无意义的通用提示。
return Promise.reject(error);
// 网络级失败(HTTP 状态 >500、网关错误、超时、断网等 validateStatus 放行之外的响应)
// 原为静默 reject,接口报错页面无任何感知;统一补错误提示,取消请求与刷新令牌请求除外
if (!axios.isCancel(error) && !isRefreshTokenRequest(error?.config)) {
ElMessage({
message: error?.response?.data?.msg || '请求失败,请稍后重试',
type: 'error',
});
}
return Promise.reject(new Error(error));
}
);
+32 -155
View File
@@ -10,17 +10,17 @@
<el-button type="primary" :loading="searching" @click="searchKeyword">搜索</el-button>
</div>
<div class="address-map-picker__content">
<map-search-results
v-if="searchResults.length"
:results="searchResults"
@select="selectSearchResult"
/>
<div ref="map" class="address-map-picker__map"></div>
<map-search-results :results="searchResults" @select="selectSearchResult" />
</div>
<div class="address-map-picker__info">{{ status }}</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button
type="primary"
:disabled="!hasCoordinateValue(selected.longitude) || !hasCoordinateValue(selected.latitude) || resolving"
@click="confirm"
>
<el-button type="primary" :disabled="!selected.longitude || resolving" @click="confirm">
确定
</el-button>
</template>
@@ -28,10 +28,8 @@
</template>
<script>
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
const AMAP_KEY = '13dcb15bc29bfe9c432d17ecd808b812';
const AMAP_SECURITY_CODE = 'f9d036ef29e376e15d309e0de5c0f7ab';
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
let amapLoader;
export default {
@@ -44,14 +42,6 @@ export default {
type: String,
default: '',
},
longitude: {
type: [String, Number],
default: '',
},
latitude: {
type: [String, Number],
default: '',
},
},
emits: ['update:modelValue', 'confirm'],
data() {
@@ -76,11 +66,9 @@ export default {
this.visible = value;
if (value) {
this.keyword = this.address || '';
this.selected = {};
this.status = '可搜索地址或点击地图选点';
this.searchResults = [];
this.selected = this.buildInitialSelection();
this.status = this.selected.longitude
? this.selected.address || '已选点,可确认回填'
: '可搜索地址或点击地图选点';
}
},
},
@@ -94,32 +82,6 @@ export default {
}
},
methods: {
hasCoordinateValue(value) {
return value !== '' && value !== null && value !== undefined;
},
buildInitialSelection() {
// Number('') === 0,空字符串不能当有效坐标,否则会落到 [0,0](非洲附近)
if (!this.hasCoordinateValue(this.longitude) || !this.hasCoordinateValue(this.latitude)) {
return {};
}
const longitude = Number(this.longitude);
const latitude = Number(this.latitude);
if (
!Number.isFinite(longitude) ||
!Number.isFinite(latitude) ||
longitude < -180 ||
longitude > 180 ||
latitude < -90 ||
latitude > 90
) {
return {};
}
return {
longitude,
latitude,
address: this.address || '',
};
},
loadAmap() {
if (window.AMap && window.AMap.Map) {
return Promise.resolve();
@@ -148,29 +110,15 @@ export default {
return new Promise(resolve => {
this.$nextTick(() => {
if (!this.$refs.map) return resolve(null);
const initial = this.buildInitialSelection();
const hasPoint =
Number.isFinite(initial.longitude) && Number.isFinite(initial.latitude);
const center = hasPoint
? [initial.longitude, initial.latitude]
: [116.40769, 39.89945];
const zoom = hasPoint ? 14 : 11;
if (!this.amap) {
this.amap = new window.AMap.Map(this.$refs.map, {
center,
zoom,
center: [116.40769, 39.89945],
zoom: 11,
});
this.amap.on('click', event => this.pickPoint(event.lnglat));
} else {
this.amap.resize();
this.clearMarker();
this.amap.setZoomAndCenter(zoom, center);
}
if (hasPoint) {
const point = new window.AMap.LngLat(initial.longitude, initial.latitude);
this.renderMarker(point);
this.selected = { ...initial };
this.status = initial.address || '已选点,可确认回填';
}
resolve(this.amap);
});
@@ -228,17 +176,23 @@ export default {
this.searchResults = [];
this.searching = true;
this.initMap()
.then(map => (map ? searchAmapPlaceText(keyword) : null))
.then(pois => {
if (!pois) return;
this.searchResults = normalizeMapSearchResults(pois, keyword);
const first = this.searchResults[0];
if (!first?.location) {
.then(map => (map ? this.runGeocode('location', keyword) : null))
.then(result => {
if (!result) return;
const geocodes = result.geocodes || (result.location ? [result] : []);
this.searchResults = geocodes.map((item, index) => ({
id: item.id || index,
name: item.formattedAddress || item.address || keyword,
address: item.formattedAddress || item.address || keyword,
location: item.location,
}));
const point = geocodes[0]?.location || result.location;
if (!point) {
throw new Error('未找到匹配地址');
}
return this.$nextTick().then(() => {
this.amap?.resize();
this.pickPoint(first.location, first.address || first.name || keyword);
this.pickPoint(point, geocodes[0]?.formattedAddress || keyword);
});
})
.catch(() => {
@@ -255,10 +209,8 @@ export default {
}
},
pickPoint(point, fallbackAddress = '') {
const longitude =
typeof point.getLng === 'function' ? point.getLng() : point.lng ?? point.longitude;
const latitude =
typeof point.getLat === 'function' ? point.getLat() : point.lat ?? point.latitude;
const longitude = typeof point.getLng === 'function' ? point.getLng() : point.lng;
const latitude = typeof point.getLat === 'function' ? point.getLat() : point.lat;
if (!Number.isFinite(Number(longitude)) || !Number.isFinite(Number(latitude))) {
this.$message.warning('选点坐标无效');
return;
@@ -275,12 +227,7 @@ export default {
this.runGeocode('address', lnglat)
.then(result => {
const detailAddress = result.regeocode?.formattedAddress || fallbackAddress;
const region = this.resolveRegionFromGeocode(result);
this.selected = {
...this.selected,
address: detailAddress,
...region,
};
this.selected = { ...this.selected, address: detailAddress };
this.keyword = detailAddress || this.keyword;
this.status = detailAddress || '已选点,可确认回填';
})
@@ -294,26 +241,6 @@ export default {
this.resolving = false;
});
},
resolveRegionFromGeocode(result = {}) {
const component = result.regeocode?.addressComponent || {};
const city = Array.isArray(component.city) ? '' : String(component.city || '').trim();
const province = String(component.province || '').trim();
const district = String(component.district || '').trim();
const country = String(component.country || '').trim();
const adcode = String(component.adcode || '').trim();
return {
country,
province,
city: city || province,
district,
adcode,
regionCode: adcode,
provinceName: province,
cityName: city || province,
districtName: district,
countryName: country,
};
},
renderMarker(point) {
this.clearMarker();
this.marker = new window.AMap.Marker({ position: point });
@@ -326,53 +253,12 @@ export default {
this.marker = null;
}
},
syncSelectedFromMarker() {
// 确认时以当前标记点坐标为准,避免换地址后仍回填上一次经纬度
if (!this.marker || typeof this.marker.getPosition !== 'function') {
return;
}
const point = this.marker.getPosition();
if (!point) {
return;
}
const longitude =
typeof point.getLng === 'function' ? point.getLng() : point.lng ?? point.longitude;
const latitude =
typeof point.getLat === 'function' ? point.getLat() : point.lat ?? point.latitude;
if (!Number.isFinite(Number(longitude)) || !Number.isFinite(Number(latitude))) {
return;
}
this.selected = {
...this.selected,
longitude: Number(longitude),
latitude: Number(latitude),
};
},
confirm() {
this.syncSelectedFromMarker();
if (!this.hasCoordinateValue(this.selected.longitude) || !this.hasCoordinateValue(this.selected.latitude)) {
this.$message.warning('请先搜索或点击地图完成选点');
return;
}
if (!this.selected.address) {
this.$message.warning('请等待地址解析完成后再确认');
return;
}
this.$emit('confirm', {
address: this.selected.address,
longitude: Number(this.selected.longitude),
latitude: Number(this.selected.latitude),
country: this.selected.country || this.selected.countryName || '',
province: this.selected.province || this.selected.provinceName || '',
city: this.selected.city || this.selected.cityName || '',
district: this.selected.district || this.selected.districtName || '',
adcode: this.selected.adcode || this.selected.regionCode || '',
regionCode: this.selected.regionCode || this.selected.adcode || '',
countryName: this.selected.countryName || this.selected.country || '',
provinceName: this.selected.provinceName || this.selected.province || '',
cityName: this.selected.cityName || this.selected.city || '',
districtName: this.selected.districtName || this.selected.district || '',
});
this.$emit('confirm', this.selected.address);
this.visible = false;
},
},
@@ -392,6 +278,7 @@ export default {
}
&__map {
flex: 1 1 auto;
width: 100%;
min-width: 0;
height: 420px;
@@ -399,19 +286,9 @@ export default {
}
&__content {
position: relative;
display: block;
display: flex;
gap: 12px;
min-width: 0;
overflow: visible;
.address-map-picker__map {
position: relative;
z-index: 1;
}
:deep(.map-search-results) {
z-index: 2000;
}
}
&__info {
@@ -1,108 +0,0 @@
<template>
<el-dialog
:model-value="modelValue"
title="变更记录详情"
append-to-body
destroy-on-close
align-center
width="1100px"
class="change-record-detail-dialog"
@update:model-value="$emit('update:modelValue', $event)"
@open="resetPage"
>
<el-table
:data="pageRows"
border
max-height="60vh"
:show-overflow-tooltip="false"
>
<el-table-column prop="field" label="变更字段" min-width="180" />
<el-table-column
prop="before"
label="变更前"
min-width="360"
class-name="change-record-detail-value"
/>
<el-table-column
prop="after"
label="变更后"
min-width="500"
class-name="change-record-detail-value"
/>
</el-table>
<el-empty v-if="!total" description="暂无变更内容" :image-size="60" />
<div class="change-record-detail-dialog__pager">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
background
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
/>
</div>
<template #footer>
<el-button type="primary" @click="$emit('update:modelValue', false)">关闭</el-button>
</template>
</el-dialog>
</template>
<script>
export default {
name: 'ChangeRecordDetailDialog',
props: {
modelValue: {
type: Boolean,
default: false,
},
rows: {
type: Array,
default: () => [],
},
},
emits: ['update:modelValue'],
data() {
return {
currentPage: 1,
pageSize: 10,
};
},
computed: {
total() {
return this.rows.length;
},
pageRows() {
const start = (this.currentPage - 1) * this.pageSize;
return this.rows.slice(start, start + this.pageSize);
},
},
watch: {
rows() {
const maxPage = Math.max(1, Math.ceil(this.total / this.pageSize) || 1);
if (this.currentPage > maxPage) this.currentPage = 1;
},
},
methods: {
resetPage() {
this.currentPage = 1;
this.pageSize = 10;
},
},
};
</script>
<style lang="scss" scoped>
.change-record-detail-dialog__pager {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
:deep(.change-record-detail-value .cell) {
white-space: pre-wrap;
word-break: break-word;
overflow: visible;
text-overflow: clip;
line-height: 1.6;
}
</style>
+1 -1
View File
@@ -576,7 +576,7 @@ export default {
}
.mode-content :deep(.el-form-item) {
margin-bottom: 24px;
margin-bottom: 14px;
}
.mode-content :deep(.el-form-item:last-child) {
@@ -0,0 +1,380 @@
<template>
<el-dialog v-model="visible" :title="title" width="92%" append-to-body destroy-on-close @open="handleOpen">
<el-form :model="query" inline label-width="130px" @submit.prevent>
<el-form-item label="发票号码">
<el-input v-model="query.invoiceNo" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="开票日期">
<el-date-picker
v-model="query.invoiceDateRange"
type="daterange"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item label="销方(开票方税号)">
<el-input
v-model="query.salerTaxNo"
clearable
style="width: 180px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="购方(收票方税号)">
<el-input
v-model="query.buyerTaxNo"
clearable
style="width: 180px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="发票类型">
<el-select
v-model="query.invoiceType"
clearable
filterable
placeholder="不限"
style="width: 230px"
>
<el-option
v-for="option in invoiceTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button :disabled="loading" @click="resetQuery">重置</el-button>
<el-button type="primary" :disabled="loading" @click="handleQuery">查询</el-button>
</el-form-item>
</el-form>
<el-table
ref="poolTable"
v-loading="loading"
:data="rows"
:row-key="rowKey"
border
height="520"
:highlight-current-row="!multiple"
@selection-change="selection => (selected = selection)"
@row-dblclick="handleRowDblclick"
>
<el-table-column v-if="multiple" type="selection" width="52" fixed="left" align="center" reserve-selection />
<el-table-column v-else width="52" align="center" fixed="left">
<template #default="{ row }">
<el-radio
:model-value="current && current.invoiceNo"
:value="row.invoiceNo"
@change="current = row"
>
<span></span>
</el-radio>
</template>
</el-table-column>
<el-table-column prop="invoiceNo" label="发票号码" min-width="180" />
<el-table-column prop="invoiceDate" label="开票日期" min-width="120" />
<el-table-column prop="invoiceType" label="发票类型" min-width="140" />
<el-table-column prop="issuerName" label="开票单位" min-width="180" show-overflow-tooltip />
<el-table-column prop="receiverName" label="受票单位" min-width="180" show-overflow-tooltip />
<el-table-column label="金额(含税)" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
</el-table-column>
<el-table-column label="税率" min-width="90" align="center">
<template #default="{ row }">{{ formatRate(row.taxRate) }}</template>
</el-table-column>
<el-table-column label="税额" min-width="120" align="right">
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
</el-table-column>
<el-table-column label="剩余可用金额" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.availableTaxAmount) }}</template>
</el-table-column>
<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="210" show-overflow-tooltip />
<el-table-column label="认领状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.claimed" type="danger" size="small">已认领</el-tag>
<el-tag v-else type="info" size="small">未认领</el-tag>
</template>
</el-table-column>
<el-table-column label="登记状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.registered" type="warning" size="small">已登记</el-tag>
<el-tag v-else type="info" size="small">未登记</el-tag>
</template>
</el-table-column>
<el-table-column label="附件" width="120" fixed="right" align="center">
<template #default="{ row }">
<el-link
type="primary"
:underline="false"
:disabled="attachmentBusy === row.invoiceNo"
@click.stop="viewAttachment(row)"
>查看</el-link
>
<el-link
type="primary"
:underline="false"
:disabled="attachmentBusy === row.invoiceNo"
@click.stop="downloadAttachment(row)"
>下载</el-link
>
</template>
</el-table-column>
<el-table-column v-if="!multiple" label="操作" width="90" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click.stop="confirmRows([row])">选择</el-link>
</template>
</el-table-column>
</el-table>
<div class="kingdee-pool-dialog__pager">
<span class="kingdee-pool-dialog__pager-info">第 {{ page.current }} 页</span>
<el-button :disabled="page.current <= 1 || loading" @click="prevPage">上一页</el-button>
<el-button :disabled="!page.hasMore || loading" @click="nextPage">下一页</el-button>
<el-select v-model="page.size" style="width: 110px" @change="handleSizeChange">
<el-option v-for="n in [10, 20, 50, 100]" :key="n" :label="`${n}条/页`" :value="n" />
</el-select>
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleConfirm">{{ confirmText }}</el-button>
</template>
</el-dialog>
</template>
<script>
import {
downloadKingdeeInvoiceFile,
isKingdeePathAttachment,
viewKingdeeInvoiceFile,
} from '@/utils/kingdee-attachment';
export default {
name: 'KingdeeInvoicePoolDialog',
props: {
modelValue: { type: Boolean, required: true },
title: { type: String, default: '查询金蝶票据池' },
confirmText: { type: String, default: '确定' },
multiple: { type: Boolean, default: false },
initialInvoiceNo: { type: String, default: '' },
queryApi: { type: Function, required: true },
},
emits: ['update:modelValue', 'confirm'],
data() {
return {
loading: false,
// 与后端 BfiConstant.INVOICE_TYPE_NAMES 对齐:value 为发票类型编码,label 为中文名
// 选票场景只开放数电票(普票/专票)
invoiceTypeOptions: [
{ value: '26', label: '数电发票(普通发票)' },
{ value: '27', label: '数电发票(增值税专用发票)' },
],
rows: [],
selected: [],
current: null,
page: { current: 1, size: 10, hasMore: false },
// 续扫游标(服务端过滤补页后翻页须按游标对齐,防漏行/重行)
nextCursor: null,
// 下一页请求待回传的游标:nextPage 时暂存,load 成功或失败后清空
pendingCursor: null,
// 已浏览页缓存:上一页直接回显不再请求(0 远程调用)
pageCache: {},
// 正在查看/下载附件的发票号(行级 loading 守卫)
attachmentBusy: null,
query: this.emptyQuery(),
};
},
computed: {
visible: {
get() {
return this.modelValue;
},
set(value) {
this.$emit('update:modelValue', value);
},
},
},
methods: {
emptyQuery() {
return {
invoiceNo: '',
// 默认查近一个月,随请求传后端并在页面回显
invoiceDateRange: [this.$dayjs().subtract(1, 'month').format('YYYY-MM-DD'), this.$dayjs().format('YYYY-MM-DD')],
salerTaxNo: '',
buyerTaxNo: '',
invoiceType: '',
};
},
// 远程实时查询的票未入本地镜像,id 恒空,用发票号码兜底作为行主键
rowKey(row) {
return row.invoiceNo || row.id;
},
// 票池行附件字段为复数 attachmentsJson,行内首项即发票文件({source,path,name})
rowAttachment(row = {}) {
const value = row.attachmentsJson;
if (!value) return null;
let parsed = value;
if (typeof value === 'string') {
try {
parsed = JSON.parse(value);
} catch {
return null;
}
}
return Array.isArray(parsed) ? parsed[0] || null : parsed || null;
},
attachmentName(row) {
const attachment = this.rowAttachment(row);
return attachment?.name || `${row.invoiceNo || '发票'}.pdf`;
},
async viewAttachment(row) {
const attachment = this.rowAttachment(row);
if (!isKingdeePathAttachment(attachment)) {
this.$message.warning('当前发票暂无可查看附件');
return;
}
this.attachmentBusy = row.invoiceNo;
try {
await viewKingdeeInvoiceFile(row.invoiceNo, this.attachmentName(row));
} finally {
this.attachmentBusy = null;
}
},
async downloadAttachment(row) {
const attachment = this.rowAttachment(row);
if (!isKingdeePathAttachment(attachment)) {
this.$message.warning('当前发票暂无可下载附件');
return;
}
this.attachmentBusy = row.invoiceNo;
try {
await downloadKingdeeInvoiceFile(row.invoiceNo, this.attachmentName(row));
} finally {
this.attachmentBusy = null;
}
},
handleOpen() {
this.query = this.emptyQuery();
this.query.invoiceNo = this.initialInvoiceNo || '';
this.current = null;
this.selected = [];
this.$refs.poolTable?.clearSelection();
this.page.current = 1;
this.resetScanState();
this.load();
},
handleQuery() {
this.page.current = 1;
this.resetScanState();
this.load();
},
handleSizeChange() {
this.page.current = 1;
this.resetScanState();
this.load();
},
prevPage() {
// 已浏览页直接回显缓存,不再请求
const cached = this.pageCache[this.page.current - 1];
if (cached) {
this.page.current -= 1;
this.rows = cached.rows;
this.page.hasMore = cached.hasMore;
this.nextCursor = cached.nextCursor;
return;
}
this.page.current -= 1;
this.load();
},
nextPage() {
this.pendingCursor = this.nextCursor;
this.page.current += 1;
this.load();
},
resetQuery() {
this.query = this.emptyQuery();
this.handleQuery();
},
// 新查询/换页容量/重新打开时清空游标与已浏览页缓存,重新锚定扫描
resetScanState() {
this.nextCursor = null;
this.pendingCursor = null;
this.pageCache = {};
},
unwrap(response) {
const body = response?.data || response || {};
return body?.data || body;
},
async load() {
this.loading = true;
// 请求前快照页码:prev/next 会先改写 current,远程失败时回滚避免停留在错页
const lastRequested = this.page.current;
const cursor = this.pendingCursor;
this.pendingCursor = null;
try {
const range = this.query.invoiceDateRange || [];
const data = this.unwrap(
await this.queryApi({
current: this.page.current,
size: this.page.size,
cursor: cursor || undefined,
invoiceNo: this.query.invoiceNo || undefined,
invoiceDateStart: range[0] || undefined,
invoiceDateEnd: range[1] || undefined,
salerTaxNo: this.query.salerTaxNo || undefined,
buyerTaxNo: this.query.buyerTaxNo || undefined,
invoiceType: this.query.invoiceType || undefined,
})
);
this.rows = data.records || [];
this.page.hasMore = !!data.hasMore;
this.nextCursor = data.nextCursor || null;
this.pageCache[this.page.current] = { rows: this.rows, hasMore: this.page.hasMore, nextCursor: this.nextCursor };
} catch (error) {
if (this.page.current !== lastRequested) {
this.page.current = lastRequested;
}
throw error;
} finally {
this.loading = false;
}
},
handleRowDblclick(row) {
if (!this.multiple) {
this.confirmRows([row]);
}
},
handleConfirm() {
const picked = this.multiple ? this.selected : this.current ? [this.current] : [];
this.confirmRows(picked);
},
confirmRows(rows) {
if (!rows.length) {
this.$message.warning(this.multiple ? '请至少选择一条发票' : '请选择一张发票');
return;
}
this.$emit('confirm', rows);
this.visible = false;
},
formatMoney(value) {
const number = Number(value);
return Number.isFinite(number) ? number.toFixed(2) : '-';
},
formatRate(value) {
const number = Number(value);
return Number.isFinite(number) ? `${number}%` : '-';
},
},
};
</script>
<style lang="scss" scoped>
.kingdee-pool-dialog__pager {
display: flex;
align-items: center;
gap: 12px;
justify-content: flex-end;
margin-top: 12px;
}
</style>
+22 -181
View File
@@ -1,55 +1,20 @@
<template>
<div v-if="results.length" class="map-search-results">
<div class="map-search-results__list">
<div
v-for="(item, index) in pageResults"
:key="item.id || `${currentPage}-${index}`"
class="map-search-results__item"
:class="{ 'is-active': isActive(item, index) }"
@click="handleSelect(item, index)"
>
<img
v-if="item.photo || item.image"
class="map-search-results__thumb"
:src="item.photo || item.image"
alt=""
/>
<div class="map-search-results__body">
<div class="map-search-results__name">
{{ item.name || item.address || '未命名地址' }}
</div>
<div class="map-search-results__address">
地址:{{ item.address || item.name || '-' }}
</div>
</div>
</div>
</div>
<div v-if="totalPages > 1" class="map-search-results__pager">
<button
v-for="page in visiblePages"
:key="page"
type="button"
class="map-search-results__page"
:class="{ 'is-active': page === currentPage }"
@click="currentPage = page"
>
{{ page }}
</button>
<button
type="button"
class="map-search-results__page map-search-results__page--next"
:disabled="currentPage >= totalPages"
@click="goNext"
>
下一页
</button>
<div class="map-search-results__title">搜索结果</div>
<el-empty v-if="!results.length" description="暂无搜索结果" :image-size="60" />
<div
v-for="(item, index) in results"
:key="item.id || index"
class="map-search-results__item"
@click="$emit('select', item)"
>
<div class="map-search-results__name">{{ item.name || item.address || '未命名地址' }}</div>
<div class="map-search-results__address">{{ item.address || item.name || '-' }}</div>
</div>
</div>
</template>
<script>
const PAGE_SIZE = 5;
export default {
props: {
results: {
@@ -58,167 +23,43 @@ export default {
},
},
emits: ['select'],
data() {
return {
currentPage: 1,
activeKey: '',
};
},
computed: {
totalPages() {
return Math.max(1, Math.ceil((this.results || []).length / PAGE_SIZE));
},
pageResults() {
const start = (this.currentPage - 1) * PAGE_SIZE;
return (this.results || []).slice(start, start + PAGE_SIZE);
},
visiblePages() {
const maxButtons = 3;
let start = Math.max(1, this.currentPage - 1);
let end = Math.min(this.totalPages, start + maxButtons - 1);
start = Math.max(1, end - maxButtons + 1);
const pages = [];
for (let page = start; page <= end; page += 1) {
pages.push(page);
}
return pages;
},
},
watch: {
results() {
this.currentPage = 1;
this.activeKey = '';
},
},
methods: {
resultKey(item, index) {
return item?.id ?? `${this.currentPage}-${index}`;
},
isActive(item, index) {
return this.activeKey === this.resultKey(item, index);
},
handleSelect(item, index) {
this.activeKey = this.resultKey(item, index);
this.$emit('select', item);
},
goNext() {
if (this.currentPage < this.totalPages) {
this.currentPage += 1;
}
},
},
};
</script>
<style lang="scss" scoped>
.map-search-results {
position: absolute;
top: 0;
left: 0;
z-index: 2000;
display: flex;
flex-direction: column;
width: 360px;
max-height: 360px;
overflow: hidden;
width: 260px;
height: 420px;
overflow-y: auto;
border: 1px solid #eff1f7;
background: #fff;
border: 1px solid #dcdfe6;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
pointer-events: auto;
flex: 0 0 260px;
&__list {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
&__title {
padding: 10px 12px;
border-bottom: 1px solid #eff1f7;
font-weight: 600;
}
&__item {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 10px 12px;
cursor: pointer;
border-bottom: 1px solid #ebeef5;
border-bottom: 1px solid #f5f5f5;
}
&__item:hover,
&__item.is-active {
background: #f5f5f5;
}
&__thumb {
flex: 0 0 48px;
width: 48px;
height: 48px;
object-fit: cover;
border: 1px solid #ebeef5;
background: #fafafa;
}
&__body {
flex: 1 1 auto;
min-width: 0;
&__item:hover {
background: #f5f9ff;
}
&__name {
overflow: hidden;
color: #303133;
font-size: 14px;
font-weight: 600;
line-height: 1.4;
white-space: nowrap;
text-overflow: ellipsis;
}
&__address {
margin-top: 4px;
color: #909399;
font-size: 12px;
line-height: 1.5;
word-break: break-all;
}
&__pager {
display: flex;
flex: 0 0 auto;
flex-wrap: wrap;
gap: 6px;
align-items: center;
padding: 8px 10px;
border-top: 1px solid #ebeef5;
background: #fff;
}
&__page {
min-width: 28px;
height: 28px;
padding: 0 8px;
color: #606266;
font-size: 13px;
line-height: 26px;
text-align: center;
background: #fff;
border: 1px solid #dcdfe6;
cursor: pointer;
&:hover:not(:disabled) {
color: #409eff;
border-color: #409eff;
}
&.is-active {
color: #409eff;
border-color: #409eff;
}
&:disabled {
color: #c0c4cc;
cursor: not-allowed;
}
&--next {
min-width: 56px;
}
}
}
</style>
-117
View File
@@ -1,117 +0,0 @@
<template>
<div class="avue-crud__right table-header-tools">
<el-button v-if="showRefresh" icon="el-icon-refresh" circle @click="$emit('refresh')" />
<el-button
v-if="showColumn"
icon="el-icon-operation"
circle
@click="columnSettingVisible = true"
/>
<el-button
v-if="showSearch"
icon="el-icon-search"
circle
@click="$emit('update:searchVisible', !searchVisible)"
/>
<el-button v-if="showGrid" icon="el-icon-grid" circle @click="toggleTableSize" />
<el-dialog
v-if="showColumn"
v-model="columnSettingVisible"
title="列设置"
width="360px"
append-to-body
destroy-on-close
>
<el-checkbox-group
class="table-header-tools__column-setting"
:model-value="visibleColumnKeys"
@update:model-value="$emit('update:visibleColumnKeys', $event)"
>
<el-checkbox v-for="item in columns" :key="item.prop" :label="item.prop">
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
<template #footer>
<el-button @click="columnSettingVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
const TABLE_SIZE_OPTIONS = ['default', 'large', 'small'];
export default {
name: 'TableHeaderTools',
props: {
columns: {
type: Array,
default: () => [],
},
visibleColumnKeys: {
type: Array,
default: () => [],
},
tableSize: {
type: String,
default: 'default',
},
searchVisible: {
type: Boolean,
default: true,
},
showRefresh: {
type: Boolean,
default: true,
},
showColumn: {
type: Boolean,
default: true,
},
showSearch: {
type: Boolean,
default: true,
},
showGrid: {
type: Boolean,
default: true,
},
},
emits: ['refresh', 'update:visibleColumnKeys', 'update:tableSize', 'update:searchVisible'],
data() {
return {
columnSettingVisible: false,
};
},
deactivated() {
// keep-alive 缓存页面时关闭列设置弹窗,避免 Teleport 到 body 的弹窗切页后残留
this.columnSettingVisible = false;
},
methods: {
toggleTableSize() {
const index = TABLE_SIZE_OPTIONS.indexOf(this.tableSize);
const next = TABLE_SIZE_OPTIONS[(index + 1) % TABLE_SIZE_OPTIONS.length];
this.$emit('update:tableSize', next);
},
},
};
</script>
<style scoped lang="scss">
.table-header-tools.avue-crud__right {
width: auto;
margin: 0 0 0 auto;
flex: none;
white-space: nowrap;
text-align: right;
}
.table-header-tools__column-setting {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 360px;
overflow: auto;
}
</style>
@@ -9,21 +9,18 @@
<el-table :data="rows" border @selection-change="selectedRows = $event">
<el-table-column v-if="!readonly" type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column
label="文件名"
min-width="240"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<el-table-column label="文件名" min-width="240" show-overflow-tooltip>
<template #default="{ row }">
<span
class="attachment-file-name"
class="vehicle-attachment-table__file-name"
role="button"
tabindex="0"
@click="previewAttachment(row)"
@keydown.enter="previewAttachment(row)"
@keydown.space.prevent="previewAttachment(row)"
>{{ attachmentName(row) }}</span>
>
{{ attachmentName(row) }}
</span>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220">
@@ -311,6 +308,16 @@ export default {
margin-bottom: 16px;
}
&__file-name {
color: #409eff;
cursor: pointer;
}
&__file-name:hover,
&__file-name:focus-visible {
text-decoration: underline;
}
&__upload {
display: flex;
justify-content: flex-start;
@@ -212,14 +212,7 @@ export default {
.filter(Boolean);
},
acceptText() {
return this.acceptedList
.map(item => {
const value = String(item).trim();
if (!value || value.includes('/') || value.startsWith('.')) return value;
return `.${value}`;
})
.filter(Boolean)
.join(',');
return this.acceptedList.join(',');
},
tipText() {
return this.tip || `${UNIFIED_ATTACHMENT_TIP_PREFIX}${this.maxSize}M`;
@@ -311,22 +304,12 @@ export default {
});
},
handleSuccess(response, file, files) {
const code = response?.code;
const codeInvalid = code !== undefined && code !== null && code !== '' && Number(code) !== 200;
if (!response || response.success === false || codeInvalid) {
if (!response || response.success === false || (response.code && response.code !== 200)) {
this.$message.error((response && response.msg) || '上传失败');
return;
}
if (file && !file.response) {
file.response = response;
}
const normalized = this.normalizeUploadFile(file, response);
if (!normalized.url) {
this.$message.error('上传成功但未返回文件地址');
return;
}
this.$emit('success', normalized);
this.emitUploadFiles(this.multiple ? files : [file], normalized);
this.emitUploadFiles(this.multiple ? files : [file]);
this.$emit('success', this.normalizeUploadFile(file));
this.$message.success('上传成功');
},
handleChange(file, files) {
@@ -340,89 +323,35 @@ export default {
this.emitUploadFiles(files);
return true;
},
emitUploadFiles(files, preferredFile) {
const list = (files || [])
.filter(file => {
if (file?.status === 'fail') return false;
return (
file?.status === 'success' ||
this.getFileUrl(file) ||
file?.response?.data ||
file?.response?.link ||
file?.response
);
})
.map(file => this.normalizeUploadFile(file))
emitUploadFiles(files) {
const list = files
.filter(file => file.status === 'success' || this.getFileUrl(file))
.map(this.normalizeUploadFile)
.filter(item => item.url);
if (preferredFile?.url) {
const exists = list.some(
item =>
(preferredFile.uid && item.uid && String(item.uid) === String(preferredFile.uid)) ||
item.url === preferredFile.url
);
if (!exists) list.push(preferredFile);
}
const current = this.parseValue(this.modelValue).map(item => ({
...item,
url: this.getFileUrl(item),
}));
const merged = [...current];
list.forEach(file => {
const index = merged.findIndex(
item =>
(file.uid && item.uid && String(item.uid) === String(file.uid)) ||
(file.url && this.getFileUrl(item) === file.url)
);
if (index >= 0) merged.splice(index, 1, { ...merged[index], ...file });
else merged.push(file);
});
const result = merged.filter(item => this.getFileUrl(item));
this.$emit('update:modelValue', result);
this.$emit('change', result);
this.$emit('update:modelValue', list);
this.$emit('change', list);
},
normalizeUploadFile(file, responseOverride) {
const response = responseOverride || file?.response;
let data = {};
if (response && typeof response === 'object') {
if (response.data && typeof response.data === 'object') {
data = response.data;
} else if (response.link || response.url || response.domain) {
data = response;
}
}
if (!data.link && !data.url && !data.domain) {
data = file?.data && typeof file.data === 'object' ? file.data : file || {};
}
const url =
data.link ||
data.url ||
data.domain ||
data.fileLink ||
data.fileUrl ||
file?.url ||
file?.link ||
'';
normalizeUploadFile(file) {
const data = (file.response && file.response.data) || file.data || file;
const url = data.link || data.url || data.domain || file.url || '';
const originalName =
data.originalName ||
file?.originalName ||
file?.name ||
file.originalName ||
file.name ||
data.name ||
this.getFileName(url) ||
'附件';
const extension = this.getExtension({ name: originalName, url });
return {
...data,
uid: file?.uid || data.uid,
uid: file.uid || data.uid,
originalName,
name: originalName,
url,
link: data.link || url,
size: data.size || data.attachSize || file?.size || '',
size: data.size || data.attachSize || file.size || '',
extension,
mimeType: data.mimeType || data.contentType || file?.raw?.type || MIME_MAP[extension] || '',
mimeType: data.mimeType || data.contentType || file.raw?.type || MIME_MAP[extension] || '',
};
},
handlePreview(file) {
-1
View File
@@ -1,7 +1,6 @@
FROM nginx
VOLUME /tmp
ENV LANG en_US.UTF-8
ADD ./src/docker/nginx.conf /etc/nginx/conf.d/default.conf
ADD ./dist/ /usr/share/nginx/html/
EXPOSE 80
EXPOSE 443
-19
View File
@@ -1,19 +0,0 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
location / {
add_header Cache-Control "no-store, no-cache, must-revalidate";
try_files $uri $uri/ /index.html;
}
}
-4
View File
@@ -1,12 +1,8 @@
import store from './store';
import { isChunkLoadError, reloadForChunkError } from './utils/chunk-reload';
export default {
install: app => {
app.config.errorHandler = (err, vm, info) => {
if (isChunkLoadError(err) && reloadForChunkError()) {
return;
}
store.commit('ADD_LOGS', {
type: 'error',
message: err.message,
-2
View File
@@ -1,5 +1,4 @@
import { createApp } from 'vue';
import { installChunkReload } from './utils/chunk-reload';
import website from './config/website';
import axios from './axios';
import router from './router/';
@@ -47,7 +46,6 @@ import sectionCard from './components/section-card/main.vue';
import mapSearchResults from './components/map-search-results/main.vue';
window.$crudCommon = crudCommon;
installChunkReload();
debug();
window.axios = axios;
const app = createApp(App);
-11
View File
@@ -146,13 +146,6 @@ export const createOption = () => ({
showWordLimit: true,
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
},
{
label: '创建人',
prop: 'createUserName',
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '更新时间',
prop: 'updateTime',
@@ -305,10 +298,6 @@ export const createOption = () => ({
searchOrder: 3,
searchPlaceholder: '请选择',
dicData: [],
props: {
label: 'label',
value: 'value',
},
filterable: true,
hide: true,
display: false,
-18
View File
@@ -2,9 +2,6 @@ const dimensionOptions = [
{ label: '重量', value: '重量' },
{ label: '体积', value: '体积' },
{ label: '数量', value: '数量' },
{ label: '整车', value: '整车' },
{ label: '里程', value: '里程' },
{ label: '吨·公里', value: '吨·公里' },
];
const statusOptions = [
@@ -41,21 +38,6 @@ export const createOption = () => ({
menuWidth: 240,
menuFixed: 'right',
column: [
{
label: '计量单位编码',
prop: 'unitCode',
minWidth: 150,
span: 24,
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',
+141 -145
View File
@@ -35,7 +35,26 @@ export const formatCargoValue = value => {
};
export const option = createCrudOption([
// ---------- 列表字段(顺序即表格展示顺序) ----------
{
label: '一级货物类型',
prop: 'firstCargoTypeName',
formslot: true,
order: 60,
span: 12,
minWidth: 150,
editDisabled: true,
rules: [{ required: true, message: '请选择一级货物类型', trigger: 'change' }],
},
{
label: '二级货物类型',
prop: 'secondCargoTypeName',
formslot: true,
order: 59,
span: 12,
minWidth: 150,
editDisabled: true,
rules: [{ required: true, message: '请选择二级货物类型', trigger: 'change' }],
},
{
label: '货物名称',
prop: 'cargoName',
@@ -59,148 +78,6 @@ export const option = createCrudOption([
{ max: 20, message: '货物编号不能超过20个字符', trigger: 'blur' },
],
},
{
label: '一级货物类型',
prop: 'firstCargoTypeName',
formslot: true,
order: 60,
span: 12,
minWidth: 150,
editDisabled: true,
rules: [{ required: true, message: '请选择一级货物类型', trigger: 'change' }],
},
{
label: '二级货物类型',
prop: 'secondCargoTypeName',
formslot: true,
order: 59,
span: 12,
minWidth: 150,
editDisabled: true,
rules: [{ required: true, message: '请选择二级货物类型', trigger: 'change' }],
},
{
label: '二级货物类型编码',
prop: 'secondCargoTypeCode',
display: false,
addDisplay: false,
editDisabled: true,
minWidth: 160,
},
{
label: '包装',
prop: 'packageType',
type: 'select',
dicData: packageOptions,
order: 39,
span: 12,
minWidth: 100,
},
{
label: '品牌',
prop: 'brand',
search: false,
order: 40,
span: 12,
minWidth: 120,
placeholder: '请输入',
rules: textRule('品牌', 50),
},
{
label: '规格',
prop: 'specification',
order: 29,
span: 12,
minWidth: 120,
placeholder: '请输入',
rules: textRule('规格', 50),
},
{
label: '型号',
prop: 'model',
order: 19,
span: 12,
minWidth: 120,
placeholder: '请输入',
rules: textRule('型号', 50, true),
},
{
label: '单价',
prop: 'cargoValue',
formslot: true,
order: 30,
formatter: row => formatCargoValue(row.cargoValue),
minWidth: 120,
rules: [
{
validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback();
return;
}
if (!/^\d+(\.\d{1,2})?$/.test(String(value))) {
callback(new Error('单价只能输入数字,最多保留2位小数'));
return;
}
callback();
},
trigger: 'blur',
},
],
},
{
label: '计价单位',
prop: 'priceUnit',
type: 'select',
dicData: [],
display: false,
minWidth: 120,
},
{
label: '尺寸',
prop: 'sizeText',
minWidth: 120,
addDisplay: false,
editDisplay: false,
rules: textRule('尺寸', 100),
},
{
label: '说明',
prop: 'descriptionOne',
order: 20,
span: 12,
minWidth: 160,
placeholder: '请输入',
rules: textRule('说明', 100),
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
order: 10,
minRows: 2,
span: 24,
maxlength: 200,
showWordLimit: true,
placeholder: '请输入',
rules: textRule('备注', 200),
},
{
label: '组织',
prop: 'deptName',
minWidth: 150,
addDisplay: false,
editDisplay: false,
},
...auditColumns
.filter(column => ['createUserName', 'createTime', 'updateTime'].includes(column.prop))
.sort(
(first, second) =>
['createUserName', 'createTime', 'updateTime'].indexOf(first.prop) -
['createUserName', 'createTime', 'updateTime'].indexOf(second.prop)
),
// ---------- 表单隐藏字段 ----------
{
label: '一级货物类型编码',
prop: 'firstCargoTypeCode',
@@ -209,8 +86,15 @@ export const option = createCrudOption([
addDisplay: false,
editDisabled: true,
},
// ---------- 搜索字段(与表格、表单独立) ----------
{
label: '二级货物类型编码',
prop: 'secondCargoTypeCode',
display: false,
addDisplay: false,
editDisabled: true,
minWidth: 180,
},
// 搜索字段与表格、表单字段独立,避免筛选配置影响数据展示和编辑布局。
{
label: '货物名称',
prop: 'searchCargoName',
@@ -302,6 +186,118 @@ export const option = createCrudOption([
editDisplay: false,
viewDisplay: false,
},
{
label: '包装',
prop: 'packageType',
type: 'select',
dicData: packageOptions,
order: 39,
span: 12,
minWidth: 100,
},
{
label: '品牌',
prop: 'brand',
search: false,
order: 40,
span: 12,
minWidth: 120,
placeholder: '请输入',
rules: textRule('品牌', 50),
},
{
label: '规格',
prop: 'specification',
order: 29,
span: 12,
minWidth: 120,
placeholder: '请输入',
rules: textRule('规格', 50),
},
{
label: '货值',
prop: 'cargoValue',
formslot: true,
order: 30,
formatter: row => formatCargoValue(row.cargoValue),
minWidth: 120,
rules: [
{
validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback();
return;
}
if (!/^\d+(\.\d{1,2})?$/.test(String(value))) {
callback(new Error('货值只能输入数字,最多保留2位小数'));
return;
}
callback();
},
trigger: 'blur',
},
],
},
{
label: '计价单位',
prop: 'priceUnit',
type: 'select',
dicData: [],
display: false,
minWidth: 120,
},
{
label: '型号',
prop: 'model',
order: 19,
span: 12,
minWidth: 120,
placeholder: '请输入',
rules: textRule('型号', 50, true),
},
{
label: '尺寸',
prop: 'sizeText',
minWidth: 120,
addDisplay: false,
editDisplay: false,
rules: textRule('尺寸', 100),
},
{
label: '说明',
prop: 'descriptionOne',
order: 20,
span: 12,
minWidth: 160,
placeholder: '请输入',
rules: textRule('说明', 100),
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
order: 10,
minRows: 2,
span: 24,
maxlength: 200,
showWordLimit: true,
placeholder: '请输入',
rules: textRule('备注', 200),
},
{
label: '组织',
prop: 'deptName',
minWidth: 150,
addDisplay: false,
editDisplay: false,
},
...auditColumns
.filter(column => ['updateTime', 'createTime'].includes(column.prop))
.sort(
(first, second) =>
['updateTime', 'createTime'].indexOf(first.prop) -
['updateTime', 'createTime'].indexOf(second.prop)
),
]);
// 搜索栏标签统一加宽,给较长的货物类型字段预留足够展示空间。
+3 -2
View File
@@ -281,13 +281,14 @@ export const option = {
filterable: true,
},
...auditColumns
.filter(column => column.prop !== 'createUserName')
.map(column =>
['createTime', 'updateTime'].includes(column.prop) ? { ...column, minWidth: 190 } : column
)
.sort(
(first, second) =>
['createUserName', 'updateUserName', 'createTime', 'updateTime'].indexOf(first.prop) -
['createUserName', 'updateUserName', 'createTime', 'updateTime'].indexOf(second.prop)
['updateUserName', 'updateTime', 'createTime'].indexOf(first.prop) -
['updateUserName', 'updateTime', 'createTime'].indexOf(second.prop)
),
]),
dialogCustomClass: 'avue-dialog--common-route',
-1
View File
@@ -117,7 +117,6 @@ export const contractCategoryOptions = [
export const signTypeOptions = [
{ label: '新签', value: '新签' },
{ label: '续签', value: '续签' },
{ label: '补充协议', value: '补充协议' },
];
export const contractStageOptions = [
+13 -49
View File
@@ -6,7 +6,6 @@ import {
contractStageOptions,
createCrudOption,
effectiveTypeOptions,
nonNegativeRule,
selectRule,
signTypeOptions,
textRule,
@@ -62,14 +61,13 @@ export const config = {
['contractNo', '合同编号'],
['contractName', '合同名称'],
['projectName', '所属项目'],
['organizationName', '合同负责部门'],
['organizationName', '所属组织'],
['contractCategory', '合同类别'],
['signType', '签约类型'],
['partyA', '甲方'],
['partyB', '乙方'],
['handlerUserName', '经办人'],
['signDate', '签订日期'],
['applyDate', '申请日期'],
['contractFormat', '合同格式'],
['legalSealFlag', '是否需要加盖法人章'],
['copyCount', '一式(份)'],
@@ -78,8 +76,7 @@ export const config = {
['templateFlag', '是否范本'],
['originalContractNo', '原件合同编号'],
['electronicSealFlag', '是否电子章'],
['mainContent', '合同主要内容', 2],
['remark', '其他事项说明', 2],
['remark', '备注', 2],
],
},
{
@@ -117,7 +114,7 @@ export const config = {
operations: [
{
action: 'flow',
label: '流程',
label: '查看流程',
statusProp: 'approvalStatus',
status: ['reviewing', 'change_reviewing'],
stage: ['temporary', 'formal'],
@@ -209,10 +206,10 @@ export const config = {
},
{
action: 'flow',
label: '流程',
label: '查看流程',
statusProp: 'approvalStatus',
status: ['approved', 'change_approved'],
stage: ['temporary', 'formal', 'terminated'],
stage: ['formal', 'terminated'],
permission: 'contract_manage_view',
},
{
@@ -297,17 +294,17 @@ export const option = createCrudOption([
display: false,
},
{
label: '合同负责部门',
label: '所属组织',
prop: 'organizationName',
search: true,
searchOrder: 3,
searchslot: true,
searchPlaceholder: '请选择 合同负责部门',
searchPlaceholder: '请选择 所属组织',
formslot: true,
order: 120,
dicData: [],
minWidth: 150,
rules: selectRule('合同负责部门'),
rules: selectRule('所属组织'),
},
{
label: '合同类别',
@@ -438,17 +435,6 @@ export const option = createCrudOption([
minWidth: 130,
rules: [{ required: true, message: '请选择签订日期', trigger: 'change' }],
},
{
label: '申请日期',
prop: 'applyDate',
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
sortable: true,
minWidth: 130,
addDisplay: false,
editDisplay: false,
},
{
label: '结算类型',
prop: 'settlementMode',
@@ -482,10 +468,11 @@ export const option = createCrudOption([
{ label: '否', value: 0 },
{ label: '是', value: 1 },
],
clearable: true,
value: 0,
hide: true,
order: 70,
minWidth: 100,
rules: selectRule('是否需要加盖法人章'),
},
{
label: '一式(份)',
@@ -513,10 +500,6 @@ export const option = createCrudOption([
hide: true,
order: 45,
minWidth: 130,
rules: [
{ required: true, message: '请输入合同金额', trigger: 'blur' },
nonNegativeRule('合同金额'),
],
},
{
label: '是否范本',
@@ -549,13 +532,6 @@ export const option = createCrudOption([
order: 30,
minWidth: 110,
},
{
label: '创建人',
prop: 'createUserName',
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '创建时间',
prop: 'createTime',
@@ -634,19 +610,7 @@ export const option = createCrudOption([
minWidth: 110,
},
{
label: '合同主要内容',
prop: 'mainContent',
type: 'textarea',
span: 24,
minRows: 4,
hide: true,
order: 41,
maxlength: 1000,
showWordLimit: true,
rules: textRule('合同主要内容', 1000),
},
{
label: '其他事项说明',
label: '备注',
prop: 'remark',
type: 'textarea',
span: 24,
@@ -655,7 +619,7 @@ export const option = createCrudOption([
order: 40,
maxlength: 2000,
showWordLimit: true,
rules: textRule('其他事项说明', 2000),
rules: textRule('备注', 2000),
},
{
label: '',
@@ -766,7 +730,7 @@ export const option = createCrudOption([
labelWidth: 0,
},
...auditColumns
.filter(column => !['createTime', 'createUserName'].includes(column.prop))
.filter(column => column.prop !== 'createTime')
.map(column => ({ ...column, hide: true })),
]);
+1 -2
View File
@@ -31,7 +31,7 @@ export const loadingManageConfig = {
exportColumns: [
{ prop: 'loadingNo', label: '配载单号' },
{ prop: 'loadingSubNos', label: '配载子单号' },
{ prop: 'vehicleNo', label: '车牌号/船班号/船次号/班次号' },
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' },
{ prop: 'driverName', label: '司机' },
{ prop: 'driverPhone', label: '联系电话' },
{ prop: 'carrierType', label: '承运类型' },
@@ -41,7 +41,6 @@ export const loadingManageConfig = {
{ prop: 'carrierName', label: '承运商' },
{ prop: 'transportType', label: '运输方式' },
{ prop: 'dataSource', label: '数据来源' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
{ prop: 'updateTime', label: '更新时间' },
{ prop: 'currentProcessNode', label: '过程节点' },
+3 -3
View File
@@ -112,12 +112,12 @@ export const option = {
editDisplay: false,
},
...auditColumns
.filter(column => ['createUserName', 'createTime', 'updateTime'].includes(column.prop))
.filter(column => ['updateTime', 'createTime'].includes(column.prop))
.map(column => ({ ...column, minWidth: 190 }))
.sort(
(first, second) =>
['createUserName', 'createTime', 'updateTime'].indexOf(first.prop) -
['createUserName', 'createTime', 'updateTime'].indexOf(second.prop)
['updateTime', 'createTime'].indexOf(first.prop) -
['updateTime', 'createTime'].indexOf(second.prop)
),
{
label: '项目ID集合',
+3 -17
View File
@@ -57,24 +57,16 @@ export const config = {
action: 'submitApproval',
label: '提交',
statusProp: 'approvalStatus',
status: ['draft', 'rejected', 'withdrawn'],
status: ['draft'],
permission: 'project_apply_submit',
},
{
action: 'withdraw',
label: '撤回',
statusProp: 'approvalStatus',
status: ['draft', 'reviewing'],
status: ['reviewing'],
permission: 'project_apply_withdraw',
},
{
action: 'mkAudit',
label: '流程',
statusProp: 'approvalStatus',
status: ['reviewing', 'change_reviewing', 'approved', 'change_approved'],
permission: 'project_apply_approve',
mkOnly: true,
},
{
action: 'approve',
label: '通过',
@@ -82,7 +74,6 @@ export const config = {
statusProp: 'approvalStatus',
status: ['reviewing', 'change_reviewing'],
permission: 'project_apply_approve',
localOnly: true,
},
{
action: 'reject',
@@ -91,7 +82,6 @@ export const config = {
statusProp: 'approvalStatus',
status: ['reviewing', 'change_reviewing'],
permission: 'project_apply_reject',
localOnly: true,
},
{
action: 'startChange',
@@ -452,9 +442,5 @@ export const option = createCrudOption([
span: 24,
hide: true,
},
...auditColumns.map(column =>
['createUserName', 'createTime'].includes(column.prop)
? column
: { ...column, hide: true }
),
...auditColumns.map(column => ({ ...column, hide: true })),
]);
+16 -18
View File
@@ -55,8 +55,8 @@ export const config = {
'templateType',
'transportType',
'createUserName',
'updateTime',
'remark',
'updateTime',
'createTime',
],
exportColumns: [
@@ -65,8 +65,8 @@ export const config = {
{ prop: 'templateType', label: '模板类型' },
{ prop: 'transportType', label: '运输方式' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'updateTime', label: '更新时间' },
{ prop: 'remark', label: '备注' },
{ prop: 'updateTime', label: '更新时间' },
{ prop: 'createTime', label: '创建时间' },
],
enableAllDept: false,
@@ -82,14 +82,12 @@ export const config = {
detailAttachmentDescriptionPlain: true,
enableTransportPlanForm: true,
enableShippingTemplateFreight: true,
editableRoadAddress: false,
editableRoadAddress: true,
fixedTransportAddressType: true,
enableTemplateCodePreview: true,
attachmentTitle: '附件',
defaultForm: {
templateType: '运输计划',
transportType: '公路运输',
transportTypeName: '公路运输',
},
transportFormRequiredFields: [
['projectName', '项目'],
@@ -121,16 +119,15 @@ export const option = {
labelWidth: 0,
},
{
label: '模板类型',
prop: 'templateType',
type: 'select',
label: '模板编号',
prop: 'templateCode',
search: true,
searchOrder: 2,
searchOrder: 4,
span: 8,
order: 390,
dicData: templateTypeOptions,
minWidth: 120,
rules: selectRule('模板类型'),
minWidth: 170,
disabled: true,
placeholder: '系统自动生成',
display: true,
},
{
@@ -145,15 +142,16 @@ export const option = {
display: true,
},
{
label: '模板编号',
prop: 'templateCode',
label: '模板类型',
prop: 'templateType',
type: 'select',
search: true,
searchOrder: 4,
searchOrder: 2,
span: 8,
order: 370,
minWidth: 170,
disabled: true,
placeholder: '系统自动生成',
dicData: templateTypeOptions,
minWidth: 120,
rules: selectRule('模板类型'),
display: true,
},
{
@@ -332,13 +332,9 @@ export const option = {
addDisplay: false,
editDisplay: false,
},
...auditColumns.map(column =>
['createUserName', 'createTime'].includes(column.prop)
? column
: { ...column, hide: true }
),
...auditColumns.map(column => ({ ...column, hide: true })),
])),
dialogWidth: 1100,
dialogWidth: '96%',
menuFixed: 'right',
menuWidth: 320,
index: false,
+7 -11
View File
@@ -209,12 +209,12 @@ export const config = {
attachmentTitle: '附件',
searchRangeMap: {
planStartDateRange: ['planStartDateStart', 'planStartDateEnd'],
createTimeRange: ['createTimeStart', 'createTimeEnd', '00:00:00', '23:59:59'],
planEndDateRange: ['planEndDateStart', 'planEndDateEnd'],
},
actions: ['copy', 'complete'],
statusProp: 'businessStatus',
statusTextProp: 'businessStatusName',
deleteStatus: ['draft'],
deleteStatus: ['draft', 'waiting_dispatch'],
editStatus: ['draft', 'waiting_dispatch', 'dispatching'],
detailButton: true,
detailSections: [
@@ -249,10 +249,9 @@ export const config = {
title: '其它信息',
fields: [
['remark', '备注'],
['createUserName', '创建人'],
['createTime', '创建时间'],
['updateUserName', '更新人'],
['updateTime', '更新时间'],
['createTime', '创建时间'],
],
},
],
@@ -342,7 +341,6 @@ export const option = {
{
label: '发货地址',
prop: 'departureAddress',
slot: true,
formslot: true,
search: true,
searchOrder: 7,
@@ -354,7 +352,6 @@ export const option = {
{
label: '到货地址',
prop: 'arrivalAddress',
slot: true,
formslot: true,
search: true,
searchOrder: 6,
@@ -400,6 +397,7 @@ export const option = {
span: 6,
order: 350,
minWidth: 150,
rules: [{ required: true, message: '请选择计划开始日期', trigger: 'change' }],
},
{
label: '计划结束日期',
@@ -410,6 +408,7 @@ export const option = {
span: 6,
order: 340,
minWidth: 150,
rules: [{ required: true, message: '请选择计划结束日期', trigger: 'change' }],
},
{
label: '数据来源',
@@ -456,9 +455,6 @@ export const option = {
showWordLimit: true,
rules: textRule('备注', 200),
},
{
...auditColumn('createUserName'),
},
{
...auditColumn('updateUserName'),
},
@@ -581,8 +577,8 @@ export const option = {
viewDisplay: false,
},
{
label: '创建时间',
prop: 'createTimeRange',
label: '计划结束日期',
prop: 'planEndDateRange',
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
+3 -44
View File
@@ -98,24 +98,6 @@ const getProcessConfigNodes = row => {
const isAcceptProcessNode = node => node.key === 'accept' || node.name === '接单';
/**
* 过程节点列显示文案。
*
* 该列在库里存的是「原始值」,且历史上写入了两种格式:司机打卡推进写的是配置的
* key(unload / dispatch / load …),配载单新建默认值写的是中文名(接单)。
* 故这里按两种格式都认一次,认不出就原样显示,不把值吞掉。
*
* 运单创建后、司机首次打卡前该字段为空,属正常——显示「-」,避免留白看不出是没值还是没渲染。
*/
const formatCurrentProcessNode = row => {
const value = String(getFirstValue(row, ['currentProcessNode', 'currentNode']) || '').trim();
if (!value) return '-';
const node = getProcessConfigNodes(row).find(
item => item.key === value || item.name === value
);
return node?.name || node?.key || value;
};
const isTruthyFlag = value => {
if (value === true || value === 1) return true;
if (value === false || value === 0 || value === null || value === undefined || value === '') {
@@ -561,8 +543,7 @@ export const option = {
prop: 'projectName',
formslot: true,
search: true,
searchLabel: '项目名称',
searchPlaceholder: '请选择或输入',
searchLabel: '项目',
searchOrder: 22,
span: 6,
order: 890,
@@ -574,7 +555,6 @@ export const option = {
prop: 'customerName',
search: true,
searchLabel: '客户名称',
searchPlaceholder: '请选择或输入',
searchOrder: 21,
minWidth: 150,
addDisplay: false,
@@ -583,10 +563,10 @@ export const option = {
display: false,
},
{
label: '车牌号/船班号/船次号/班次号',
label: '车牌号/航班号/船号/班列号',
prop: 'vehicleNo',
search: true,
searchLabel: '车牌号/船班号/船次号/班次号',
searchLabel: '车/船/航班/班列',
searchOrder: 13,
minWidth: 210,
display: false,
@@ -596,7 +576,6 @@ export const option = {
prop: 'driverName',
search: true,
searchLabel: '司机名称',
searchPlaceholder: '请选择或输入',
searchOrder: 14,
minWidth: 120,
display: false,
@@ -626,7 +605,6 @@ export const option = {
{
label: '过程节点',
prop: 'currentProcessNode',
formatter: row => formatCurrentProcessNode(row),
minWidth: 140,
addDisplay: false,
editDisplay: false,
@@ -648,7 +626,6 @@ export const option = {
prop: 'carrierName',
search: true,
searchLabel: '承运商名称',
searchPlaceholder: '请选择或输入',
searchOrder: 15,
minWidth: 150,
display: false,
@@ -752,30 +729,15 @@ export const option = {
prop: 'planName',
formslot: true,
search: true,
searchPlaceholder: '请选择或输入',
searchOrder: 7,
span: 6,
order: 870,
minWidth: 150,
formatter: row => {
const value = row?.planName;
if (
value === undefined ||
value === null ||
value === '' ||
String(value) === '-1' ||
Number(value) === -1
) {
return '';
}
return value;
},
},
{
label: '货物类型',
prop: 'cargoType',
search: true,
searchPlaceholder: '请选择或输入',
searchOrder: 18,
formatter: row => formatGoodsField(row, ['cargoType', 'goodsType', 'typeName']),
minWidth: 130,
@@ -902,9 +864,6 @@ export const option = {
viewDisplay: false,
display: false,
},
{
...auditColumn('createUserName'),
},
{
...auditColumn('createTime'),
},
+1
View File
@@ -13,6 +13,7 @@ export const invoiceApplicationTableColumns = [
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
];
export const invoiceApplicationDetailColumns = [
+4 -2
View File
@@ -11,6 +11,8 @@ export const invoiceReceiptTableColumns = [
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
{ prop: 'createUserName', label: '创建人', minWidth: 110 },
{ prop: 'createTime', label: '创建时间', minWidth: 170 },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
{ prop: 'kingdeeReceivedInvoiceNo', label: '金蝶单据号', minWidth: 150 },
{ prop: 'kingdeePushStatusName', label: '金蝶状态', minWidth: 130 },
{ prop: 'kingdeePushMsg', label: '失败原因', minWidth: 160 },
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
];
+21 -165
View File
@@ -1,165 +1,21 @@
/**
* 付款申请列表(avue-crud)配置
* ------------------------------------------------------------------
* 列表页与其它模块统一使用 avue-crud,头部 4 个按钮(刷新 / 列显隐 / 搜索显隐 /
* 表格卡片切换)由 avue-crud 自带,不做二次封装。
*
* 搜索项与表格列 label 不一致的(如表格列「收款方」/ 搜索项「客户名称」),
* 单独定义一个 hide 的搜索列,并用 searchProp 映射到后端字段。
*
* ⚠️ avue 的 searchOrder 为降序排序,值越大越靠前(见 header-search2.js:
* `list.sort((a, b) => (b.searchOrder || 0) - (a.searchOrder || 0))`)。
*/
const textFormatter = (row, value) =>
value === null || value === undefined || value === '' ? '-' : value;
const moneyFormatter = (row, value) => Number(value || 0).toFixed(2);
// 仅用于搜索、不在表格中出现的列
const searchOnlyColumn = ({ label, prop, searchOrder, searchProp, dicData }) => {
const column = {
label,
prop,
search: true,
searchOrder,
searchPlaceholder: '请输入',
hide: true,
showColumn: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
};
if (searchProp) column.searchProp = searchProp;
if (dicData) {
column.type = 'select';
column.searchPlaceholder = '请选择';
column.dicData = dicData;
}
return column;
};
export const paymentApplicationOption = ({
paymentTypeOptions = [],
approvalStatusOptions = [],
kingdeeStatusOptions = [],
} = {}) => ({
height: 'auto',
calcHeight: 32,
tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
searchShow: true,
searchIcon: true,
searchLabelWidth: 88,
searchMenuSpan: 24,
searchIndex: 4,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
indexWidth: 70,
indexFixed: 'left',
selection: true,
selectionWidth: 52,
selectionFixed: 'left',
addBtn: false,
viewBtn: false,
delBtn: false,
editBtn: false,
copyBtn: false,
menuWidth: 320,
menuFixed: 'right',
dialogClickModal: false,
column: [
// ---------- 搜索项(前 4 项默认展示,其余折叠;searchOrder 越大越靠前) ----------
{
label: '单据号',
prop: 'paymentNo',
slot: true,
search: true,
searchOrder: 9,
searchPlaceholder: '请输入',
minWidth: 150,
},
{
label: '日期',
prop: 'applyDateRange',
type: 'daterange',
searchType: 'daterange',
search: true,
searchOrder: 8,
hide: true,
showColumn: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
searchOnlyColumn({
label: '客户名称',
prop: 'payeeNameSearch',
searchOrder: 7,
searchProp: 'payeeName',
}),
searchOnlyColumn({
label: '项目名称',
prop: 'projectNameSearch',
searchOrder: 6,
searchProp: 'projectName',
}),
searchOnlyColumn({
label: '关联结算单',
prop: 'settlementNo',
searchOrder: 4,
}),
searchOnlyColumn({
label: '付款类型',
prop: 'paymentType',
searchOrder: 3,
dicData: paymentTypeOptions,
}),
searchOnlyColumn({
label: '审核状态',
prop: 'approvalStatus',
searchOrder: 2,
dicData: approvalStatusOptions,
}),
searchOnlyColumn({
label: '金蝶单据状态',
prop: 'kingdeeStatus',
searchOrder: 1,
dicData: kingdeeStatusOptions,
}),
// ---------- 表格列 ----------
{ label: '付款类型', prop: 'paymentTypeName', minWidth: 110, formatter: textFormatter },
{ label: '所属项目', prop: 'projectName', minWidth: 140, formatter: textFormatter },
{
label: '所属组织',
prop: 'deptName',
search: true,
searchOrder: 5,
searchslot: true,
minWidth: 120,
formatter: textFormatter,
},
{ label: '合同编号', prop: 'contractNo', minWidth: 130, formatter: textFormatter },
{ label: '合同名称', prop: 'contractName', minWidth: 140, formatter: textFormatter },
{ label: '付款方', prop: 'payerName', minWidth: 130, formatter: textFormatter },
{ label: '收款方', prop: 'payeeName', minWidth: 130, formatter: textFormatter },
{ label: '申请付款金额', prop: 'appliedAmount', minWidth: 140, formatter: moneyFormatter },
{ label: '已付款金额', prop: 'paidAmount', minWidth: 130, formatter: moneyFormatter },
{
label: '匹配发票金额',
prop: 'matchedInvoiceAmount',
minWidth: 130,
formatter: moneyFormatter,
},
{ label: '申请人', prop: 'applicantName', minWidth: 100, formatter: textFormatter },
{ label: '申请日期', prop: 'applyDate', minWidth: 120, formatter: textFormatter },
{ label: '审核状态', prop: 'approvalStatusName', slot: true, minWidth: 110 },
{ label: '当前节点', prop: 'currentNode', minWidth: 120, formatter: textFormatter },
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 120, formatter: textFormatter },
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 130, formatter: textFormatter },
{ label: '金蝶单据状态', prop: 'kingdeeStatusName', minWidth: 130, formatter: textFormatter },
],
});
export const paymentApplicationTableColumns = [
{ prop: 'paymentNo', label: '单据号', minWidth: 140, link: true },
{ prop: 'paymentTypeName', label: '付款类型', minWidth: 110 },
{ prop: 'projectName', label: '所属项目', minWidth: 140 },
{ prop: 'deptName', label: '所属组织', minWidth: 120 },
{ prop: 'contractNo', label: '合同编号', minWidth: 130 },
{ prop: 'contractName', label: '合同名称', minWidth: 140 },
{ prop: 'payerName', label: '付款方', minWidth: 130 },
{ prop: 'payeeName', label: '收款方', minWidth: 130 },
{ prop: 'appliedAmount', label: '申请付款金额', minWidth: 140, money: true },
{ prop: 'paidAmount', label: '已付款金额', minWidth: 130, money: true },
{ prop: 'matchedInvoiceAmount', label: '匹配发票金额', minWidth: 130, money: true },
{ prop: 'applicantName', label: '申请人', minWidth: 100 },
{ prop: 'applyDate', label: '申请日期', minWidth: 120 },
{ prop: 'approvalStatusName', label: '审核状态', minWidth: 110, status: true },
{ prop: 'currentNode', label: '当前节点', minWidth: 120 },
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 120 },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 130 },
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 130 },
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
];
@@ -21,6 +21,7 @@ export const formalSettlementTableColumns = [
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 },
{ label: '金蝶单据状态', prop: 'kingdeeSyncStatus', minWidth: 135 },
{ label: '附件同步', prop: 'attachmentSyncStatusName', minWidth: 100 },
{ label: '创建人', prop: 'createUserName', minWidth: 110 },
{ label: '创建时间', prop: 'createTime', minWidth: 170 },
];
@@ -88,6 +88,8 @@ export const updateFeeFormFields = [
];
export const transferSearchFields = [
{ label: '单据号', prop: 'documentNo', type: 'input' },
{ label: '合同编码', prop: 'contractNo', type: 'input' },
{ label: '合同名称', prop: 'contractName', type: 'input' },
{ label: '批次号', prop: 'batchNo', type: 'input' },
{ label: '生成日期', prop: 'generateDateRange', type: 'daterange' },
@@ -104,7 +106,7 @@ export const generateWaybillColumns = [
{ label: '运单号', prop: 'waybillNo', minWidth: 160, link: true },
{ label: '项目名称', prop: 'projectName', minWidth: 160 },
{ label: '客户/委托方', prop: 'customerName', minWidth: 160 },
{ label: '车牌号/船班号/船次号/班次号', prop: 'vehicleNo', minWidth: 190 },
{ label: '车牌号/航班号/船号/班列号', prop: 'vehicleNo', minWidth: 190 },
{ label: '司机', prop: 'driverName', minWidth: 120 },
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
{ label: '运输类型', prop: 'transportType', minWidth: 130 },
+2 -3
View File
@@ -5,7 +5,6 @@ export const GRANT_TYPE_DIC = [
{ label: '社交登录', value: 'social' },
{ label: '客户端凭证', value: 'client_credentials' },
{ label: '刷新令牌', value: 'refresh_token' },
{ label: '退出登录', value: 'logout' },
];
export const authLogOption = {
@@ -39,7 +38,7 @@ export const authLogOption = {
prop: 'realName',
},
{
label: '操作类型',
label: '授权类型',
prop: 'grantType',
type: 'select',
search: true,
@@ -81,7 +80,7 @@ export const authLogOption = {
width: 120,
},
{
label: '操作时间',
label: '登录时间',
prop: 'loginTime',
sortable: true,
span: 24,
-205
View File
@@ -1,205 +0,0 @@
const identityDic = [
{ label: '司机', value: 'driver' },
{ label: '管理员', value: 'admin' },
];
const statusDic = [
{ label: '启用', value: 1 },
{ label: '停用', value: 0 },
];
const validatePhone = (rule, value, callback) => {
const phone = String(value || '').trim();
if (!phone) {
callback(new Error('请输入手机号'));
return;
}
if (!/^1\d{10}$/.test(phone)) {
callback(new Error('请输入正确的手机号'));
return;
}
callback();
};
const validateRemark = (rule, value, callback) => {
if (String(value || '').length > 200) {
callback(new Error('备注不能超过200字'));
} else {
callback();
}
};
export default {
height: 'auto',
calcHeight: 22,
dialogWidth: 760,
labelPosition: 'right',
labelWidth: 'auto',
tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
saveBtnText: '提交',
updateBtnText: '提交',
searchShow: true,
searchIcon: true,
searchLabelWidth: 160,
searchMenuSpan: 24,
searchIndex: 4,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
indexWidth: 70,
viewBtn: false,
delBtn: false,
editBtn: false,
addBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 320,
column: [
{
label: '手机号',
prop: 'phone',
minWidth: 130,
search: true,
searchOrder: 4,
searchSpan: 6,
searchPlaceholder: '请输入',
maxlength: 11,
showWordLimit: true,
order: 100,
rules: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ validator: validatePhone, trigger: 'blur' },
],
},
{
label: '账号',
prop: 'account',
minWidth: 130,
search: true,
searchOrder: 3,
searchSpan: 6,
searchPlaceholder: '请输入',
maxlength: 45,
order: 90,
placeholder: '默认与手机号相同',
},
{
label: '姓名',
prop: 'realName',
minWidth: 100,
search: true,
searchOrder: 2,
searchSpan: 6,
searchPlaceholder: '请输入',
maxlength: 10,
showWordLimit: true,
order: 80,
},
{
label: '昵称',
prop: 'name',
minWidth: 100,
maxlength: 20,
showWordLimit: true,
order: 70,
},
{
label: '小程序身份',
prop: 'identities',
type: 'select',
multiple: true,
dataType: 'string',
dicData: identityDic,
minWidth: 140,
search: true,
searchOrder: 1,
searchSpan: 6,
searchPlaceholder: '请选择',
order: 60,
rules: [{ required: true, message: '请选择小程序身份', trigger: 'change' }],
},
{
label: '性别',
prop: 'sex',
type: 'select',
dicUrl: '/blade-system/dict/dictionary?code=sex',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
minWidth: 80,
hide: true,
order: 50,
},
{
label: 'openid',
prop: 'openid',
minWidth: 180,
overHidden: true,
hide: true,
addDisplay: false,
editDisplay: true,
viewDisplay: true,
order: 40,
placeholder: '可清空以解绑微信',
},
{
label: '状态',
prop: 'status',
type: 'select',
dicData: statusDic,
dataType: 'number',
minWidth: 90,
search: true,
searchOrder: 0,
searchSpan: 6,
addDisplay: false,
editDisplay: false,
value: 1,
order: 30,
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
span: 24,
minRows: 2,
maxlength: 200,
showWordLimit: true,
hide: true,
order: 20,
rules: [{ validator: validateRemark, trigger: 'blur' }],
},
{
label: '创建时间',
prop: 'createTime',
minWidth: 170,
addDisplay: false,
editDisplay: false,
viewDisplay: true,
order: 10,
},
{
label: '更新人',
prop: 'updateUserName',
minWidth: 100,
addDisplay: false,
editDisplay: false,
viewDisplay: true,
order: 5,
},
{
label: '更新时间',
prop: 'updateTime',
minWidth: 170,
addDisplay: false,
editDisplay: false,
viewDisplay: true,
order: 1,
},
],
};
+7 -11
View File
@@ -1,9 +1,12 @@
import { getDeptLazyTree } from '@/api/system/dept';
import { validateLoginPassword } from '@/utils/validate';
export const userOption = safe => {
const validatePass = (rule, value, callback) => {
validateLoginPassword(rule, value, callback);
if (value === '') {
callback(new Error('请输入密码'));
} else {
callback();
}
};
const validatePass2 = (rule, value, callback) => {
if (value === '') {
@@ -27,8 +30,7 @@ export const userOption = safe => {
index: true,
selection: true,
addBtn: false,
viewBtn: false,
menuWidth: 160,
viewBtn: true,
dialogType: 'dialog',
dialogWidth: '90%',
labelPosition: 'right',
@@ -38,11 +40,8 @@ export const userOption = safe => {
{
label: '账号名',
prop: 'account',
slot: true,
minWidth: 180,
search: true,
searchOrder: 2,
searchPlaceholder: '支持模糊搜索',
display: false,
},
{
@@ -50,7 +49,6 @@ export const userOption = safe => {
prop: 'realName',
search: true,
searchOrder: 4,
searchPlaceholder: '支持模糊搜索',
display: false,
},
{
@@ -58,7 +56,6 @@ export const userOption = safe => {
prop: 'phone',
search: true,
searchOrder: 3,
searchPlaceholder: '支持模糊搜索',
display: false,
},
{ label: '工号', prop: 'code', display: false },
@@ -66,8 +63,7 @@ export const userOption = safe => {
label: '所属部门',
prop: 'deptName',
slot: true,
minWidth: 200,
overHidden: false,
overHidden: true,
display: false,
},
{ label: '岗位', prop: 'postName', overHidden: true, display: false },
+10 -51
View File
@@ -1,10 +1,7 @@
// 一级 value 与二级 value 都用中文原文:库里 exception_type 存的是大类中文名、
// exception_reason 存子类中文名(小程序上报即如此),此处必须与之一致,
// 否则按异常类型做精确匹配的查询会一行都查不到。
export const exceptionCategoryOptions = [
{
label: '交通异常',
value: '交通异常',
value: 'traffic',
children: [
{ label: '道路拥堵', value: '道路拥堵' },
{ label: '交通事故', value: '交通事故' },
@@ -15,7 +12,7 @@ export const exceptionCategoryOptions = [
},
{
label: '车辆异常',
value: '车辆异常',
value: 'vehicle',
children: [
{ label: '车辆故障维修', value: '车辆故障维修' },
{ label: '车辆事故', value: '车辆事故' },
@@ -27,7 +24,7 @@ export const exceptionCategoryOptions = [
},
{
label: '货物异常',
value: '货物异常',
value: 'cargo',
children: [
{ label: '货损', value: '货损' },
{ label: '货差', value: '货差' },
@@ -38,7 +35,7 @@ export const exceptionCategoryOptions = [
},
{
label: '其他异常',
value: '其他异常',
value: 'other',
children: [
{ label: '变更路线地址', value: '变更路线地址' },
{ label: '其他', value: '其他异常-其他' },
@@ -46,16 +43,6 @@ export const exceptionCategoryOptions = [
},
];
/** 一级类型的中文名列表,供编辑表单的级联选择使用 */
export const exceptionTypeOptions = exceptionCategoryOptions.map(item => ({
label: item.label,
value: item.value,
}));
/** 按一级类型取二级选项 */
export const exceptionReasonOptions = typeValue =>
exceptionCategoryOptions.find(item => item.value === typeValue)?.children || [];
export const disposalStatusOptions = [
{ label: '待处理', value: 'pending' },
{ label: '处理中', value: 'processing' },
@@ -78,30 +65,20 @@ export const tableColumns = [
{ label: '项目', prop: 'projectName', minWidth: 160 },
{ label: '车辆', prop: 'vehicleNo', minWidth: 150, link: true },
{ label: '上报人', prop: 'reporterName', minWidth: 120 },
{ label: '上报时间', prop: 'reportTime', minWidth: 170 },
{ label: '异常类型', prop: 'exceptionTypeName', minWidth: 160 },
{ label: '上报说明', prop: 'reportDescription', minWidth: 180 },
{ label: '异常节点', prop: 'nodeName', minWidth: 120 },
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
{ label: '最新跟进说明', prop: 'latestFollowContent', minWidth: 200 },
{ label: '状态', prop: 'disposalStatusName', minWidth: 120 },
];
// 详情/跟进弹窗的只读信息,按来源分两块:
// 「运单信息」是上报时从运单复制的字段,「异常信息」是司机上报的内容。
// 与编辑弹窗的分区口径一致,改动时两处要一起改。
export const detailWaybillFields = [
{ label: '运单号', prop: 'waybillNo' },
{ label: '配载单号/总单号', prop: 'loadingOrMasterNo' },
{ label: '车牌号', prop: 'vehicleNo' },
{ label: '项目', prop: 'projectName' },
{ label: '承运商', prop: 'carrierName' },
];
export const detailExceptionFields = [
export const detailFields = [
{ label: '配载单号/总单号', prop: 'loadingOrMasterNo', link: true },
{ label: '运单号', prop: 'waybillNo', link: true },
{ label: '项目', prop: 'projectName', link: true },
{ label: '车牌号', prop: 'vehicleNo', link: true },
{ label: '上报人', prop: 'reporterName' },
{ label: '上报时间', prop: 'reportTime' },
{ label: '异常节点', prop: 'nodeName' },
{ label: '承运商', prop: 'carrierName' },
{ label: '异常类型', prop: 'exceptionTypeName' },
{ label: '异常原因', prop: 'exceptionReason' },
{ label: '上报说明', prop: 'reportDescription', span: 2 },
@@ -112,21 +89,3 @@ export const followColumns = [
{ label: '跟进人', prop: 'followUserName', minWidth: 130 },
{ label: '跟进时间', prop: 'followTime', minWidth: 170 },
];
// 编辑表单只开放上报四件套。车牌、项目、承运商、配载单号由上报时从运单回填,
// 允许手改会让记录与运单脱钩;处置状态由跟进与完成流转,不在此处。
export const editFormFields = [
{ label: '异常类型', prop: 'exceptionType', type: 'select', required: true },
{ label: '异常原因', prop: 'exceptionReason', type: 'select' },
{ label: '上报说明', prop: 'reportDescription', type: 'textarea', required: true, span: 2 },
];
// 编辑弹窗顶部的运单信息,只读展示,帮助运管员确认改的是哪一单。
// 上报人与上报时间属上报行为,放在「异常信息」里,不在此处重复。
export const editWaybillFields = [
{ label: '运单号', prop: 'waybillNo' },
{ label: '配载单号/总单号', prop: 'loadingOrMasterNo' },
{ label: '车牌号', prop: 'vehicleNo' },
{ label: '项目', prop: 'projectName' },
{ label: '承运商', prop: 'carrierName' },
];
@@ -1,51 +0,0 @@
export const option = {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 4,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
indexWidth: 70,
selection: false,
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: false,
columnBtn: false,
menuWidth: 110,
column: [
{
label: '司机姓名',
prop: 'driverName',
search: true,
searchOrder: 3,
minWidth: 120,
},
{
label: '司机手机号',
prop: 'driverMobile',
search: true,
searchOrder: 2,
minWidth: 130,
},
{
label: '审核状态',
prop: 'statusText',
slot: true,
width: 110,
align: 'center',
},
{
label: '提交时间',
prop: 'submitTime',
slot: true,
minWidth: 170,
align: 'center',
},
],
};
@@ -1,58 +0,0 @@
export const option = {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 4,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
indexWidth: 70,
selection: false,
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: false,
columnBtn: false,
menuWidth: 110,
column: [
{
label: '车牌号',
prop: 'bizKey',
search: true,
searchOrder: 3,
minWidth: 140,
},
{
label: '司机姓名',
prop: 'driverName',
search: true,
searchOrder: 2,
minWidth: 120,
},
{
label: '司机手机号',
prop: 'driverMobile',
search: true,
searchOrder: 1,
minWidth: 130,
},
{
label: '审核状态',
prop: 'statusText',
slot: true,
width: 110,
align: 'center',
},
{
label: '提交时间',
prop: 'submitTime',
slot: true,
minWidth: 170,
align: 'center',
},
],
};
-17
View File
@@ -125,23 +125,6 @@ export const option = {
slot: true,
minWidth: 160,
},
{
label: '创建人',
prop: 'createUserName',
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '创建时间',
prop: 'createTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
minWidth: 170,
addDisplay: false,
editDisplay: false,
},
{
label: '准驾车型',
prop: 'drivingType',
@@ -91,23 +91,6 @@ export const option = {
minWidth: 140,
align: 'center',
},
{
label: '创建人',
prop: 'createUserName',
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '创建时间',
prop: 'createTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
minWidth: 170,
addDisplay: false,
editDisplay: false,
},
{
label: '',
prop: 'expireStatus',
@@ -95,8 +95,6 @@ export const option = {
align: 'center',
},
{
// 这里是「最新一次提交的状态」缓存,不等于该车当前是否已认证:
// 已认证由生效快照指针决定,曾审核通过即永久保持,重新提交不会让它退回审核中
label: '认证状态',
prop: 'certificationStatus',
slot: true,
@@ -106,10 +104,9 @@ export const option = {
type: 'select',
dicData: [
{ label: '全部', value: '' },
{ label: '审核中', value: 0 },
{ label: '已通过', value: 1 },
{ label: '认证中', value: 0 },
{ label: '已认证', value: 1 },
{ label: '已驳回', value: 2 },
{ label: '已作废', value: 3 },
],
minWidth: 110,
align: 'center',
@@ -154,23 +151,6 @@ export const option = {
minWidth: 130,
align: 'center',
},
{
label: '创建人',
prop: 'createUserName',
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '创建时间',
prop: 'createTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
minWidth: 170,
addDisplay: false,
editDisplay: false,
},
{
label: '',
prop: 'expireStatus',
@@ -12,9 +12,6 @@ export const option = {
height: 'auto',
calcHeight: 32,
dialogWidth: 1200,
// 弹窗根节点 class:avue-crud 弹窗默认 append-to-body 到 body,
// 页面内 scoped 样式命中不到弹窗内容,故用该 class 在全局样式里做兜底(见 styles/element-ui.scss)
dialogCustomClass: 'vehicle-dispatch-dialog',
labelPosition: 'right',
labelWidth: 'auto',
tip: false,
@@ -104,7 +101,6 @@ export const option = {
minWidth: 120,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '当前节点',
@@ -112,7 +108,6 @@ export const option = {
minWidth: 120,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '当前处理人',
@@ -120,7 +115,6 @@ export const option = {
minWidth: 130,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '创建人',
@@ -128,7 +122,6 @@ export const option = {
minWidth: 120,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '创建时间',
@@ -139,7 +132,6 @@ export const option = {
minWidth: 170,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
// ========== 新增/编辑表单专用(不进表格)==========
{
@@ -173,7 +165,6 @@ export const option = {
type: 'textarea',
hide: true,
span: 24,
minRows: 2,
maxlength: 200,
showWordLimit: true,
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
+1 -16
View File
@@ -1,7 +1,7 @@
import { formatUpdateUserName } from '@/utils/audit';
const validateNonNegative = (rule, value, callback) => {
if (value === undefined || value === null || value === '' || Number(value) === -1) {
if (value === undefined || value === null || value === '') {
callback();
} else if (Number(value) < 0) {
callback(new Error('不能小于 0'));
@@ -10,9 +10,6 @@ const validateNonNegative = (rule, value, callback) => {
}
};
const formatEmptySentinel = (row, value) =>
value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value;
export const vehicleTypeDic = [
{ label: '车辆', value: '车辆' },
{ label: '船舶', value: '船舶' },
@@ -138,7 +135,6 @@ export const option = {
minWidth: 140,
span: 12,
placeholder: '请输入',
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -150,7 +146,6 @@ export const option = {
minWidth: 140,
span: 12,
placeholder: '请输入',
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -209,14 +204,6 @@ export const option = {
editDisplay: false,
viewDisplay: false,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -294,8 +281,6 @@ export const exportColumns = [
{ prop: 'insuranceClaimAmount', label: '保险理赔金额' },
{ prop: 'accidentReasonDamage', label: '事故原因及损坏情况' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
+1 -15
View File
@@ -1,7 +1,7 @@
import { formatUpdateUserName } from '@/utils/audit';
const validateNonNegative = (rule, value, callback) => {
if (value === undefined || value === null || value === '' || Number(value) === -1) {
if (value === undefined || value === null || value === '') {
callback();
} else if (Number(value) < 0) {
callback(new Error('不能小于 0'));
@@ -10,9 +10,6 @@ const validateNonNegative = (rule, value, callback) => {
}
};
const formatEmptySentinel = (row, value) =>
value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value;
export const vehicleTypeDic = [
{ label: '车辆', value: '车辆' },
{ label: '船舶', value: '船舶' },
@@ -162,7 +159,6 @@ export const option = {
minWidth: 100,
span: 12,
placeholder: '请输入',
formatter: formatEmptySentinel,
rules: [
{ required: true, message: '请输入费用', trigger: 'blur' },
{ validator: validateNonNegative, trigger: 'blur' },
@@ -210,14 +206,6 @@ export const option = {
editDisplay: false,
viewDisplay: false,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -294,8 +282,6 @@ export const exportColumns = [
{ prop: 'fee', label: '费用(元)' },
{ prop: 'assessmentUnit', label: '评定(复核)单位' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
-11
View File
@@ -147,15 +147,6 @@ export const option = {
viewDisplay: false,
minWidth: 170,
},
{
label: '创建人',
prop: 'createUserName',
display: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -233,8 +224,6 @@ export const exportColumns = [
{ prop: 'specificationModel', label: '规格型号' },
{ prop: 'remark', label: '备注' },
{ prop: 'originalEquipmentNo', label: '原厂设备号' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
-10
View File
@@ -172,14 +172,6 @@ export const option = {
editDisplay: false,
viewDisplay: false,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -226,8 +218,6 @@ export const exportColumns = [
{ prop: 'exitStation', label: '出口站' },
{ prop: 'dataSource', label: '数据来源' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
+1 -15
View File
@@ -1,7 +1,7 @@
import { formatUpdateUserName } from '@/utils/audit';
const validateNonNegative = (rule, value, callback) => {
if (value === undefined || value === null || value === '' || Number(value) === -1) {
if (value === undefined || value === null || value === '') {
callback();
} else if (Number(value) < 0) {
callback(new Error('不能小于 0'));
@@ -141,7 +141,6 @@ export const option = {
formslot: true,
append: '元',
minWidth: 120,
formatter: (row, value) => (value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value),
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -151,7 +150,6 @@ export const option = {
formslot: true,
append: '元',
minWidth: 120,
formatter: (row, value) => (value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value),
rules: [
{ required: true, message: '请输入保费', trigger: 'blur' },
{ validator: validateNonNegative, trigger: 'blur' },
@@ -161,7 +159,6 @@ export const option = {
label: '发票号',
prop: 'invoiceNo',
minWidth: 150,
formatter: (row, value) => (value === undefined || value === null || value === '' || String(value) === '-1' ? '' : value),
rules: [{ max: 80, message: '最多 80 个字符', trigger: 'blur' }],
},
{
@@ -171,7 +168,6 @@ export const option = {
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
minWidth: 120,
formatter: (row, value) => (value === undefined || value === null || value === '' || String(value) === '-1' ? '' : value),
},
{
label: '备注',
@@ -182,14 +178,6 @@ export const option = {
hide: true,
rules: [{ max: 200, message: '备注最多 200 个字', trigger: 'blur' }],
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -268,8 +256,6 @@ export const exportColumns = [
{ prop: 'invoiceNo', label: '发票号' },
{ prop: 'invoiceDate', label: '开票日期' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
+1 -18
View File
@@ -1,7 +1,6 @@
import { formatUpdateUserName } from '@/utils/audit';
const isEmpty = value =>
value === undefined || value === null || value === '' || Number(value) === -1;
const isEmpty = value => value === undefined || value === null || value === '';
const validateNonNegative = (rule, value, callback) => {
if (isEmpty(value)) {
@@ -13,8 +12,6 @@ const validateNonNegative = (rule, value, callback) => {
}
};
const formatEmptySentinel = (row, value) => (isEmpty(value) ? '' : value);
export const option = {
height: 'auto',
calcHeight: 32,
@@ -63,7 +60,6 @@ export const option = {
append: 'km',
minWidth: 210,
slot: true,
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -74,7 +70,6 @@ export const option = {
append: 'km',
minWidth: 210,
slot: true,
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -85,7 +80,6 @@ export const option = {
append: 'km',
minWidth: 170,
slot: true,
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -96,7 +90,6 @@ export const option = {
append: 'km',
minWidth: 170,
slot: true,
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -140,14 +133,6 @@ export const option = {
editDisplay: false,
viewDisplay: false,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -223,8 +208,6 @@ export const exportColumns = [
{ prop: 'monthlyMileage', label: '本月行驶里程数' },
{ prop: 'totalMileage', label: '累计行驶里程数' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
+1 -18
View File
@@ -1,7 +1,6 @@
import { formatUpdateUserName } from '@/utils/audit';
const isEmpty = value =>
value === undefined || value === null || value === '' || Number(value) === -1;
const isEmpty = value => value === undefined || value === null || value === '';
const validateNonNegative = (rule, value, callback) => {
if (isEmpty(value)) {
@@ -13,8 +12,6 @@ const validateNonNegative = (rule, value, callback) => {
}
};
const formatEmptySentinel = (row, value) => (isEmpty(value) ? '' : value);
export const vehicleTypeDic = [
{ label: '车辆', value: '车辆' },
{ label: '船舶', value: '船舶' },
@@ -127,7 +124,6 @@ export const option = {
type: 'input',
formslot: true,
minWidth: 110,
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -137,7 +133,6 @@ export const option = {
formslot: true,
append: '元/升',
minWidth: 120,
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -146,7 +141,6 @@ export const option = {
type: 'input',
formslot: true,
minWidth: 130,
formatter: formatEmptySentinel,
rules: [
{ required: true, message: '请输入交易金额', trigger: 'blur' },
{ validator: validateNonNegative, trigger: 'blur' },
@@ -158,7 +152,6 @@ export const option = {
type: 'input',
formslot: true,
minWidth: 110,
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -232,14 +225,6 @@ export const option = {
editDisplay: false,
viewDisplay: false,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -319,8 +304,6 @@ export const exportColumns = [
{ prop: 'balance', label: '余额' },
{ prop: 'station', label: '站点' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
@@ -165,14 +165,6 @@ export const option = {
editDisplay: false,
viewDisplay: false,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -217,8 +209,6 @@ export const exportColumns = [
{ prop: 'amount', label: '金额' },
{ prop: 'dataSource', label: '数据来源' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
+2 -17
View File
@@ -1,7 +1,7 @@
import { formatUpdateUserName } from '@/utils/audit';
const validateNonNegative = (rule, value, callback) => {
if (value === undefined || value === null || value === '' || Number(value) === -1) {
if (value === undefined || value === null || value === '') {
callback();
} else if (Number(value) < 0) {
callback(new Error('不能小于 0'));
@@ -11,7 +11,7 @@ const validateNonNegative = (rule, value, callback) => {
};
const validatePositiveInteger = (rule, value, callback) => {
if (value === undefined || value === null || value === '' || Number(value) === -1) {
if (value === undefined || value === null || value === '') {
callback();
} else if (!Number.isInteger(Number(value)) || Number(value) <= 0) {
callback(new Error('换胎数量必须为正整数'));
@@ -20,9 +20,6 @@ const validatePositiveInteger = (rule, value, callback) => {
}
};
const formatEmptySentinel = (row, value) =>
value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value;
export const option = {
height: 'auto',
calcHeight: 32,
@@ -101,7 +98,6 @@ export const option = {
minWidth: 110,
span: 12,
placeholder: '请输入',
formatter: formatEmptySentinel,
rules: [{ validator: validatePositiveInteger, trigger: 'blur' }],
},
{
@@ -112,7 +108,6 @@ export const option = {
minWidth: 120,
span: 12,
placeholder: '请输入',
formatter: formatEmptySentinel,
rules: [
{ required: true, message: '请输入换胎费用', trigger: 'blur' },
{ validator: validateNonNegative, trigger: 'blur' },
@@ -147,14 +142,6 @@ export const option = {
slot: true,
formslot: true,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -230,8 +217,6 @@ export const exportColumns = [
{ prop: 'replacementCost', label: '换胎费用' },
{ prop: 'replacementDescription', label: '换胎说明' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
@@ -97,14 +97,6 @@ export const option = {
slot: true,
formslot: true,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -177,8 +169,6 @@ export const exportColumns = [
{ prop: 'changeItem', label: '变更事项' },
{ prop: 'changeContent', label: '变更内容' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
+2 -17
View File
@@ -1,7 +1,7 @@
import { formatUpdateUserName } from '@/utils/audit';
const validateNonNegative = (rule, value, callback) => {
if (value === undefined || value === null || value === '' || Number(value) === -1) {
if (value === undefined || value === null || value === '') {
callback();
} else if (Number(value) < 0) {
callback(new Error('不能小于 0'));
@@ -11,7 +11,7 @@ const validateNonNegative = (rule, value, callback) => {
};
const validateDeductPoints = (rule, value, callback) => {
if (value === undefined || value === null || value === '' || Number(value) === -1) {
if (value === undefined || value === null || value === '') {
callback();
} else if (!Number.isInteger(Number(value)) || Number(value) < 0 || Number(value) > 15) {
callback(new Error('被扣分数范围为 0-15 分'));
@@ -20,9 +20,6 @@ const validateDeductPoints = (rule, value, callback) => {
}
};
const formatEmptySentinel = (row, value) =>
value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value;
export const vehicleTypeDic = [
{ label: '车辆', value: '车辆' },
{ label: '船舶', value: '船舶' },
@@ -153,7 +150,6 @@ export const option = {
minWidth: 100,
span: 12,
placeholder: '请输入',
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -164,7 +160,6 @@ export const option = {
minWidth: 110,
span: 12,
placeholder: '请输入',
formatter: formatEmptySentinel,
rules: [{ validator: validateDeductPoints, trigger: 'blur' }],
},
{
@@ -221,14 +216,6 @@ export const option = {
slot: true,
formslot: true,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -307,8 +294,6 @@ export const exportColumns = [
{ prop: 'processStatus', label: '状态' },
{ prop: 'processDescription', label: '过程描述' },
{ prop: 'processResult', label: '处理结果' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
+1 -12
View File
@@ -1,18 +1,7 @@
<template>
<router-view #="{ Component }">
<keep-alive :include="$store.getters.tagsKeep">
<component :is="tabView($route, Component)" />
<component :is="Component" />
</keep-alive>
</router-view>
</template>
<script>
import { tabView } from '@/router/tab';
export default {
name: 'layout',
methods: {
tabView,
},
};
</script>
+2 -2
View File
@@ -13,8 +13,8 @@ export default {
data() {
return {
loginForm: {
username: '',
password: '',
username: 'admin',
password: '123456',
},
};
},
+1 -3
View File
@@ -24,7 +24,6 @@
-->
<userLogin v-if="activeName === 'user'"></userLogin>
<registerLogin v-else-if="activeName === 'register'"></registerLogin>
<!-- 暂时默认账号密码登录,IAM 选择入口先隐藏
<div v-else class="iam-login">
<el-button type="primary" class="login-submit" @click.prevent="handleIamLogin">
IAM统一身份认证
@@ -33,7 +32,6 @@
账号密码登录
</el-button>
</div>
-->
</div>
</div>
</div>
@@ -63,7 +61,7 @@ export default {
return {
website: website,
time: '',
activeName: 'user',
activeName: 'iam',
socialForm: {
tenantId: '000000',
source: '',
+2 -2
View File
@@ -93,9 +93,9 @@ export default {
//角色ID
roleId: '',
//用户名
username: '',
username: 'admin',
//密码
password: '',
password: 'admin',
//账号类型
type: 'account',
//验证码的值
+10 -13
View File
@@ -3,8 +3,6 @@ import store from './store';
import { tabKeyOf } from '@/router/tab';
import { getToken } from '@/utils/auth';
import {
consumeReloadQuery,
hasReloadQuery,
isChunkLoadError,
reloadForChunkError,
setPendingRoutePath,
@@ -19,20 +17,19 @@ const lockPage = '/lock'; //锁屏页
router.onError(error => {
if (!isChunkLoadError(error)) return;
if (reloadForChunkError()) {
ElMessage.warning('页面资源已更新,正在重新加载…');
ElMessage.warning('页面资源加载失败,正在重新加载…');
}
});
window.addEventListener('unhandledrejection', event => {
if (!isChunkLoadError(event.reason)) return;
if (reloadForChunkError()) {
event.preventDefault();
ElMessage.warning('页面资源加载失败,正在重新加载…');
}
});
router.beforeEach((to, from, next) => {
if (hasReloadQuery(to.query)) {
next({
path: to.path,
query: consumeReloadQuery(to.query),
hash: to.hash,
replace: true,
});
return;
}
setPendingRoutePath(to.fullPath);
const meta = to.meta || {};
const isMenu = meta.menu === undefined ? to.query.menu : meta.menu;
@@ -62,7 +59,7 @@ router.beforeEach((to, from, next) => {
fullPath: tabKeyOf(to),
params: to.params,
query: to.query,
meta: { ...meta, keepAlive: true },
meta: meta,
});
}
next();
+10 -15
View File
@@ -2,9 +2,6 @@ import website from '@/config/website';
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 跨层级缓存
@@ -99,23 +96,21 @@ RouterPlugin.install = function (option = {}) {
component: (() => {
// 判断是否为首路由
if (first) {
return wrapViewLoader(
modules[
option.store.getters.isMacOs || !website.setting.menu
? '../page/index/layout.vue'
: '../page/index/index.vue'
]
);
return modules[
option.store.getters.isMacOs || !website.setting.menu
? '../page/index/layout.vue'
: '../page/index/index.vue'
];
// 判断是否为多层路由
} else if (isChild && !first) {
return wrapViewLoader(modules['../page/index/layout.vue']);
return modules['../page/index/layout.vue'];
// 判断是否为最终的页面视图
} else {
let result = modules[`../${component}.vue`];
if (!result) {
isComponent = false;
}
return wrapViewLoader(result);
return result;
}
})(),
name,
@@ -132,7 +127,7 @@ RouterPlugin.install = function (option = {}) {
if (first) {
oMenu[propsDefault.path] = `${path}`;
let componentPath = oMenu.component || component;
let result = wrapViewLoader(modules[`../${componentPath}.vue`]);
let result = modules[`../${componentPath}.vue`];
if (!result) {
isComponent = false;
}
@@ -178,7 +173,7 @@ export const formatPath = (ele, first) => {
const icon = ele[propsDefault.icon];
ele[propsDefault.icon] = icon || '';
ele.meta = {
keepAlive: true,
keepAlive: ele.isOpen === 2,
};
const iframeComponent = 'components/iframe/main';
const iframeSrc = href => {
@@ -220,7 +215,7 @@ export const formatPath = (ele, first) => {
ele[propsDefault.children].forEach(child => {
child.component = 'views' + child[propsDefault.path];
child.meta = {
keepAlive: true,
keepAlive: child.isOpen === 2,
};
if (isURL(child[propsDefault.href])) {
let href = child[propsDefault.href];
-98
View File
@@ -74,104 +74,6 @@ 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: '/business/project-apply/public-view',
name: '查看项目信息',
component: () => import('@/views/business/project-apply-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'project-apply',
},
},
{
path: '/business/contract-manage/public-view',
name: '查看合同信息',
component: () => import('@/views/business/contract-manage-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'contract-manage',
},
},
{
path: '/settlement/pre-settlement/public-view',
name: '查看预结算信息',
component: () => import('@/views/settlement/pre-settlement-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'pre-settlement',
},
},
{
path: '/settlement/formal-settlement/public-view',
name: '查看正式结算信息',
component: () => import('@/views/settlement/formal-settlement-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'formal-settlement',
},
},
{
path: '/payment/payment-application/public-view',
name: '查看付款申请信息',
component: () => import('@/views/payment/payment-application-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'payment-application',
},
},
{
path: '/payment/invoice-application/public-view',
name: '查看开票申请信息',
component: () => import('@/views/payment/invoice-application-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'invoice-application',
},
},
{
path: '/payment/invoice-receipt/public-view',
name: '查看收票登记信息',
component: () => import('@/views/payment/invoice-receipt-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'invoice-receipt',
},
},
{
path: '/payment/bill-payment/public-view',
name: '查看汇票付款信息',
component: () => import('@/views/payment/bill-payment-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'bill-payment',
},
},
{
path: '/',
name: '主页',
+1 -1
View File
@@ -128,5 +128,5 @@ export function tabView(route, Component) {
};
wrapperMap.set(tabKey, wrapper);
}
return wrapper;
return h(wrapper);
}
+38 -58
View File
@@ -1,8 +1,5 @@
import Layout from '@/page/index/index.vue';
import Store from '@/store/';
import { wrapViewLoader } from '@/utils/chunk-reload';
const loadView = loader => wrapViewLoader(loader);
export default [
{
@@ -13,7 +10,7 @@ export default [
path: '',
name: '汇票付款表单',
meta: { keepAlive: false, activeMenu: '/payment/bill-payment' },
component: loadView(() => import('@/views/payment/bill-payment-form.vue')),
component: () => import('@/views/payment/bill-payment-form.vue'),
},
],
},
@@ -25,7 +22,7 @@ export default [
path: '',
name: '汇票台账表单',
meta: { keepAlive: false, activeMenu: '/payment/bill-ledger' },
component: loadView(() => import('@/views/payment/bill-ledger-form.vue')),
component: () => import('@/views/payment/bill-ledger-form.vue'),
},
],
},
@@ -37,7 +34,7 @@ export default [
path: '',
name: '认领记录详情',
meta: { keepAlive: false, activeMenu: '/payment/receipt-claim-record' },
component: loadView(() => import('@/views/payment/receipt-claim-record-form.vue')),
component: () => import('@/views/payment/receipt-claim-record-form.vue'),
},
],
},
@@ -49,7 +46,7 @@ export default [
path: '',
name: '收款流水认领',
meta: { keepAlive: false, activeMenu: '/payment/receipt-flow' },
component: loadView(() => import('@/views/payment/receipt-flow-form.vue')),
component: () => import('@/views/payment/receipt-flow-form.vue'),
},
],
},
@@ -61,7 +58,7 @@ export default [
path: '',
name: '收票登记',
meta: { keepAlive: false, activeMenu: '/payment/invoice-receipt' },
component: loadView(() => import('@/views/payment/invoice-receipt-form.vue')),
component: () => import('@/views/payment/invoice-receipt-form.vue'),
},
],
},
@@ -73,7 +70,7 @@ export default [
path: '',
name: '开票申请',
meta: { keepAlive: false, activeMenu: '/payment/invoice-application' },
component: loadView(() => import('@/views/payment/invoice-application-form.vue')),
component: () => import('@/views/payment/invoice-application-form.vue'),
},
],
},
@@ -85,7 +82,7 @@ export default [
path: '',
name: '付款申请',
meta: { keepAlive: false, activeMenu: '/payment/payment-application' },
component: loadView(() => import('@/views/payment/payment-application-form.vue')),
component: () => import('@/views/payment/payment-application-form.vue'),
},
],
},
@@ -100,7 +97,7 @@ export default [
keepAlive: false,
activeMenu: '/settlement/pre-settlement',
},
component: loadView(() => import('@/views/settlement/pre-settlement-form.vue')),
component: () => import('@/views/settlement/pre-settlement-form.vue'),
},
],
},
@@ -115,7 +112,7 @@ export default [
keepAlive: false,
activeMenu: '/settlement/formal-settlement',
},
component: loadView(() => import('@/views/settlement/formal-settlement-form.vue')),
component: () => import('@/views/settlement/formal-settlement-form.vue'),
},
],
},
@@ -130,7 +127,7 @@ export default [
keepAlive: false,
activeMenu: '/settlement/transport-reconciliation',
},
component: loadView(() => import('@/views/settlement/transport-reconciliation-form.vue')),
component: () => import('@/views/settlement/transport-reconciliation-form.vue'),
},
],
},
@@ -142,7 +139,7 @@ export default [
path: '',
name: '执行凭证批次详情',
meta: { keepAlive: false, activeMenu: '/business/voucher-manage' },
component: loadView(() => import('@/views/business/voucher-manage-detail.vue')),
component: () => import('@/views/business/voucher-manage-detail.vue'),
},
],
},
@@ -154,7 +151,7 @@ export default [
path: '',
name: '新增合同管理',
meta: { keepAlive: false },
component: loadView(() => import('@/views/business/contract-manage.vue')),
component: () => import('@/views/business/contract-manage.vue'),
},
],
},
@@ -166,7 +163,7 @@ export default [
path: '',
name: '合同详情',
meta: { keepAlive: false, activeMenu: '/business/contract-manage' },
component: loadView(() => import('@/views/business/contract-manage.vue')),
component: () => import('@/views/business/contract-manage.vue'),
},
],
},
@@ -178,7 +175,7 @@ export default [
path: '',
name: '新增项目申请',
meta: { keepAlive: false },
component: loadView(() => import('@/views/business/project-apply.vue')),
component: () => import('@/views/business/project-apply.vue'),
},
],
},
@@ -190,7 +187,7 @@ export default [
path: '',
name: '新增编辑运单管理',
meta: { keepAlive: false },
component: loadView(() => import('@/views/business/waybill-manage.vue')),
component: () => import('@/views/business/waybill-manage.vue'),
},
],
},
@@ -202,7 +199,7 @@ export default [
path: '',
name: '运单管理详情',
meta: { keepAlive: false, activeMenu: '/business/waybill-manage' },
component: loadView(() => import('@/views/business/waybill-manage-detail.vue')),
component: () => import('@/views/business/waybill-manage-detail.vue'),
},
],
},
@@ -214,7 +211,7 @@ export default [
path: '',
name: '导入运单',
meta: { keepAlive: false, activeMenu: '/business/waybill-import' },
component: loadView(() => import('@/views/business/waybill-import.vue')),
component: () => import('@/views/business/waybill-import.vue'),
},
],
},
@@ -226,7 +223,7 @@ export default [
path: '',
name: '新建导入运单',
meta: { keepAlive: false, activeMenu: '/business/waybill-import' },
component: loadView(() => import('@/views/business/waybill-import-form.vue')),
component: () => import('@/views/business/waybill-import-form.vue'),
},
],
},
@@ -238,18 +235,6 @@ export default [
path: '',
name: '新增编辑配载管理',
meta: { keepAlive: false },
component: loadView(() => import('@/views/business/loading-manage.vue')),
},
],
},
{
path: '/business/loading-manage/detail',
component: Layout,
children: [
{
path: '',
name: '配载单详情',
meta: { keepAlive: false, activeMenu: '/business/loading-manage' },
component: () => import('@/views/business/loading-manage.vue'),
},
],
@@ -262,7 +247,7 @@ export default [
path: '',
name: '新增编辑运输计划',
meta: { keepAlive: false },
component: loadView(() => import('@/views/business/transport-plan.vue')),
component: () => import('@/views/business/transport-plan.vue'),
},
],
},
@@ -274,7 +259,7 @@ export default [
path: '',
name: '导入运输计划',
meta: { keepAlive: false, activeMenu: '/business/transport-plan' },
component: loadView(() => import('@/views/business/transport-plan-import.vue')),
component: () => import('@/views/business/transport-plan-import.vue'),
},
],
},
@@ -286,7 +271,7 @@ export default [
path: '',
name: '计划调度',
meta: { keepAlive: false, activeMenu: '/business/transport-plan' },
component: loadView(() => import('@/views/business/transport-plan-dispatch.vue')),
component: () => import('@/views/business/transport-plan-dispatch.vue'),
},
],
},
@@ -298,7 +283,7 @@ export default [
path: '',
name: '新增编辑运单模板',
meta: { keepAlive: false },
component: loadView(() => import('@/views/business/shipping-template.vue')),
component: () => import('@/views/business/shipping-template.vue'),
},
],
},
@@ -310,7 +295,7 @@ export default [
path: '',
name: '新增客商档案',
meta: { keepAlive: false },
component: loadView(() => import('@/views/vehicle/customer-archive.vue')),
component: () => import('@/views/vehicle/customer-archive.vue'),
},
],
},
@@ -322,7 +307,7 @@ export default [
path: '',
name: '合同变更',
meta: { keepAlive: false },
component: loadView(() => import('@/views/business/contract-manage-change.vue')),
component: () => import('@/views/business/contract-manage-change.vue'),
},
],
},
@@ -338,7 +323,7 @@ export default [
meta: {
i18n: 'dashboard',
},
component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/wel/index.vue')),
component: () => import(/* webpackChunkName: "views" */ '@/views/wel/index.vue'),
},
{
path: 'dashboard',
@@ -347,7 +332,7 @@ export default [
i18n: 'dashboard',
menu: false,
},
component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/wel/dashboard.vue')),
component: () => import(/* webpackChunkName: "views" */ '@/views/wel/dashboard.vue'),
},
],
},
@@ -362,7 +347,7 @@ export default [
meta: {
i18n: 'test',
},
component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/util/test.vue')),
component: () => import(/* webpackChunkName: "views" */ '@/views/util/test.vue'),
},
],
},
@@ -377,9 +362,8 @@ export default [
meta: {
i18n: 'dict',
},
component: loadView(() =>
import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-horizontal.vue')
),
component: () =>
import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-horizontal.vue'),
},
],
},
@@ -394,9 +378,8 @@ export default [
meta: {
i18n: 'dict',
},
component: loadView(() =>
import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-vertical.vue')
),
component: () =>
import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-vertical.vue'),
},
],
},
@@ -411,7 +394,7 @@ export default [
meta: {
i18n: 'info',
},
component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/system/userinfo.vue')),
component: () => import(/* webpackChunkName: "views" */ '@/views/system/userinfo.vue'),
},
],
},
@@ -426,9 +409,8 @@ export default [
meta: {
i18n: 'work',
},
component: loadView(() =>
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/form.vue')
),
component: () =>
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/form.vue'),
},
{
path: 'handle/:taskId/:processInstanceId/:businessId',
@@ -436,9 +418,8 @@ export default [
meta: {
i18n: 'work',
},
component: loadView(() =>
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/handle.vue')
),
component: () =>
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/handle.vue'),
},
{
path: 'detail/:processInstanceId/:businessId',
@@ -446,9 +427,8 @@ export default [
meta: {
i18n: 'work',
},
component: loadView(() =>
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/detail.vue')
),
component: () =>
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/detail.vue'),
},
],
},
+5 -6
View File
@@ -1,12 +1,8 @@
import { isAdministrator as checkAdministrator } from '@/utils/auth';
const getters = {
tag: state => state.tags.tag,
language: state => state.common.language,
setting: state => state.common.setting,
userInfo: state => state.user.userInfo,
// 超级管理员:角色别名中包含 administrator
isAdministrator: state => checkAdministrator(state.user.userInfo),
colorName: state => state.common.colorName,
themeName: state => state.common.themeName,
isMacOs: (state, getters) => getters.themeName === 'mac-os',
@@ -20,8 +16,11 @@ const getters = {
lockPasswd: state => state.common.lockPasswd,
tagList: state => state.tags.tagList,
tagsKeep: (state, getters) => {
// 所有已打开标签均纳入 keep-alive,关闭标签后自动从白名单移除并释放实例
return getters.tagList.map(ele => ele.fullPath).filter(Boolean);
return getters.tagList
.filter(ele => {
return (ele.meta || {}).keepAlive;
})
.map(ele => ele.fullPath);
},
tagWel: state => state.tags.tagWel,
token: state => state.user.token,
+3 -10
View File
@@ -150,15 +150,12 @@ a {
bottom: 0;
}
.map-picker-content {
position: relative;
display: block;
min-width: 0;
overflow: visible;
display: flex;
gap: 12px;
> [class$='__map'],
> .address-map {
position: relative;
z-index: 1;
flex: 1 1 auto;
width: 100%;
min-width: 0;
box-sizing: border-box;
@@ -168,8 +165,4 @@ a {
height: 100% !important;
}
}
> .map-search-results {
z-index: 2000;
}
}
+5 -103
View File
@@ -173,10 +173,7 @@
}
.el-dialog .el-col {
// 取消列自身下边距,改由 form-item 统一控制行距;
// flow-root 避免子级 margin 折叠穿透导致换行后行距失效、校验提示被下一行遮挡
margin-bottom: 0;
display: flow-root;
}
.el-dialog .el-form-item,
@@ -184,7 +181,7 @@
.el-dialog .el-form-item--small.el-form-item,
.el-dialog .el-form-item--large.el-form-item,
.el-dialog .el-form-item--medium.el-form-item {
margin-bottom: 24px;
//margin-bottom: 20px;
}
/* 全站弹窗:底部操作栏浮动效果(参照 business/project-apply)
@@ -365,14 +362,9 @@
}
}
// 4) 弹窗内表单项上下间距:预留绝对定位校验提示(约 14~18px)的可视空间
// 4) 弹窗内表单项默认上下间距 16px(覆盖默认 18px / 22px,与 customer-archive 对齐)
.el-dialog .el-form-item {
//margin-bottom: 24px;
}
// 校验提示浮在控件下方,提高层级避免被下一行输入框背景遮挡
.el-form-item__error {
z-index: 2;
//margin-bottom: 16px;
}
// 5) 通用分组白卡组件(<section-card>):标题融入卡头
@@ -454,7 +446,7 @@
.dialog-form--carded {
.el-form-item {
align-items: center;
margin-bottom: 24px;
margin-bottom: 14px;
}
.el-form-item__label {
@@ -652,28 +644,6 @@
background: #fff;
}
// 车辆调度新增/编辑/查看弹窗:使用部门 el-cascader 默认 display: inline-block、宽度按内容收缩,
// 会比同行的 el-input(--el-input-width 默认 100%)窄一大截,需显式拉满列宽。
// 两个必须注意的点(踩过):
// 1) avue-crud 弹窗默认 append-to-body 到 body,页面内 `.vehicle-dispatch-page :deep(...)`
// 这类以页面容器为祖先的选择器匹配不到弹窗内容,必须用弹窗自身 class(dialogCustomClass)在全局兜底;
// 2) avue 表单项在 el-form-item__content 与控件之间还有两层包裹 div
// (form/index3.js 渲染:content > div > [div|elTooltip] > 控件),
// 所以要用后代选择器,不能写 `>` 直接子级。
.vehicle-dispatch-dialog .el-form-item__content .el-cascader {
width: 100%;
max-width: 100%;
}
// 车辆调度弹窗 label 宽度统一:option 用 labelWidth: 'auto' 时 label 宽度随字数变化
// (3 字 64px / 2 字 40px / 4 字带星号 78px),控件可用宽度随之变化,
// 于是同一行两列控件宽度不一致(如车牌号 486px vs 使用部门 472px)。
// 本弹窗左右两列最长 label 均为 4 个汉字(所属组织 / 使用部门 / 申请时间),故统一按 4em + 24px 取宽。
.vehicle-dispatch-dialog .el-form-item__label {
flex: 0 0 calc(4em + 24px) !important;
width: calc(4em + 24px) !important;
}
// 过程配置新增/编辑/查看弹窗(手写 el-dialog,参照 vehicle/credit-score-quantification):
// 灰底弹窗体(全站 .el-dialog__body 已统一)内堆叠 <section-card> 分组白卡,
// 卡间 12px 间距由 section-card 自身 margin-bottom 保证,与项目其他弹窗风格统一
@@ -1025,66 +995,6 @@ td.el-table-fixed-column--right {
text-overflow: clip;
}
// 全站统一:内容型长文本列(文件名 / 单号 / 备注等)单元格内也放了 el-link / span.attachment-file-name,
// 会被上面两条「含 el-link 的单元格」规则一并命中,导致:
// ① 上面第 950 行的 display: flex 让链接成为 flex item,flex item 默认 min-width: auto 无法收缩,
// 超长文本(如未规范命名的附件名)会直接把列撑破;
// ② 豁免规则的 overflow: visible 让溢出内容不再被单元格裁切,直接压到相邻列上。
// 因此对「内容列」做显式兜底:单元格自身恢复块级 + 省略号截断,链接参与收缩并显示省略号。
// 用法:在 <el-table-column> 上配置 class-name="is-ellipsis-link"(动态列可 :class-name 绑定)。
// 操作列不要加此类,否则会失去自动换行能力。
.el-table .el-table__body .cell.is-ellipsis-link {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.el-link {
display: inline-block;
min-width: 0;
max-width: 100%;
overflow: hidden;
vertical-align: middle;
// Element Plus 的 el-link__inner 是 inline-flex,flex 容器上的 text-overflow 不生效,
// 必须降级为块级容器,省略号才会真正渲染出来。
.el-link__inner {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.attachment-file-name {
display: inline-block;
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
}
// 附件上传表格:文件名统一用 span,视觉与可点击行为对齐原 el-link primary
.attachment-file-name {
color: var(--el-color-primary, #409eff);
cursor: pointer;
&:hover,
&:focus-visible {
text-decoration: underline;
}
&.is-disabled {
color: var(--el-text-color-placeholder, #c0c4cc);
cursor: not-allowed;
text-decoration: none;
}
}
// 全站统一:Avue 内置行操作按钮(查看 / 编辑 / 删除 / 复制等)视觉对齐为 el-link 链接样式。
// 选择器仅命中表格行操作列 .avue-crud__menu 内的 is-text 按钮,不误伤顶部工具栏(.avue-crud__header)
// 与弹窗按钮(.avue-crud__dialog__menu)。事件(rowView/rowEdit/rowDel)与权限(permission 指令)
@@ -1163,12 +1073,7 @@ td.el-table-fixed-column--right {
align-items: normal;
}
.el-form-item--default {
margin-bottom: 24px;
}
// 独立表单页(非弹窗)同样避免 el-col 折叠吃掉行距
.el-form .el-col {
display: flow-root;
margin-bottom: 14px;
}
// 配套:星号必须保持「高度 auto + 继承行高」,stretch 才会按首行排布;
@@ -1343,6 +1248,3 @@ td.el-table-fixed-column--right {
cursor: not-allowed;
}
}
.is-ellipsis-link {
overflow: hidden;
}
+1 -24
View File
@@ -6,31 +6,8 @@ const RefreshTokenKey = website.refreshTokenKey;
const SessionId = 'JSESSIONID';
const UserId = 'b-user-id';
/** 超级管理员角色别名 */
export const ADMINISTRATOR_ROLE_ALIAS = 'administrator';
/**
* 是否超级管理员:角色别名中包含 administrator 即为超级管理员
*
* @param {object|string|string[]} [userInfoOrAuthority] 用户信息,或 authority / role_name 字符串/数组
* @returns {boolean}
*/
export function isAdministrator(userInfoOrAuthority) {
let authority = userInfoOrAuthority;
if (authority && typeof authority === 'object' && !Array.isArray(authority)) {
authority = authority.authority ?? authority.role_name ?? authority.roleName ?? '';
}
const roles = Array.isArray(authority)
? authority
: String(authority || '')
.split(/[,,]/)
.map(role => role.trim())
.filter(Boolean);
return roles.some(role => String(role).trim() === ADMINISTRATOR_ROLE_ALIAS);
}
export function getToken() {
return Cookies.get(TokenKey);
return Cookies.get(TokenKey);
}
export function setToken(token) {
-140
View File
@@ -1,140 +0,0 @@
import { getDetail, getList as getCustomerList } from '@/api/vehicle/customer-archive';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { getList as getVehicleList } from '@/api/transportCapacity/transport-vehicle';
const orgNameCache = new Map();
const extractRecords = res => {
const data = res?.data?.data ?? res?.data ?? res;
if (Array.isArray(data)) return data;
if (Array.isArray(data?.records)) return data.records;
if (Array.isArray(data?.data)) return data.data;
return [];
};
const normalizeOrgName = value => String(value || '').trim();
const uniqueOrgNames = (values = []) => {
const seen = new Set();
const result = [];
values.forEach(value => {
const name = normalizeOrgName(value);
if (!name || seen.has(name)) return;
seen.add(name);
result.push(name);
});
return result;
};
/**
* 解析承运商客商档案上联系人的所属组织(branchName),无联系人时回退客商所属组织。
*/
export const resolveCarrierOrganizationNames = async ({ carrierId, carrierName } = {}) => {
const id = String(carrierId || '').trim();
const name = normalizeOrgName(carrierName);
const cacheKey = id || name;
if (!cacheKey) return [];
if (orgNameCache.has(cacheKey)) return orgNameCache.get(cacheKey);
let detail = null;
if (id) {
try {
const res = await getDetail(id);
detail = res?.data?.data || res?.data || null;
} catch (error) {
detail = null;
}
}
if (!detail && name) {
try {
const res = await getCustomerList(1, 20, { fullName: name });
const records = extractRecords(res);
const matched =
records.find(item => normalizeOrgName(item.fullName || item.customerName) === name) ||
records[0];
if (matched?.id) {
const detailRes = await getDetail(matched.id);
detail = detailRes?.data?.data || detailRes?.data || matched;
}
} catch (error) {
detail = null;
}
}
const contacts = Array.isArray(detail?.contacts) ? detail.contacts : [];
const orgNames = uniqueOrgNames([
...contacts.map(item => item.branchName),
detail?.deptName,
detail?.organizationName,
]);
orgNameCache.set(cacheKey, orgNames);
if (id && name) orgNameCache.set(name, orgNames);
return orgNames;
};
export const clearCarrierOrganizationCache = (carrier = {}) => {
const id = String(carrier.carrierId || '').trim();
const name = normalizeOrgName(carrier.carrierName);
if (id) orgNameCache.delete(id);
if (name) orgNameCache.delete(name);
};
export const matchOrganizationName = (value, orgNames = []) => {
const text = normalizeOrgName(value);
if (!text || !orgNames.length) return false;
return orgNames.some(org => text === org || text.includes(org) || org.includes(text));
};
const filterByOrganizations = (records = [], orgNames = []) => {
if (!orgNames.length) return [];
return records.filter(item => matchOrganizationName(item.organizationName, orgNames));
};
/**
* 按承运商联系人所属组织筛选司机。
* 未选择承运商时返回空列表(需先选承运商)。
*/
export const fetchDriversByCarrierOrganizations = async (
query = {},
{ carrierId, carrierName } = {}
) => {
if (!String(carrierId || '').trim() && !normalizeOrgName(carrierName)) {
return [];
}
const orgNames = await resolveCarrierOrganizationNames({ carrierId, carrierName });
if (!orgNames.length) return [];
const size = Math.max(Number(query.size) || 50, 50);
const params = { ...query };
delete params.size;
// 单组织时用后端模糊条件缩小范围;多组织再前端精确过滤
if (orgNames.length === 1) {
params.organizationName = orgNames[0];
}
const res = await getDriverList(1, Math.min(size * 5, 200), params);
return filterByOrganizations(extractRecords(res), orgNames).slice(0, size);
};
/**
* 按承运商联系人所属组织筛选车辆。
* 未选择承运商时返回空列表。
*/
export const fetchVehiclesByCarrierOrganizations = async (
query = {},
{ carrierId, carrierName } = {}
) => {
if (!String(carrierId || '').trim() && !normalizeOrgName(carrierName)) {
return [];
}
const orgNames = await resolveCarrierOrganizationNames({ carrierId, carrierName });
if (!orgNames.length) return [];
const size = Math.max(Number(query.size) || 50, 50);
const params = { ...query };
delete params.size;
if (orgNames.length === 1) {
params.organizationName = orgNames[0];
}
const res = await getVehicleList(1, Math.min(size * 5, 200), params);
return filterByOrganizations(extractRecords(res), orgNames).slice(0, size);
};

Some files were not shown because too many files have changed in this diff Show More