diff --git a/.env.development b/.env.development
index 03c69cc..3043a7e 100644
--- a/.env.development
+++ b/.env.development
@@ -3,8 +3,9 @@
VITE_APP_ENV = 'development'
#接口地址
-# 开发环境建议填 [/api]:由下方 vite.config.mjs 的 proxy 同源转发到后端(172.16.203.228:8000),避免跨域(CORS)被浏览器拦截
-# 如需直连线上绝对地址(如 http://172.16.203.228:8000/api),必须让后端 CORS 把 Allow-Origin 改为具体前端域名,不能用 `*`(配合 credentials 会被浏览器拒绝)
+# 开发环境建议填 [/api]:由 vite.config.mjs 的 proxy 同源转发到本地网关 http://localhost(80)
+# 本地需先启动:Nacos + blade-gateway(80) + blade-auth + blade-system 等;8080 端口一般是 Nacos 控制台不是网关
+# 如需临时联调远程,把 vite proxy target 改为 http://172.16.203.228:8000 并去掉 rewrite
VITE_APP_API=/api
#调试参数
diff --git a/.workbuddy/memory/2026-09-10.md b/.workbuddy/memory/2026-09-10.md
new file mode 100644
index 0000000..db2cbed
--- /dev/null
+++ b/.workbuddy/memory/2026-09-10.md
@@ -0,0 +1,6 @@
+# 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'
diff --git a/.workbuddy/memory/2026-09-11.md b/.workbuddy/memory/2026-09-11.md
new file mode 100644
index 0000000..e1c56db
--- /dev/null
+++ b/.workbuddy/memory/2026-09-11.md
@@ -0,0 +1,28 @@
+# 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 分支的 ``**,中间不能插其它组件,否则 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 才能看全字段名的可发现性差。
diff --git a/.workbuddy/memory/2026-09-17.md b/.workbuddy/memory/2026-09-17.md
new file mode 100644
index 0000000..1fb0550
--- /dev/null
+++ b/.workbuddy/memory/2026-09-17.md
@@ -0,0 +1,63 @@
+# 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` 被沙箱敏感内容保护中断。
+- 踩坑:**`
diff --git a/src/components/map-search-results/main.vue b/src/components/map-search-results/main.vue
index 2cd3b73..c50ef14 100644
--- a/src/components/map-search-results/main.vue
+++ b/src/components/map-search-results/main.vue
@@ -1,20 +1,55 @@
-
搜索结果
-
-
-
{{ item.name || item.address || '未命名地址' }}
-
{{ item.address || item.name || '-' }}
+
+
+
![]()
+
+
+ {{ item.name || item.address || '未命名地址' }}
+
+
+ 地址:{{ item.address || item.name || '-' }}
+
+
+
+
+
diff --git a/src/components/vehicle-attachment-upload/main.vue b/src/components/vehicle-attachment-upload/main.vue
index 436efec..81961de 100644
--- a/src/components/vehicle-attachment-upload/main.vue
+++ b/src/components/vehicle-attachment-upload/main.vue
@@ -212,7 +212,14 @@ export default {
.filter(Boolean);
},
acceptText() {
- return this.acceptedList.join(',');
+ return this.acceptedList
+ .map(item => {
+ const value = String(item).trim();
+ if (!value || value.includes('/') || value.startsWith('.')) return value;
+ return `.${value}`;
+ })
+ .filter(Boolean)
+ .join(',');
},
tipText() {
return this.tip || `${UNIFIED_ATTACHMENT_TIP_PREFIX}${this.maxSize}M`;
@@ -304,12 +311,22 @@ export default {
});
},
handleSuccess(response, file, files) {
- if (!response || response.success === false || (response.code && response.code !== 200)) {
+ const code = response?.code;
+ const codeInvalid = code !== undefined && code !== null && code !== '' && Number(code) !== 200;
+ if (!response || response.success === false || codeInvalid) {
this.$message.error((response && response.msg) || '上传失败');
return;
}
- this.emitUploadFiles(this.multiple ? files : [file]);
- this.$emit('success', this.normalizeUploadFile(file));
+ if (file && !file.response) {
+ file.response = response;
+ }
+ const normalized = this.normalizeUploadFile(file, response);
+ if (!normalized.url) {
+ this.$message.error('上传成功但未返回文件地址');
+ return;
+ }
+ this.$emit('success', normalized);
+ this.emitUploadFiles(this.multiple ? files : [file], normalized);
this.$message.success('上传成功');
},
handleChange(file, files) {
@@ -323,35 +340,89 @@ export default {
this.emitUploadFiles(files);
return true;
},
- emitUploadFiles(files) {
- const list = files
- .filter(file => file.status === 'success' || this.getFileUrl(file))
- .map(this.normalizeUploadFile)
+ emitUploadFiles(files, preferredFile) {
+ const list = (files || [])
+ .filter(file => {
+ if (file?.status === 'fail') return false;
+ return (
+ file?.status === 'success' ||
+ this.getFileUrl(file) ||
+ file?.response?.data ||
+ file?.response?.link ||
+ file?.response
+ );
+ })
+ .map(file => this.normalizeUploadFile(file))
.filter(item => item.url);
- this.$emit('update:modelValue', list);
- this.$emit('change', list);
+
+ if (preferredFile?.url) {
+ const exists = list.some(
+ item =>
+ (preferredFile.uid && item.uid && String(item.uid) === String(preferredFile.uid)) ||
+ item.url === preferredFile.url
+ );
+ if (!exists) list.push(preferredFile);
+ }
+
+ const current = this.parseValue(this.modelValue).map(item => ({
+ ...item,
+ url: this.getFileUrl(item),
+ }));
+ const merged = [...current];
+ list.forEach(file => {
+ const index = merged.findIndex(
+ item =>
+ (file.uid && item.uid && String(item.uid) === String(file.uid)) ||
+ (file.url && this.getFileUrl(item) === file.url)
+ );
+ if (index >= 0) merged.splice(index, 1, { ...merged[index], ...file });
+ else merged.push(file);
+ });
+
+ const result = merged.filter(item => this.getFileUrl(item));
+ this.$emit('update:modelValue', result);
+ this.$emit('change', result);
},
- normalizeUploadFile(file) {
- const data = (file.response && file.response.data) || file.data || file;
- const url = data.link || data.url || data.domain || file.url || '';
+ normalizeUploadFile(file, responseOverride) {
+ const response = responseOverride || file?.response;
+ let data = {};
+ if (response && typeof response === 'object') {
+ if (response.data && typeof response.data === 'object') {
+ data = response.data;
+ } else if (response.link || response.url || response.domain) {
+ data = response;
+ }
+ }
+ if (!data.link && !data.url && !data.domain) {
+ data = file?.data && typeof file.data === 'object' ? file.data : file || {};
+ }
+ const url =
+ data.link ||
+ data.url ||
+ data.domain ||
+ data.fileLink ||
+ data.fileUrl ||
+ file?.url ||
+ file?.link ||
+ '';
const originalName =
data.originalName ||
- file.originalName ||
- file.name ||
+ file?.originalName ||
+ file?.name ||
data.name ||
this.getFileName(url) ||
'附件';
const extension = this.getExtension({ name: originalName, url });
return {
...data,
- uid: file.uid || data.uid,
+ uid: file?.uid || data.uid,
originalName,
name: originalName,
url,
link: data.link || url,
- size: data.size || data.attachSize || file.size || '',
+ size: data.size || data.attachSize || file?.size || '',
extension,
- mimeType: data.mimeType || data.contentType || file.raw?.type || MIME_MAP[extension] || '',
+ mimeType: data.mimeType || data.contentType || file?.raw?.type || MIME_MAP[extension] || '',
};
},
handlePreview(file) {
diff --git a/src/docker/Dockerfile b/src/docker/Dockerfile
index 88425be..88c648f 100644
--- a/src/docker/Dockerfile
+++ b/src/docker/Dockerfile
@@ -1,6 +1,7 @@
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
\ No newline at end of file
diff --git a/src/docker/nginx.conf b/src/docker/nginx.conf
new file mode 100644
index 0000000..0155f82
--- /dev/null
+++ b/src/docker/nginx.conf
@@ -0,0 +1,19 @@
+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;
+ }
+}
diff --git a/src/error.js b/src/error.js
index d2b5611..1c507e5 100644
--- a/src/error.js
+++ b/src/error.js
@@ -1,8 +1,12 @@
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,
diff --git a/src/main.js b/src/main.js
index 9e780e5..c53d2e6 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,4 +1,5 @@
import { createApp } from 'vue';
+import { installChunkReload } from './utils/chunk-reload';
import website from './config/website';
import axios from './axios';
import router from './router/';
@@ -46,6 +47,7 @@ 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);
diff --git a/src/option/base/measurement-unit.js b/src/option/base/measurement-unit.js
index c135823..7c68b87 100644
--- a/src/option/base/measurement-unit.js
+++ b/src/option/base/measurement-unit.js
@@ -38,6 +38,21 @@ 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',
diff --git a/src/option/business/shipping-template.js b/src/option/business/shipping-template.js
index eb90940..ad2f735 100644
--- a/src/option/business/shipping-template.js
+++ b/src/option/business/shipping-template.js
@@ -55,8 +55,8 @@ export const config = {
'templateType',
'transportType',
'createUserName',
- 'remark',
'updateTime',
+ 'remark',
'createTime',
],
exportColumns: [
@@ -65,8 +65,8 @@ export const config = {
{ prop: 'templateType', label: '模板类型' },
{ prop: 'transportType', label: '运输方式' },
{ prop: 'createUserName', label: '创建人' },
- { prop: 'remark', label: '备注' },
{ prop: 'updateTime', label: '更新时间' },
+ { prop: 'remark', label: '备注' },
{ prop: 'createTime', label: '创建时间' },
],
enableAllDept: false,
@@ -82,12 +82,14 @@ export const config = {
detailAttachmentDescriptionPlain: true,
enableTransportPlanForm: true,
enableShippingTemplateFreight: true,
- editableRoadAddress: true,
+ editableRoadAddress: false,
fixedTransportAddressType: true,
enableTemplateCodePreview: true,
attachmentTitle: '附件',
defaultForm: {
templateType: '运输计划',
+ transportType: '公路运输',
+ transportTypeName: '公路运输',
},
transportFormRequiredFields: [
['projectName', '项目'],
@@ -119,15 +121,16 @@ export const option = {
labelWidth: 0,
},
{
- label: '模板编号',
- prop: 'templateCode',
+ label: '模板类型',
+ prop: 'templateType',
+ type: 'select',
search: true,
- searchOrder: 4,
+ searchOrder: 2,
span: 8,
order: 390,
- minWidth: 170,
- disabled: true,
- placeholder: '系统自动生成',
+ dicData: templateTypeOptions,
+ minWidth: 120,
+ rules: selectRule('模板类型'),
display: true,
},
{
@@ -142,16 +145,15 @@ export const option = {
display: true,
},
{
- label: '模板类型',
- prop: 'templateType',
- type: 'select',
+ label: '模板编号',
+ prop: 'templateCode',
search: true,
- searchOrder: 2,
+ searchOrder: 4,
span: 8,
order: 370,
- dicData: templateTypeOptions,
- minWidth: 120,
- rules: selectRule('模板类型'),
+ minWidth: 170,
+ disabled: true,
+ placeholder: '系统自动生成',
display: true,
},
{
diff --git a/src/option/business/temporary-credit-limit.js b/src/option/business/temporary-credit-limit.js
index 4ad6fb4..eab56af 100644
--- a/src/option/business/temporary-credit-limit.js
+++ b/src/option/business/temporary-credit-limit.js
@@ -334,7 +334,7 @@ export const option = {
},
...auditColumns.map(column => ({ ...column, hide: true })),
])),
- dialogWidth: '96%',
+ dialogWidth: 1100,
menuFixed: 'right',
menuWidth: 320,
index: false,
diff --git a/src/option/business/transport-plan.js b/src/option/business/transport-plan.js
index 45b9154..5fb8150 100644
--- a/src/option/business/transport-plan.js
+++ b/src/option/business/transport-plan.js
@@ -209,12 +209,12 @@ export const config = {
attachmentTitle: '附件',
searchRangeMap: {
planStartDateRange: ['planStartDateStart', 'planStartDateEnd'],
- planEndDateRange: ['planEndDateStart', 'planEndDateEnd'],
+ createTimeRange: ['createTimeStart', 'createTimeEnd', '00:00:00', '23:59:59'],
},
actions: ['copy', 'complete'],
statusProp: 'businessStatus',
statusTextProp: 'businessStatusName',
- deleteStatus: ['draft', 'waiting_dispatch'],
+ deleteStatus: ['draft'],
editStatus: ['draft', 'waiting_dispatch', 'dispatching'],
detailButton: true,
detailSections: [
@@ -341,6 +341,7 @@ export const option = {
{
label: '发货地址',
prop: 'departureAddress',
+ slot: true,
formslot: true,
search: true,
searchOrder: 7,
@@ -352,6 +353,7 @@ export const option = {
{
label: '到货地址',
prop: 'arrivalAddress',
+ slot: true,
formslot: true,
search: true,
searchOrder: 6,
@@ -577,8 +579,8 @@ export const option = {
viewDisplay: false,
},
{
- label: '计划结束日期',
- prop: 'planEndDateRange',
+ label: '创建时间',
+ prop: 'createTimeRange',
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
diff --git a/src/option/business/waybill-manage.js b/src/option/business/waybill-manage.js
index fe51191..c981066 100644
--- a/src/option/business/waybill-manage.js
+++ b/src/option/business/waybill-manage.js
@@ -543,7 +543,8 @@ export const option = {
prop: 'projectName',
formslot: true,
search: true,
- searchLabel: '项目',
+ searchLabel: '项目名称',
+ searchPlaceholder: '请选择或输入',
searchOrder: 22,
span: 6,
order: 890,
@@ -555,6 +556,7 @@ export const option = {
prop: 'customerName',
search: true,
searchLabel: '客户名称',
+ searchPlaceholder: '请选择或输入',
searchOrder: 21,
minWidth: 150,
addDisplay: false,
@@ -576,6 +578,7 @@ export const option = {
prop: 'driverName',
search: true,
searchLabel: '司机名称',
+ searchPlaceholder: '请选择或输入',
searchOrder: 14,
minWidth: 120,
display: false,
@@ -626,6 +629,7 @@ export const option = {
prop: 'carrierName',
search: true,
searchLabel: '承运商名称',
+ searchPlaceholder: '请选择或输入',
searchOrder: 15,
minWidth: 150,
display: false,
@@ -729,6 +733,7 @@ export const option = {
prop: 'planName',
formslot: true,
search: true,
+ searchPlaceholder: '请选择或输入',
searchOrder: 7,
span: 6,
order: 870,
@@ -738,6 +743,7 @@ export const option = {
label: '货物类型',
prop: 'cargoType',
search: true,
+ searchPlaceholder: '请选择或输入',
searchOrder: 18,
formatter: row => formatGoodsField(row, ['cargoType', 'goodsType', 'typeName']),
minWidth: 130,
diff --git a/src/option/system/authlog.js b/src/option/system/authlog.js
index d550790..3e937f1 100644
--- a/src/option/system/authlog.js
+++ b/src/option/system/authlog.js
@@ -5,6 +5,7 @@ export const GRANT_TYPE_DIC = [
{ label: '社交登录', value: 'social' },
{ label: '客户端凭证', value: 'client_credentials' },
{ label: '刷新令牌', value: 'refresh_token' },
+ { label: '退出登录', value: 'logout' },
];
export const authLogOption = {
@@ -38,7 +39,7 @@ export const authLogOption = {
prop: 'realName',
},
{
- label: '授权类型',
+ label: '操作类型',
prop: 'grantType',
type: 'select',
search: true,
@@ -80,7 +81,7 @@ export const authLogOption = {
width: 120,
},
{
- label: '登录时间',
+ label: '操作时间',
prop: 'loginTime',
sortable: true,
span: 24,
diff --git a/src/option/system/user.js b/src/option/system/user.js
index 197198b..9cc07c3 100644
--- a/src/option/system/user.js
+++ b/src/option/system/user.js
@@ -1,12 +1,9 @@
import { getDeptLazyTree } from '@/api/system/dept';
+import { validateLoginPassword } from '@/utils/validate';
export const userOption = safe => {
const validatePass = (rule, value, callback) => {
- if (value === '') {
- callback(new Error('请输入密码'));
- } else {
- callback();
- }
+ validateLoginPassword(rule, value, callback);
};
const validatePass2 = (rule, value, callback) => {
if (value === '') {
diff --git a/src/page/index/layout.vue b/src/page/index/layout.vue
index 0439730..f0531f9 100644
--- a/src/page/index/layout.vue
+++ b/src/page/index/layout.vue
@@ -1,7 +1,18 @@
-
+
+
+
diff --git a/src/page/login/facelogin.vue b/src/page/login/facelogin.vue
index e1da201..0c09cb7 100644
--- a/src/page/login/facelogin.vue
+++ b/src/page/login/facelogin.vue
@@ -13,8 +13,8 @@ export default {
data() {
return {
loginForm: {
- username: 'admin',
- password: '123456',
+ username: '',
+ password: '',
},
};
},
diff --git a/src/page/login/index.vue b/src/page/login/index.vue
index a9ee607..0ab99ca 100644
--- a/src/page/login/index.vue
+++ b/src/page/login/index.vue
@@ -24,6 +24,7 @@
-->
+
@@ -61,7 +63,7 @@ export default {
return {
website: website,
time: '',
- activeName: 'iam',
+ activeName: 'user',
socialForm: {
tenantId: '000000',
source: '',
diff --git a/src/page/login/userlogin.vue b/src/page/login/userlogin.vue
index d981745..c22a330 100644
--- a/src/page/login/userlogin.vue
+++ b/src/page/login/userlogin.vue
@@ -93,9 +93,9 @@ export default {
//角色ID
roleId: '',
//用户名
- username: 'admin',
+ username: '',
//密码
- password: 'admin',
+ password: '',
//账号类型
type: 'account',
//验证码的值
diff --git a/src/permission.js b/src/permission.js
index 27b2028..9051363 100644
--- a/src/permission.js
+++ b/src/permission.js
@@ -3,6 +3,8 @@ import store from './store';
import { tabKeyOf } from '@/router/tab';
import { getToken } from '@/utils/auth';
import {
+ consumeReloadQuery,
+ hasReloadQuery,
isChunkLoadError,
reloadForChunkError,
setPendingRoutePath,
@@ -17,19 +19,20 @@ const lockPage = '/lock'; //锁屏页
router.onError(error => {
if (!isChunkLoadError(error)) return;
if (reloadForChunkError()) {
- ElMessage.warning('页面资源加载失败,正在重新加载…');
- }
-});
-
-window.addEventListener('unhandledrejection', event => {
- if (!isChunkLoadError(event.reason)) return;
- if (reloadForChunkError()) {
- event.preventDefault();
- ElMessage.warning('页面资源加载失败,正在重新加载…');
+ 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;
@@ -59,7 +62,7 @@ router.beforeEach((to, from, next) => {
fullPath: tabKeyOf(to),
params: to.params,
query: to.query,
- meta: meta,
+ meta: { ...meta, keepAlive: true },
});
}
next();
diff --git a/src/router/avue-router.js b/src/router/avue-router.js
index 7c0a38c..5538125 100644
--- a/src/router/avue-router.js
+++ b/src/router/avue-router.js
@@ -2,6 +2,9 @@ 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 跨层级缓存
@@ -96,21 +99,23 @@ RouterPlugin.install = function (option = {}) {
component: (() => {
// 判断是否为首路由
if (first) {
- return modules[
- option.store.getters.isMacOs || !website.setting.menu
- ? '../page/index/layout.vue'
- : '../page/index/index.vue'
- ];
+ return wrapViewLoader(
+ modules[
+ option.store.getters.isMacOs || !website.setting.menu
+ ? '../page/index/layout.vue'
+ : '../page/index/index.vue'
+ ]
+ );
// 判断是否为多层路由
} else if (isChild && !first) {
- return modules['../page/index/layout.vue'];
+ return wrapViewLoader(modules['../page/index/layout.vue']);
// 判断是否为最终的页面视图
} else {
let result = modules[`../${component}.vue`];
if (!result) {
isComponent = false;
}
- return result;
+ return wrapViewLoader(result);
}
})(),
name,
@@ -127,7 +132,7 @@ RouterPlugin.install = function (option = {}) {
if (first) {
oMenu[propsDefault.path] = `${path}`;
let componentPath = oMenu.component || component;
- let result = modules[`../${componentPath}.vue`];
+ let result = wrapViewLoader(modules[`../${componentPath}.vue`]);
if (!result) {
isComponent = false;
}
@@ -173,7 +178,7 @@ export const formatPath = (ele, first) => {
const icon = ele[propsDefault.icon];
ele[propsDefault.icon] = icon || '';
ele.meta = {
- keepAlive: ele.isOpen === 2,
+ keepAlive: true,
};
const iframeComponent = 'components/iframe/main';
const iframeSrc = href => {
@@ -215,7 +220,7 @@ export const formatPath = (ele, first) => {
ele[propsDefault.children].forEach(child => {
child.component = 'views' + child[propsDefault.path];
child.meta = {
- keepAlive: child.isOpen === 2,
+ keepAlive: true,
};
if (isURL(child[propsDefault.href])) {
let href = child[propsDefault.href];
diff --git a/src/router/page/index.js b/src/router/page/index.js
index 9110d5c..ab93bb9 100644
--- a/src/router/page/index.js
+++ b/src/router/page/index.js
@@ -74,6 +74,82 @@ 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: '/business/waybill-manage/public-view',
+ name: '查看运单信息',
+ component: () => import('@/views/business/waybill-manage-public-view.vue'),
+ meta: {
+ keepAlive: false,
+ isTab: false,
+ isAuth: false,
+ bizType: 'waybill-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: '/',
name: '主页',
diff --git a/src/router/tab.js b/src/router/tab.js
index f715c00..d904ad1 100644
--- a/src/router/tab.js
+++ b/src/router/tab.js
@@ -128,5 +128,5 @@ export function tabView(route, Component) {
};
wrapperMap.set(tabKey, wrapper);
}
- return h(wrapper);
+ return wrapper;
}
diff --git a/src/router/views/index.js b/src/router/views/index.js
index 3a380e6..11d72aa 100644
--- a/src/router/views/index.js
+++ b/src/router/views/index.js
@@ -1,5 +1,8 @@
import Layout from '@/page/index/index.vue';
import Store from '@/store/';
+import { wrapViewLoader } from '@/utils/chunk-reload';
+
+const loadView = loader => wrapViewLoader(loader);
export default [
{
@@ -10,7 +13,7 @@ export default [
path: '',
name: '汇票付款表单',
meta: { keepAlive: false, activeMenu: '/payment/bill-payment' },
- component: () => import('@/views/payment/bill-payment-form.vue'),
+ component: loadView(() => import('@/views/payment/bill-payment-form.vue')),
},
],
},
@@ -22,7 +25,7 @@ export default [
path: '',
name: '汇票台账表单',
meta: { keepAlive: false, activeMenu: '/payment/bill-ledger' },
- component: () => import('@/views/payment/bill-ledger-form.vue'),
+ component: loadView(() => import('@/views/payment/bill-ledger-form.vue')),
},
],
},
@@ -34,7 +37,7 @@ export default [
path: '',
name: '认领记录详情',
meta: { keepAlive: false, activeMenu: '/payment/receipt-claim-record' },
- component: () => import('@/views/payment/receipt-claim-record-form.vue'),
+ component: loadView(() => import('@/views/payment/receipt-claim-record-form.vue')),
},
],
},
@@ -46,7 +49,7 @@ export default [
path: '',
name: '收款流水认领',
meta: { keepAlive: false, activeMenu: '/payment/receipt-flow' },
- component: () => import('@/views/payment/receipt-flow-form.vue'),
+ component: loadView(() => import('@/views/payment/receipt-flow-form.vue')),
},
],
},
@@ -58,7 +61,7 @@ export default [
path: '',
name: '收票登记',
meta: { keepAlive: false, activeMenu: '/payment/invoice-receipt' },
- component: () => import('@/views/payment/invoice-receipt-form.vue'),
+ component: loadView(() => import('@/views/payment/invoice-receipt-form.vue')),
},
],
},
@@ -70,7 +73,7 @@ export default [
path: '',
name: '开票申请',
meta: { keepAlive: false, activeMenu: '/payment/invoice-application' },
- component: () => import('@/views/payment/invoice-application-form.vue'),
+ component: loadView(() => import('@/views/payment/invoice-application-form.vue')),
},
],
},
@@ -82,7 +85,7 @@ export default [
path: '',
name: '付款申请',
meta: { keepAlive: false, activeMenu: '/payment/payment-application' },
- component: () => import('@/views/payment/payment-application-form.vue'),
+ component: loadView(() => import('@/views/payment/payment-application-form.vue')),
},
],
},
@@ -97,7 +100,7 @@ export default [
keepAlive: false,
activeMenu: '/settlement/pre-settlement',
},
- component: () => import('@/views/settlement/pre-settlement-form.vue'),
+ component: loadView(() => import('@/views/settlement/pre-settlement-form.vue')),
},
],
},
@@ -112,7 +115,7 @@ export default [
keepAlive: false,
activeMenu: '/settlement/formal-settlement',
},
- component: () => import('@/views/settlement/formal-settlement-form.vue'),
+ component: loadView(() => import('@/views/settlement/formal-settlement-form.vue')),
},
],
},
@@ -127,7 +130,7 @@ export default [
keepAlive: false,
activeMenu: '/settlement/transport-reconciliation',
},
- component: () => import('@/views/settlement/transport-reconciliation-form.vue'),
+ component: loadView(() => import('@/views/settlement/transport-reconciliation-form.vue')),
},
],
},
@@ -139,7 +142,7 @@ export default [
path: '',
name: '执行凭证批次详情',
meta: { keepAlive: false, activeMenu: '/business/voucher-manage' },
- component: () => import('@/views/business/voucher-manage-detail.vue'),
+ component: loadView(() => import('@/views/business/voucher-manage-detail.vue')),
},
],
},
@@ -151,7 +154,7 @@ export default [
path: '',
name: '新增合同管理',
meta: { keepAlive: false },
- component: () => import('@/views/business/contract-manage.vue'),
+ component: loadView(() => import('@/views/business/contract-manage.vue')),
},
],
},
@@ -163,7 +166,7 @@ export default [
path: '',
name: '合同详情',
meta: { keepAlive: false, activeMenu: '/business/contract-manage' },
- component: () => import('@/views/business/contract-manage.vue'),
+ component: loadView(() => import('@/views/business/contract-manage.vue')),
},
],
},
@@ -175,7 +178,7 @@ export default [
path: '',
name: '新增项目申请',
meta: { keepAlive: false },
- component: () => import('@/views/business/project-apply.vue'),
+ component: loadView(() => import('@/views/business/project-apply.vue')),
},
],
},
@@ -187,7 +190,7 @@ export default [
path: '',
name: '新增编辑运单管理',
meta: { keepAlive: false },
- component: () => import('@/views/business/waybill-manage.vue'),
+ component: loadView(() => import('@/views/business/waybill-manage.vue')),
},
],
},
@@ -199,7 +202,7 @@ export default [
path: '',
name: '运单管理详情',
meta: { keepAlive: false, activeMenu: '/business/waybill-manage' },
- component: () => import('@/views/business/waybill-manage-detail.vue'),
+ component: loadView(() => import('@/views/business/waybill-manage-detail.vue')),
},
],
},
@@ -211,7 +214,7 @@ export default [
path: '',
name: '导入运单',
meta: { keepAlive: false, activeMenu: '/business/waybill-import' },
- component: () => import('@/views/business/waybill-import.vue'),
+ component: loadView(() => import('@/views/business/waybill-import.vue')),
},
],
},
@@ -223,7 +226,7 @@ export default [
path: '',
name: '新建导入运单',
meta: { keepAlive: false, activeMenu: '/business/waybill-import' },
- component: () => import('@/views/business/waybill-import-form.vue'),
+ component: loadView(() => import('@/views/business/waybill-import-form.vue')),
},
],
},
@@ -235,6 +238,18 @@ 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'),
},
],
@@ -247,7 +262,7 @@ export default [
path: '',
name: '新增编辑运输计划',
meta: { keepAlive: false },
- component: () => import('@/views/business/transport-plan.vue'),
+ component: loadView(() => import('@/views/business/transport-plan.vue')),
},
],
},
@@ -259,7 +274,7 @@ export default [
path: '',
name: '导入运输计划',
meta: { keepAlive: false, activeMenu: '/business/transport-plan' },
- component: () => import('@/views/business/transport-plan-import.vue'),
+ component: loadView(() => import('@/views/business/transport-plan-import.vue')),
},
],
},
@@ -271,7 +286,7 @@ export default [
path: '',
name: '计划调度',
meta: { keepAlive: false, activeMenu: '/business/transport-plan' },
- component: () => import('@/views/business/transport-plan-dispatch.vue'),
+ component: loadView(() => import('@/views/business/transport-plan-dispatch.vue')),
},
],
},
@@ -283,7 +298,7 @@ export default [
path: '',
name: '新增编辑运单模板',
meta: { keepAlive: false },
- component: () => import('@/views/business/shipping-template.vue'),
+ component: loadView(() => import('@/views/business/shipping-template.vue')),
},
],
},
@@ -295,7 +310,7 @@ export default [
path: '',
name: '新增客商档案',
meta: { keepAlive: false },
- component: () => import('@/views/vehicle/customer-archive.vue'),
+ component: loadView(() => import('@/views/vehicle/customer-archive.vue')),
},
],
},
@@ -307,7 +322,7 @@ export default [
path: '',
name: '合同变更',
meta: { keepAlive: false },
- component: () => import('@/views/business/contract-manage-change.vue'),
+ component: loadView(() => import('@/views/business/contract-manage-change.vue')),
},
],
},
@@ -323,7 +338,7 @@ export default [
meta: {
i18n: 'dashboard',
},
- component: () => import(/* webpackChunkName: "views" */ '@/views/wel/index.vue'),
+ component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/wel/index.vue')),
},
{
path: 'dashboard',
@@ -332,7 +347,7 @@ export default [
i18n: 'dashboard',
menu: false,
},
- component: () => import(/* webpackChunkName: "views" */ '@/views/wel/dashboard.vue'),
+ component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/wel/dashboard.vue')),
},
],
},
@@ -347,7 +362,7 @@ export default [
meta: {
i18n: 'test',
},
- component: () => import(/* webpackChunkName: "views" */ '@/views/util/test.vue'),
+ component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/util/test.vue')),
},
],
},
@@ -362,8 +377,9 @@ export default [
meta: {
i18n: 'dict',
},
- component: () =>
- import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-horizontal.vue'),
+ component: loadView(() =>
+ import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-horizontal.vue')
+ ),
},
],
},
@@ -378,8 +394,9 @@ export default [
meta: {
i18n: 'dict',
},
- component: () =>
- import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-vertical.vue'),
+ component: loadView(() =>
+ import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-vertical.vue')
+ ),
},
],
},
@@ -394,7 +411,7 @@ export default [
meta: {
i18n: 'info',
},
- component: () => import(/* webpackChunkName: "views" */ '@/views/system/userinfo.vue'),
+ component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/system/userinfo.vue')),
},
],
},
@@ -409,8 +426,9 @@ export default [
meta: {
i18n: 'work',
},
- component: () =>
- import(/* webpackChunkName: "views" */ '@/views/work/process/leave/form.vue'),
+ component: loadView(() =>
+ import(/* webpackChunkName: "views" */ '@/views/work/process/leave/form.vue')
+ ),
},
{
path: 'handle/:taskId/:processInstanceId/:businessId',
@@ -418,8 +436,9 @@ export default [
meta: {
i18n: 'work',
},
- component: () =>
- import(/* webpackChunkName: "views" */ '@/views/work/process/leave/handle.vue'),
+ component: loadView(() =>
+ import(/* webpackChunkName: "views" */ '@/views/work/process/leave/handle.vue')
+ ),
},
{
path: 'detail/:processInstanceId/:businessId',
@@ -427,8 +446,9 @@ export default [
meta: {
i18n: 'work',
},
- component: () =>
- import(/* webpackChunkName: "views" */ '@/views/work/process/leave/detail.vue'),
+ component: loadView(() =>
+ import(/* webpackChunkName: "views" */ '@/views/work/process/leave/detail.vue')
+ ),
},
],
},
diff --git a/src/store/getters.js b/src/store/getters.js
index 3edda43..fb53825 100644
--- a/src/store/getters.js
+++ b/src/store/getters.js
@@ -16,11 +16,8 @@ const getters = {
lockPasswd: state => state.common.lockPasswd,
tagList: state => state.tags.tagList,
tagsKeep: (state, getters) => {
- return getters.tagList
- .filter(ele => {
- return (ele.meta || {}).keepAlive;
- })
- .map(ele => ele.fullPath);
+ // 所有已打开标签均纳入 keep-alive,关闭标签后自动从白名单移除并释放实例
+ return getters.tagList.map(ele => ele.fullPath).filter(Boolean);
},
tagWel: state => state.tags.tagWel,
token: state => state.user.token,
diff --git a/src/styles/common.scss b/src/styles/common.scss
index 892329c..97be467 100644
--- a/src/styles/common.scss
+++ b/src/styles/common.scss
@@ -150,12 +150,15 @@ a {
bottom: 0;
}
.map-picker-content {
- display: flex;
- gap: 12px;
+ position: relative;
+ display: block;
+ min-width: 0;
+ overflow: visible;
> [class$='__map'],
> .address-map {
- flex: 1 1 auto;
+ position: relative;
+ z-index: 1;
width: 100%;
min-width: 0;
box-sizing: border-box;
@@ -165,4 +168,8 @@ a {
height: 100% !important;
}
}
+
+ > .map-search-results {
+ z-index: 2000;
+ }
}
diff --git a/src/utils/carrier-org-resource.js b/src/utils/carrier-org-resource.js
new file mode 100644
index 0000000..65ffbc5
--- /dev/null
+++ b/src/utils/carrier-org-resource.js
@@ -0,0 +1,140 @@
+import { getDetail, getList as getCustomerList } from '@/api/vehicle/customer-archive';
+import { getList as getDriverList } from '@/api/transportCapacity/driver';
+import { getList as getVehicleList } from '@/api/transportCapacity/transport-vehicle';
+
+const orgNameCache = new Map();
+
+const extractRecords = res => {
+ const data = res?.data?.data ?? res?.data ?? res;
+ if (Array.isArray(data)) return data;
+ if (Array.isArray(data?.records)) return data.records;
+ if (Array.isArray(data?.data)) return data.data;
+ return [];
+};
+
+const normalizeOrgName = value => String(value || '').trim();
+
+const uniqueOrgNames = (values = []) => {
+ const seen = new Set();
+ const result = [];
+ values.forEach(value => {
+ const name = normalizeOrgName(value);
+ if (!name || seen.has(name)) return;
+ seen.add(name);
+ result.push(name);
+ });
+ return result;
+};
+
+/**
+ * 解析承运商客商档案上联系人的所属组织(branchName),无联系人时回退客商所属组织。
+ */
+export const resolveCarrierOrganizationNames = async ({ carrierId, carrierName } = {}) => {
+ const id = String(carrierId || '').trim();
+ const name = normalizeOrgName(carrierName);
+ const cacheKey = id || name;
+ if (!cacheKey) return [];
+ if (orgNameCache.has(cacheKey)) return orgNameCache.get(cacheKey);
+
+ let detail = null;
+ if (id) {
+ try {
+ const res = await getDetail(id);
+ detail = res?.data?.data || res?.data || null;
+ } catch (error) {
+ detail = null;
+ }
+ }
+ if (!detail && name) {
+ try {
+ const res = await getCustomerList(1, 20, { fullName: name });
+ const records = extractRecords(res);
+ const matched =
+ records.find(item => normalizeOrgName(item.fullName || item.customerName) === name) ||
+ records[0];
+ if (matched?.id) {
+ const detailRes = await getDetail(matched.id);
+ detail = detailRes?.data?.data || detailRes?.data || matched;
+ }
+ } catch (error) {
+ detail = null;
+ }
+ }
+
+ const contacts = Array.isArray(detail?.contacts) ? detail.contacts : [];
+ const orgNames = uniqueOrgNames([
+ ...contacts.map(item => item.branchName),
+ detail?.deptName,
+ detail?.organizationName,
+ ]);
+ orgNameCache.set(cacheKey, orgNames);
+ if (id && name) orgNameCache.set(name, orgNames);
+ return orgNames;
+};
+
+export const clearCarrierOrganizationCache = (carrier = {}) => {
+ const id = String(carrier.carrierId || '').trim();
+ const name = normalizeOrgName(carrier.carrierName);
+ if (id) orgNameCache.delete(id);
+ if (name) orgNameCache.delete(name);
+};
+
+export const matchOrganizationName = (value, orgNames = []) => {
+ const text = normalizeOrgName(value);
+ if (!text || !orgNames.length) return false;
+ return orgNames.some(org => text === org || text.includes(org) || org.includes(text));
+};
+
+const filterByOrganizations = (records = [], orgNames = []) => {
+ if (!orgNames.length) return [];
+ return records.filter(item => matchOrganizationName(item.organizationName, orgNames));
+};
+
+/**
+ * 按承运商联系人所属组织筛选司机。
+ * 未选择承运商时返回空列表(需先选承运商)。
+ */
+export const fetchDriversByCarrierOrganizations = async (
+ query = {},
+ { carrierId, carrierName } = {}
+) => {
+ if (!String(carrierId || '').trim() && !normalizeOrgName(carrierName)) {
+ return [];
+ }
+ const orgNames = await resolveCarrierOrganizationNames({ carrierId, carrierName });
+ if (!orgNames.length) return [];
+
+ const size = Math.max(Number(query.size) || 50, 50);
+ const params = { ...query };
+ delete params.size;
+ // 单组织时用后端模糊条件缩小范围;多组织再前端精确过滤
+ if (orgNames.length === 1) {
+ params.organizationName = orgNames[0];
+ }
+ const res = await getDriverList(1, Math.min(size * 5, 200), params);
+ return filterByOrganizations(extractRecords(res), orgNames).slice(0, size);
+};
+
+/**
+ * 按承运商联系人所属组织筛选车辆。
+ * 未选择承运商时返回空列表。
+ */
+export const fetchVehiclesByCarrierOrganizations = async (
+ query = {},
+ { carrierId, carrierName } = {}
+) => {
+ if (!String(carrierId || '').trim() && !normalizeOrgName(carrierName)) {
+ return [];
+ }
+ const orgNames = await resolveCarrierOrganizationNames({ carrierId, carrierName });
+ if (!orgNames.length) return [];
+
+ const size = Math.max(Number(query.size) || 50, 50);
+ const params = { ...query };
+ delete params.size;
+ if (orgNames.length === 1) {
+ params.organizationName = orgNames[0];
+ }
+ const res = await getVehicleList(1, Math.min(size * 5, 200), params);
+ return filterByOrganizations(extractRecords(res), orgNames).slice(0, size);
+};
diff --git a/src/utils/chunk-reload.js b/src/utils/chunk-reload.js
index 2d42f54..3c30bb4 100644
--- a/src/utils/chunk-reload.js
+++ b/src/utils/chunk-reload.js
@@ -1,38 +1,168 @@
/**
* 懒加载 chunk / CSS preload 失败后的整页恢复。
* 常见于:部署后旧 hash 失效、静态服务空闲断连、代理 CONNECTION_RESET。
+ *
+ * 失败后必须整页刷新:旧入口里的 hashed 资源地址不会自行更新,
+ * 仅捕获路由错误而不刷新时,未打开过的页面会一直无法进入。
*/
const RELOAD_FLAG = 'app:chunk-reload-ts';
-const RELOAD_COOLDOWN_MS = 10000;
+const RELOAD_QUERY = '_chunkreload';
+const RELOAD_COOLDOWN_MS = 15000;
const CHUNK_ERROR_RE =
- /Failed to fetch dynamically imported module|Importing a module script failed|Unable to preload CSS|error loading dynamically imported module|Loading CSS chunk|Loading chunk .+ failed|ChunkLoadError/i;
+ /Failed to fetch dynamically imported module|Importing a module script failed|Unable to preload CSS|error loading dynamically imported module|Loading CSS chunk|Loading chunk .+ failed|ChunkLoadError|Unable to preload|error loading module|Load failed/i;
/** 最近一次路由跳转目标,供 onError 时整页落到正确地址 */
let pendingFullPath = '';
+let installed = false;
+let reloading = false;
export function setPendingRoutePath(fullPath = '') {
pendingFullPath = fullPath || '';
}
+function collectErrorText(error, depth = 0) {
+ if (!error || depth > 3) return '';
+ if (typeof error === 'string') return error;
+ const parts = [
+ error.message,
+ error.msg,
+ error.name,
+ error.stack,
+ error.error && collectErrorText(error.error, depth + 1),
+ error.reason && collectErrorText(error.reason, depth + 1),
+ error.cause && collectErrorText(error.cause, depth + 1),
+ error.payload && collectErrorText(error.payload, depth + 1),
+ ];
+ const target = error.target;
+ if (target && (target.src || target.href)) {
+ parts.push(target.src || target.href);
+ }
+ try {
+ parts.push(String(error));
+ } catch (e) {
+ /* ignore */
+ }
+ return parts.filter(Boolean).join(' ');
+}
+
export function isChunkLoadError(error) {
- if (!error) return false;
- const message = error.message || String(error);
- return CHUNK_ERROR_RE.test(message);
+ const text = collectErrorText(error);
+ if (!text) return false;
+ if (CHUNK_ERROR_RE.test(text)) return true;
+ return /Failed to fetch/i.test(text) && /\.m?js(\?|$|:)/i.test(text);
+}
+
+export function hasReloadQuery(query = {}) {
+ return !!(query && query[RELOAD_QUERY] !== undefined);
+}
+
+export function consumeReloadQuery(query = {}) {
+ if (!hasReloadQuery(query)) return query;
+ const next = { ...query };
+ delete next[RELOAD_QUERY];
+ return next;
+}
+
+function buildReloadUrl(targetPath) {
+ const url = new URL(targetPath, window.location.origin);
+ url.searchParams.set(RELOAD_QUERY, String(Date.now()));
+ return url.pathname + url.search + url.hash;
}
/**
* @returns {boolean} 是否已触发刷新(冷却期内返回 false,避免死循环)
*/
export function reloadForChunkError(targetPath) {
+ if (reloading) return false;
const now = Date.now();
const last = Number(sessionStorage.getItem(RELOAD_FLAG) || 0);
if (now - last < RELOAD_COOLDOWN_MS) {
return false;
}
+ reloading = true;
sessionStorage.setItem(RELOAD_FLAG, String(now));
- const path = targetPath || pendingFullPath || window.location.pathname + window.location.search + window.location.hash;
- window.location.assign(path);
+ const path =
+ targetPath ||
+ pendingFullPath ||
+ `${window.location.pathname}${window.location.search}${window.location.hash}`;
+ const dest = buildReloadUrl(path);
+ // cache: 'reload' 会回写 HTTP 缓存,降低 index.html 仍指向旧 hash 资源的概率
+ fetch(`${window.location.pathname}?${RELOAD_QUERY}=${now}`, {
+ cache: 'reload',
+ credentials: 'same-origin',
+ headers: {
+ Pragma: 'no-cache',
+ 'Cache-Control': 'no-cache',
+ },
+ })
+ .catch(() => {})
+ .finally(() => {
+ window.location.replace(dest);
+ });
return true;
}
+
+export function wrapViewLoader(loader) {
+ if (typeof loader !== 'function') return loader;
+ return () =>
+ Promise.resolve()
+ .then(() => loader())
+ .catch(error => {
+ if (isChunkLoadError(error)) {
+ reloadForChunkError();
+ }
+ throw error;
+ });
+}
+
+export function installChunkReload() {
+ if (installed || typeof window === 'undefined') return;
+ installed = true;
+
+ window.addEventListener('vite:preloadError', event => {
+ if (reloadForChunkError()) {
+ event.preventDefault();
+ }
+ });
+
+ window.addEventListener('unhandledrejection', event => {
+ if (!isChunkLoadError(event.reason)) return;
+ if (reloadForChunkError()) {
+ event.preventDefault();
+ }
+ });
+
+ window.addEventListener(
+ 'error',
+ event => {
+ const el = event.target;
+ const isAssetNode =
+ el &&
+ el !== window &&
+ (el.tagName === 'SCRIPT' ||
+ (el.tagName === 'LINK' && /modulepreload|stylesheet/i.test(el.rel || '')));
+ if (isAssetNode || isChunkLoadError(event.error || event.message)) {
+ if (reloadForChunkError()) {
+ event.preventDefault();
+ }
+ }
+ },
+ true
+ );
+
+ document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState === 'visible') checkDeployedAssets();
+ });
+}
+
+function checkDeployedAssets() {
+ const el = document.querySelector('script[type="module"][src*="/assets/"]');
+ if (!el || !el.src) return;
+ fetch(el.src, { method: 'HEAD', cache: 'no-store', credentials: 'same-origin' })
+ .then(res => {
+ if (res.status === 404) reloadForChunkError();
+ })
+ .catch(() => {});
+}
diff --git a/src/utils/map-search.js b/src/utils/map-search.js
new file mode 100644
index 0000000..a2bf1dd
--- /dev/null
+++ b/src/utils/map-search.js
@@ -0,0 +1,39 @@
+/**
+ * 将高德 Geocoder 结果规范化为地图选址浮层列表数据。
+ * @param {Array|Object} geocodesOrResult geocodes 数组,或含 geocodes/location 的结果对象
+ * @param {string} keyword 搜索关键词兜底文案
+ * @returns {Array<{id: string|number, name: string, address: string, location: any, photo: string}>}
+ */
+export function normalizeMapSearchResults(geocodesOrResult, keyword = '') {
+ const fallback = String(keyword || '').trim();
+ let list = [];
+ if (Array.isArray(geocodesOrResult)) {
+ list = geocodesOrResult;
+ } else if (geocodesOrResult && typeof geocodesOrResult === 'object') {
+ if (Array.isArray(geocodesOrResult.geocodes)) {
+ list = geocodesOrResult.geocodes;
+ } else if (geocodesOrResult.location) {
+ list = [geocodesOrResult];
+ }
+ }
+
+ return list.map((item, index) => {
+ const buildingName = pickNamedField(item?.building);
+ const neighborhoodName = pickNamedField(item?.neighborhood);
+ const address = item?.formattedAddress || item?.address || fallback || '-';
+ return {
+ id: item?.id || index,
+ name: buildingName || neighborhoodName || address,
+ address,
+ location: item?.location,
+ photo: item?.photo || item?.image || '',
+ };
+ });
+}
+
+function pickNamedField(value) {
+ if (!value) return '';
+ if (typeof value === 'string') return value.trim();
+ if (typeof value === 'object' && value.name) return String(value.name).trim();
+ return '';
+}
diff --git a/src/utils/mk-approval.js b/src/utils/mk-approval.js
new file mode 100644
index 0000000..5b02da7
--- /dev/null
+++ b/src/utils/mk-approval.js
@@ -0,0 +1,94 @@
+import { getDictionary } from '@/api/system/dictbiz';
+import { processDelete, processSubmit } from '@/api/system/business-process';
+import { ElMessage } from 'element-plus';
+
+export const MK_BIZ = {
+ 'customer-archive': {
+ dictName: '提交客商审核流',
+ subjectPrefix: '客商准入审批',
+ rejected: ['rejected'],
+ },
+ 'project-apply': {
+ dictName: '提交项目审核流',
+ subjectPrefix: '项目审批',
+ rejected: ['rejected', 'change_rejected', 'withdrawn'],
+ },
+ 'contract-manage': {
+ dictName: '提交合同审核流',
+ subjectPrefix: '合同审批',
+ rejected: ['rejected', 'change_rejected', 'withdrawn'],
+ },
+ 'waybill-manage': {
+ dictName: '提交运单审核流',
+ subjectPrefix: '运单审批',
+ rejected: ['rejected', 'returned'],
+ },
+ 'pre-settlement': {
+ dictName: '提交预结算审核流',
+ subjectPrefix: '预结算审批',
+ rejected: ['returned'],
+ },
+ 'formal-settlement': {
+ dictName: '提交正式结算审核流',
+ subjectPrefix: '正式结算审批',
+ rejected: ['returned'],
+ },
+ 'payment-application': {
+ dictName: '提交付款审核流',
+ subjectPrefix: '付款审批',
+ rejected: ['returned'],
+ },
+};
+
+const MK_SWITCH_NAME = '开启MK';
+
+async function loadMkTemplateList() {
+ const res = await getDictionary({ code: 'mk_template' });
+ return res?.data?.data || [];
+}
+
+/** 业务字典 mk_template:名称「开启MK」且键值为 1 时才请求 MK */
+export function isMkEnabled(list = []) {
+ const matched = list.find(item => String(item.dictValue || '').trim() === MK_SWITCH_NAME);
+ return String(matched?.dictKey ?? '').trim() === '1';
+}
+
+export async function resolveMkTemplateCode(dictName, list) {
+ const dictList = list || (await loadMkTemplateList());
+ const matched = dictList.find(item => String(item.dictValue || '').trim() === dictName);
+ const templateCode = matched?.dictKey;
+ if (!templateCode) {
+ ElMessage.warning(`未配置业务字典 mk_template「${dictName}」,无法提交审核流`);
+ return Promise.reject(new Error(`未配置业务字典 mk_template「${dictName}」`));
+ }
+ return String(templateCode);
+}
+
+export async function submitMkApprovalFlow({
+ bizType,
+ formInstanceId,
+ subjectName = '',
+ approvalStatus = '',
+}) {
+ const conf = MK_BIZ[bizType];
+ if (!conf) {
+ return Promise.reject(new Error(`不支持的MK业务类型:${bizType}`));
+ }
+ const list = await loadMkTemplateList();
+ if (!isMkEnabled(list)) {
+ return;
+ }
+ if ((conf.rejected || []).includes(approvalStatus)) {
+ await processDelete({ formInstanceId: String(formInstanceId) });
+ }
+ const templateCode = await resolveMkTemplateCode(conf.dictName, list);
+ const subject = subjectName
+ ? `${conf.subjectPrefix}:${subjectName}`
+ : `${conf.subjectPrefix}:${formInstanceId}`;
+ return processSubmit({
+ templateCode,
+ formInstanceId: String(formInstanceId),
+ subject,
+ bizType,
+ });
+}
diff --git a/src/utils/validate.js b/src/utils/validate.js
index ccba6e7..6aed154 100644
--- a/src/utils/validate.js
+++ b/src/utils/validate.js
@@ -283,3 +283,31 @@ export function validatejson(val) {
// 非对象、非数组、非字符串,或者字符串不是 JSON
return false;
}
+
+/** 登录密码规则提示 */
+export const LOGIN_PASSWORD_RULE_MESSAGE =
+ '密码须大于8位,且同时包含字母、数字和特殊字符(.!@#$%^&*)';
+
+/**
+ * 校验登录密码强度:大于8位,同时包含字母、数字、特殊字符(.!@#$%^&*)
+ * @param {string} password
+ * @returns {boolean}
+ */
+export function isValidLoginPassword(password) {
+ return /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.!@#$%^&*]).{9,}$/.test(String(password || ''));
+}
+
+/**
+ * Element Plus / Avue 表单校验器:登录密码强度
+ */
+export function validateLoginPassword(rule, value, callback) {
+ if (value === undefined || value === null || String(value).trim() === '') {
+ callback(new Error('请输入登录密码'));
+ return;
+ }
+ if (!isValidLoginPassword(value)) {
+ callback(new Error(LOGIN_PASSWORD_RULE_MESSAGE));
+ return;
+ }
+ callback();
+}
diff --git a/src/views/base/airport-master.vue b/src/views/base/airport-master.vue
index ad78455..4a2ba77 100644
--- a/src/views/base/airport-master.vue
+++ b/src/views/base/airport-master.vue
@@ -94,6 +94,17 @@
/>
+
+
+
+
@@ -134,6 +152,7 @@ import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
+import AddressMapPicker from '@/components/address-map-picker/main.vue';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
@@ -181,6 +200,9 @@ const addExcelRequiredHeaderMarks = async blob => {
};
export default {
+ components: {
+ AddressMapPicker,
+ },
data() {
const validateIataCode = (rule, value, callback) => {
if (!/^[A-Z]{3}$/.test(String(value || '').toUpperCase())) {
@@ -220,6 +242,7 @@ export default {
loading: true,
data: [],
excelBox: false,
+ addressMapPickerVisible: false,
excelForm: {},
provinceOptions: [],
cityOptions: [],
@@ -368,15 +391,14 @@ export default {
{
label: '详细地址',
prop: 'detailAddress',
- type: 'textarea',
- minRows: 2,
+ formslot: true,
span: 24,
minWidth: 220,
overHidden: false,
maxlength: 255,
showWordLimit: true,
rules: [
- { required: true, message: '请输入详细地址', trigger: 'blur' },
+ { required: true, message: '请选择详细地址', trigger: 'change' },
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
],
},
@@ -504,7 +526,7 @@ export default {
propsHttp: {
res: 'data',
},
- tip: '请上传 .xls,.xlsx 标准格式文件',
+ tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
accept: '.xls,.xlsx',
action: '/blade-system/airport-master/import-airport-master',
},
@@ -669,6 +691,29 @@ export default {
handleCoordinateInput(prop, value) {
this.form[prop] = normalizeCoordinateInput(value);
},
+ openAddressMapPicker() {
+ this.addressMapPickerVisible = true;
+ },
+ handleAddressMapConfirm(payload) {
+ const selection = typeof payload === 'string' ? { address: payload } : payload || {};
+ if (selection.address) {
+ this.form.detailAddress = selection.address;
+ }
+ if (
+ selection.longitude !== undefined &&
+ selection.longitude !== null &&
+ selection.longitude !== ''
+ ) {
+ this.form.longitude = Number(selection.longitude).toFixed(6);
+ }
+ if (
+ selection.latitude !== undefined &&
+ selection.latitude !== null &&
+ selection.latitude !== ''
+ ) {
+ this.form.latitude = Number(selection.latitude).toFixed(6);
+ }
+ },
normalizeRow(row) {
row.code = row.iataCode ? `JC-${row.iataCode}` : row.code;
row.regionCode = String(row.regionCode || '').trim();
@@ -917,4 +962,13 @@ export default {
white-space: nowrap;
}
}
+
+.detail-address-input {
+ cursor: pointer;
+
+ :deep(.el-input__wrapper),
+ :deep(.el-input__inner) {
+ cursor: pointer;
+ }
+}
diff --git a/src/views/base/cargo-type.vue b/src/views/base/cargo-type.vue
index cdc487f..b1f783b 100644
--- a/src/views/base/cargo-type.vue
+++ b/src/views/base/cargo-type.vue
@@ -274,7 +274,7 @@ export default {
res: 'data',
},
headers: getUploadHeaders(),
- tip: '请上传 .xls,.xlsx 标准格式文件',
+ tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
action: '/blade-system/cargo-type/import-cargo-type',
},
{
diff --git a/src/views/base/common-address.vue b/src/views/base/common-address.vue
index cc31086..724cb2a 100644
--- a/src/views/base/common-address.vue
+++ b/src/views/base/common-address.vue
@@ -233,8 +233,8 @@
{{ mapStatus }}
@@ -270,6 +270,7 @@ import { createOption } from '@/option/base/common-address';
import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
import { getToken } from '@/utils/auth';
+import { normalizeMapSearchResults } from '@/utils/map-search';
import { isMobile } from '@/utils/validate';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
@@ -1117,12 +1118,7 @@ export default {
this.ensureAmapGeocoder()
.then(() => this.runAmapGeocode('location', keyword))
.then(result => {
- this.mapSearchResults = (result.geocodes || []).map((item, index) => ({
- id: item.id || index,
- name: item.formattedAddress || keyword,
- address: item.formattedAddress || keyword,
- location: item.location,
- }));
+ this.mapSearchResults = normalizeMapSearchResults(result, keyword);
const point = this.resolveMapPoint(result);
if (!point) {
this.mapStatus = '未找到匹配地址';
diff --git a/src/views/base/currency.vue b/src/views/base/currency.vue
index f4441bb..0ad39fe 100644
--- a/src/views/base/currency.vue
+++ b/src/views/base/currency.vue
@@ -326,7 +326,7 @@ export default {
propsHttp: {
res: 'data',
},
- tip: '请上传 .xls,.xlsx 标准格式文件',
+ tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
action: '/blade-system/currency/import-currency',
},
{
diff --git a/src/views/base/measurement-unit.vue b/src/views/base/measurement-unit.vue
index 11d1181..ebd7613 100644
--- a/src/views/base/measurement-unit.vue
+++ b/src/views/base/measurement-unit.vue
@@ -119,6 +119,7 @@ export default {
return this.isAdmin || this.permission?.[code] === true;
},
normalizeRow(row) {
+ row.unitCode = String(row.unitCode || '').trim();
row.unitName = String(row.unitName || '').trim();
row.dimension = String(row.dimension || '').trim();
row.remark = String(row.remark || '').trim();
diff --git a/src/views/base/port-terminal.vue b/src/views/base/port-terminal.vue
index 125e786..06ede97 100644
--- a/src/views/base/port-terminal.vue
+++ b/src/views/base/port-terminal.vue
@@ -194,10 +194,11 @@
@@ -244,6 +245,13 @@
+
@@ -264,12 +272,16 @@ import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
+import AddressMapPicker from '@/components/address-map-picker/main.vue';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
const DEFAULT_COUNTRY_CODE = '+86';
const newLocal = '请选择类型';
export default {
+ components: {
+ AddressMapPicker,
+ },
data() {
const validateCode = (rule, value, callback) => {
const code = String(value || '').toUpperCase();
@@ -315,6 +327,7 @@ export default {
loading: true,
data: [],
excelBox: false,
+ addressMapPickerVisible: false,
excelForm: {},
portOptions: [],
countryOptions: [],
@@ -572,11 +585,11 @@ export default {
minWidth: 220,
overHidden: false,
order: 85,
- placeholder: '请输入',
+ placeholder: '点击选择地图地址',
maxlength: 255,
showWordLimit: true,
rules: [
- { required: true, message: '请输入详细地址', trigger: 'blur' },
+ { required: true, message: '请选择详细地址', trigger: 'change' },
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
],
},
@@ -673,7 +686,7 @@ export default {
propsHttp: {
res: 'data',
},
- tip: '请上传 .xls,.xlsx 标准格式文件',
+ tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
accept: '.xls,.xlsx',
action: '/blade-system/port-terminal/import-port-terminal',
},
@@ -1040,6 +1053,21 @@ export default {
handleCoordinateInput(prop, value) {
this.form[prop] = normalizeCoordinateInput(value);
},
+ openAddressMapPicker() {
+ this.addressMapPickerVisible = true;
+ },
+ handleAddressMapConfirm(payload) {
+ const selection = typeof payload === 'string' ? { address: payload } : payload || {};
+ if (selection.address) {
+ this.form.detailAddress = selection.address;
+ }
+ if (selection.longitude !== undefined && selection.longitude !== null && selection.longitude !== '') {
+ this.form.longitude = Number(selection.longitude).toFixed(6);
+ }
+ if (selection.latitude !== undefined && selection.latitude !== null && selection.latitude !== '') {
+ this.form.latitude = Number(selection.latitude).toFixed(6);
+ }
+ },
resolveCountryCode(country) {
if (!country) {
return Promise.resolve('');
@@ -1107,7 +1135,7 @@ export default {
return false;
}
if (isBlank(row.detailAddress)) {
- this.$message.warning('请输入详细地址');
+ this.$message.warning('请选择详细地址');
return false;
}
if (isBlank(row.longitude)) {
@@ -1399,6 +1427,15 @@ export default {
line-height: 20px;
padding: 4px 0;
}
+
+.detail-address-input {
+ cursor: pointer;
+
+ :deep(.el-input__wrapper),
+ :deep(.el-input__inner) {
+ cursor: pointer;
+ }
+}
diff --git a/src/views/business/components/waybill-manage-page.vue b/src/views/business/components/waybill-manage-page.vue
index af185a0..71ea562 100644
--- a/src/views/business/components/waybill-manage-page.vue
+++ b/src/views/business/components/waybill-manage-page.vue
@@ -3,21 +3,21 @@
:class="[
'waybill-manage-page',
{
- 'waybill-manage-page--form-page': isStandaloneWaybillFormPage,
- 'waybill-manage-page--detail-page': isStandaloneWaybillDetailPage,
+ 'waybill-manage-page--form-page': formPageLocked,
+ 'waybill-manage-page--detail-page': detailPageLocked,
},
]"
>
@@ -1747,7 +1747,7 @@
-
+
运单详情
{{ detailRow.waybillNo || '-' }}
- {{
- displayStatus(detailRow, 'businessStatus')
- }}
-
- {{ getTransportTypeLabel(waybillTransportMode(detailRow)) || '-' }}
-
+
+ {{ displayStatus(detailRow, 'businessStatus') }}
+
+
+ {{
+ detailRow.transportTypeName ||
+ getTransportTypeLabel(detailRow.transportType) ||
+ waybillTransportMode(detailRow) ||
+ '-'
+ }}
+
{{ item[1] }}
-
+
+ {{ formatDetailValue(detailRow, item[0]) }}
+
+
{{ formatDetailValue(detailRow, item[0]) }}
@@ -1923,6 +1939,18 @@
}}
+
+ 里程(km){{ waybillDetailMileage(detailRow) || '-' }}
+
+
+ 计划发货日期{{ waybillDetailEstimatedStartTime(detailRow) || '-' }}
+
+
+ 计划完成日期{{ waybillDetailEstimatedEndTime(detailRow) || '-' }}
+
@@ -1943,12 +1971,32 @@
仓位{{ detailRow.cabinNo || '-' }}
+
+ 预计发货日期{{ waybillDetailEstimatedStartTime(detailRow) || '-' }}
+
+
+ 预计完成日期{{ waybillDetailEstimatedEndTime(detailRow) || '-' }}
+
{{ waybillVehicleNoLabel(detailRow) }}
{{ detailRow.vehicleNo || '-' }}
+
+ 里程(km){{ waybillDetailMileage(detailRow) || '-' }}
+
+
+ 计划发货日期{{ waybillDetailEstimatedStartTime(detailRow) || '-' }}
+
+
+ 计划完成日期{{ waybillDetailEstimatedEndTime(detailRow) || '-' }}
+
-
+
-
@@ -2206,21 +2253,113 @@
- 轨迹回放
- 实时定位
+ 实时定位
- 暂无数据
+
+ 时间段
+
+ 查询
+ 轨迹点 {{ waybillTrackInfo?.total || 0 }} 个
+
+
+
+
+ 车牌号{{ waybillLocateInfo.vehicleNo || '-' }}
+
+
+ 定位时间{{ waybillLocateInfo.locateTime || '-' }}
+
+
+ 速度{{ waybillLocateInfo.speed || '-' }}
+
+
+ 方向{{ waybillLocateInfo.direction || '-' }}
+
+
+ 地址{{ waybillLocateInfo.address || '-' }}
+
+
+ 坐标{{
+ waybillLocateInfo.longitude != null && waybillLocateInfo.latitude != null
+ ? `${waybillLocateInfo.longitude}, ${waybillLocateInfo.latitude}`
+ : '-'
+ }}
+
+
+
+
+
+ {{ waybillLocateHint || '暂无数据' }}
+
+
+
+
+
+ {{ waybillTrackHint || '暂无数据' }}
+
+
+ 暂无数据
-
{{ transportMapStatus }}
@@ -3013,6 +3123,7 @@ import {
getList as getProjectList,
} from '@/api/business/project-apply';
import { getList as getLoadingList } from '@/api/business/loading-manage';
+import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive';
import { getList as getCommonRouteList } from '@/api/business/common-route';
import {
getDetail as getShippingPlanDetail,
@@ -3023,20 +3134,26 @@ import {
getList as getProcessConfigList,
getVoucherImages as getProcessConfigVoucherImages,
} from '@/api/business/process-config';
-import { getPunchRecords as getWaybillPunchRecords } from '@/api/business/waybill-manage';
+import { getPunchRecords as getWaybillPunchRecords, locateVehicle, trackVehicle } from '@/api/business/waybill-manage';
+import { getMkPublicDetail } from '@/api/mk-process';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
+import {
+ fetchDriversByCarrierOrganizations,
+} from '@/utils/carrier-org-resource';
import { getDictionary } from '@/api/system/dictbiz';
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { addressTypeOptions } from '@/option/base/common-address';
import { packageOptions } from '@/option/business/common';
import { formatUpdateUserName } from '@/utils/audit';
+import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { getToken } from '@/utils/auth';
import { openImportDialog } from '@/utils/import-excel';
+import { normalizeMapSearchResults } from '@/utils/map-search';
import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate';
-import { InfoFilled, Location, OfficeBuilding, Rank, Search, WarnTriangleFilled } from '@element-plus/icons-vue';
-import { ElImageViewer } from 'element-plus';
+import { InfoFilled, Location, OfficeBuilding, Search, WarnTriangleFilled } from '@element-plus/icons-vue';
+import { ElAutocomplete, ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue';
import {
fallbackPlugin,
@@ -3211,7 +3328,6 @@ export default {
PageDetail,
InfoFilled,
WarnTriangleFilled,
- Rank,
ElImageViewer,
OpenFileViewer,
},
@@ -3284,8 +3400,22 @@ export default {
waybillRouteChangeNodes: [],
waybillRouteChangeRemark: '',
waybillRouteChangeRecords: [],
- waybillRouteChangeDragIndex: -1,
waybillRouteChangeSaving: false,
+ waybillLocateLoading: false,
+ waybillLocateInfo: null,
+ waybillLocateHint: '',
+ waybillLocateMapInstance: null,
+ waybillLocateMarker: null,
+ waybillLocateInfoWindow: null,
+ waybillTrackMode: '',
+ waybillTrackLoading: false,
+ waybillTrackInfo: null,
+ waybillTrackHint: '',
+ waybillTrackDateRange: [],
+ waybillTrackMapInstance: null,
+ waybillTrackPolyline: null,
+ waybillTrackStartMarker: null,
+ waybillTrackEndMarker: null,
mileageDialog: {
visible: false,
submitting: false,
@@ -3512,9 +3642,28 @@ export default {
selectionList: [],
attachmentUploadMode: false,
standaloneFormKey: '',
+ // 实例所属路由的路径快照:用于判断当前 $route 是否还是「本实例自己的路由」。
+ // 必须在 data 里初始化,不能放 created —— Vue 的 Options API 执行顺序是
+ // data → computed → watch(immediate) → created,放 created 会被首个
+ // immediate watcher 读到空值,把本该执行的首次加载也挡掉。
+ routePathLocked: this.$route.path,
+ // 页面形态(列表 / 独立表单页 / 独立详情页)在实例创建时锁定一次,之后不再随 $route 变化。
+ // 原因:标签页 keep-alive 只会让实例 deactivate,实例仍会随 $route(全局响应式)重新渲染;
+ // 若形态跟着路由翻回列表/弹窗态,缓存实例会渲染出 append-to-body 的 el-dialog —— 弹窗被
+ // Teleport 到 body 后,KeepAlive.deactivate 的 move 搬不动它,DOM 会永久残留在页面上,
+ // 表现为“从运单详情/独立表单离开后点其它菜单,详情弹窗又冒出来”。
+ formPageLocked: false,
+ detailPageLocked: false,
+ // 同上:表单模式(add / edit)也一次性锁定,避免缓存实例随 $route 变 query 导致 option 变形
+ formModeLocked: '',
};
},
created() {
+ this.formPageLocked = this.isStandaloneWaybillFormPage;
+ this.detailPageLocked = this.isStandaloneWaybillDetailPage;
+ this.formModeLocked = this.$route.query.mode || '';
+ if (this.isPublicWaybillView) return;
+ this.setupSearchAutocompletes();
if (this.config.enableAllDept && this.isAdmin) {
this.allDept = 1;
}
@@ -3538,6 +3687,14 @@ export default {
this.consumeTemplateCreatePayload();
}
},
+ // 标签切走(实例被 keep-alive 缓存)或销毁时,收起本页所有 append-to-body 的弹窗。
+ // 否则 Teleport 出去的弹窗 DOM 会一直留在 body 上,盖住后续打开的页面。
+ deactivated() {
+ this.closeInnerDialogs();
+ },
+ beforeUnmount() {
+ this.closeInnerDialogs();
+ },
computed: {
...mapGetters(['permission', 'userInfo']),
permissionList() {
@@ -3545,17 +3702,49 @@ export default {
addBtn: this.canCreate,
};
},
+ // 打卡时间轴按打卡时间先后展示:已打卡记录按打卡时间升序在前,
+ // 未打卡记录保持流程节点原顺序排在其后。
+ orderedWaybillPunchRecords() {
+ const records = Array.isArray(this.waybillPunchRecords) ? this.waybillPunchRecords : [];
+ return records
+ .map((record, index) => ({
+ record,
+ index,
+ punchedAt: this.waybillPunchRecordTimestamp(record),
+ }))
+ .sort((a, b) => {
+ const aPunched = a.punchedAt !== null;
+ const bPunched = b.punchedAt !== null;
+ if (aPunched && bPunched) return a.punchedAt - b.punchedAt || a.index - b.index;
+ if (aPunched !== bPunched) return aPunched ? -1 : 1;
+ return a.index - b.index;
+ })
+ .map(item => item.record);
+ },
isStandaloneWaybillPage() {
return this.standaloneFormPage && standaloneWaybillFormPaths.includes(this.$route.path);
},
isStandaloneWaybillFormPage() {
return this.isStandaloneWaybillPage && ['add', 'edit'].includes(this.$route.query.mode);
},
+ isPublicWaybillView() {
+ return this.$route.path === '/business/waybill-manage/public-view';
+ },
isStandaloneWaybillDetailPage() {
- return this.standaloneDetailPage && this.$route.path === standaloneWaybillDetailRoute;
+ return (
+ (this.standaloneDetailPage && this.$route.path === standaloneWaybillDetailRoute) ||
+ this.isPublicWaybillView
+ );
+ },
+ // 容器形态读实例创建时锁定的标志,不随 $route 翻转(详见 data 中 formPageLocked 的说明)
+ // 当前路由是否仍属于本实例(tab.js 按 fullPath 建实例,一个实例只对应一个 path)。
+ // 被 keep-alive 缓存后如果 $route 已经跑到别的页面上,就不该再用「当前页面的 id」
+ // 去触发本实例的请求或跳转,否则会拿别的单据 id 查自己的接口。
+ isOwnedRouteActive() {
+ return this.$route.path === this.routePathLocked;
},
detailContainer() {
- return this.isStandaloneWaybillDetailPage ? 'PageDetail' : 'el-dialog';
+ return this.detailPageLocked ? 'PageDetail' : 'el-dialog';
},
formPageTitle() {
if (this.isStandaloneWaybillFormPage) {
@@ -3567,13 +3756,13 @@ export default {
return '';
},
crudContainer() {
- return this.isStandaloneWaybillFormPage ? 'PageAvueForm' : 'avue-crud';
+ return this.formPageLocked ? 'PageAvueForm' : 'avue-crud';
},
pageFormOption() {
- if (!this.isStandaloneWaybillFormPage) return this.option;
+ if (!this.formPageLocked) return this.option;
const base = {
...this.option,
- boxType: this.$route.query.mode === 'edit' ? 'edit' : 'add',
+ boxType: this.formModeLocked === 'edit' ? 'edit' : 'add',
menuBtn: true,
submitBtn: true,
emptyBtn: false,
@@ -3821,6 +4010,10 @@ export default {
detailId: {
immediate: true,
handler(id) {
+ // 宿主 view 传的是全局 $route.query.id / detailId,实例被缓存后仍会被重渲染,
+ // 必须确认当前路由还是自己的:否则跳到别的详情页时会把「别的单据 id」灌进来,
+ // 触发 openDetail 用错 id 查运单 → 报「运单管理不存在」。
+ if (!this.isOwnedRouteActive) return;
if (id !== undefined && id !== null && id !== '') {
this.$nextTick(() => this.openDetail({ id }));
}
@@ -3837,6 +4030,25 @@ export default {
},
},
methods: {
+ // 页面被 keep-alive 缓存/卸载时调用,关闭本页所有 append-to-body 的弹窗,
+ // 避免 Teleport 出去的 DOM 残留在 body 上盖住后续页面(详见 data 里的说明)。
+ closeInnerDialogs() {
+ this.detailBox = false;
+ this.mileageDialog.visible = false;
+ this.attachmentDocumentPreviewVisible = false;
+ this.attachmentImagePreviewVisible = false;
+ this.waybillRouteChangeBox = false;
+ this.waybillCommonAddressBox = false;
+ this.flowBox = false;
+ this.transportRouteBox = false;
+ this.transportAddressBox = false;
+ this.transportStationBox = false;
+ this.transportMapBox = false;
+ this.commonCargoBox = false;
+ this.cargoImportBox = false;
+ this.waybillProcessConfigBox = false;
+ this.excelBox = false;
+ },
initStandaloneFormPage() {
if (!this.isStandaloneWaybillFormPage) return;
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
@@ -4110,8 +4322,12 @@ export default {
) {
return false;
}
+ // 进行中的运单隐藏取消(与 config.canCancel / isInProgressStatus 对齐)
+ if (typeof this.config.canCancel === 'function' && !this.config.canCancel(row)) {
+ return false;
+ }
const status = this.statusValue(row);
- return ['pending', 'processing', 'running'].includes(status);
+ return status === 'pending';
},
canComplete(row) {
if (
@@ -4342,6 +4558,42 @@ export default {
waybillIsCarrier(row = {}) {
return String(row.carrierTypeName || row.carrierType || '').trim() === '承运商';
},
+ waybillDetailTaskInfo(row = {}) {
+ return this.parseJsonObject(row.taskInfoJson || row.taskInfo || {});
+ },
+ waybillDetailMileage(row = {}) {
+ const taskInfo = this.waybillDetailTaskInfo(row);
+ const goodsRows = this.parseJsonArray(row.goodsJson);
+ const firstGoods = goodsRows[0] || {};
+ return this.normalizeMileageValue(
+ row.mileage || taskInfo.mileage || firstGoods.mileage || ''
+ );
+ },
+ waybillDetailEstimatedStartTime(row = {}) {
+ const taskInfo = this.waybillDetailTaskInfo(row);
+ const value =
+ row.estimatedStartTime ||
+ row.planStartDate ||
+ taskInfo.estimatedStartTime ||
+ taskInfo.planStartDate ||
+ '';
+ return this.formatWaybillDetailDate(value);
+ },
+ waybillDetailEstimatedEndTime(row = {}) {
+ const taskInfo = this.waybillDetailTaskInfo(row);
+ const value =
+ row.estimatedEndTime ||
+ row.planEndDate ||
+ taskInfo.estimatedEndTime ||
+ taskInfo.planEndDate ||
+ '';
+ return this.formatWaybillDetailDate(value);
+ },
+ formatWaybillDetailDate(value) {
+ const text = String(value || '').trim();
+ if (!text) return '';
+ return text.slice(0, 10);
+ },
waybillVehicleNoLabel(row = {}) {
return this.waybillIsRoadTransport(row) ? '车牌号' : '船/航/班列号';
},
@@ -4384,6 +4636,9 @@ export default {
},
openDetail(row) {
if (!this.isStandaloneWaybillDetailPage) {
+ // 已不在自己的路由上(被缓存后路由切走):此时 row.id 极可能是别的单据的 id,
+ // 再 push 会生成一堆错误的「运单管理详情」标签
+ if (!this.isOwnedRouteActive) return;
this.$router.push({ path: standaloneWaybillDetailRoute, query: { id: row.id } });
return;
}
@@ -4400,14 +4655,19 @@ export default {
this.waybillVoucherFolder = null;
this.waybillVoucherFolderView = false;
this.waybillVoucherImagesLoaded = false;
- const request =
- typeof this.api.getDetail === 'function'
+ const request = this.isPublicWaybillView
+ ? getMkPublicDetail('waybill-manage', row.id)
+ : typeof this.api.getDetail === 'function'
? this.api.getDetail(row.id)
: Promise.resolve({ data: { data: row } });
request
.then(res => {
const detail = res?.data?.data || res?.data || row;
this.detailRow = detail;
+ if (this.isPublicWaybillView) {
+ this.applyFormDetail(detail);
+ return;
+ }
this.loadWaybillPunchRecords();
this.loadWaybillVoucherImages();
if (detail.contractId) {
@@ -4440,6 +4700,13 @@ export default {
});
},
closeDetail() {
+ this.destroyWaybillLocateMap();
+ this.destroyWaybillTrackMap();
+ this.waybillTrackMode = '';
+ this.waybillLocateInfo = null;
+ this.waybillLocateHint = '';
+ this.waybillTrackInfo = null;
+ this.waybillTrackHint = '';
if (this.isStandaloneWaybillDetailPage) {
this.$router.$avueRouter?.closeTag?.();
this.$router.push({ path: '/business/waybill-manage', query: {} });
@@ -4463,7 +4730,94 @@ export default {
this.$message.info('当前配载单暂无详情数据');
return;
}
- this.$router.push({ path: '/business/loading-manage', query: { detailId: id } });
+ this.$router.push({
+ path: '/business/loading-manage/detail',
+ query: { id, name: '配载单详情' },
+ });
+ },
+ waybillDetailCustomerLabel(row = {}) {
+ return row.customer || row.fullName || row.shortName || row.customerName || row.customerCode || '';
+ },
+ waybillDetailCustomerId(row = {}) {
+ return row.id || row.customerId || '';
+ },
+ canOpenWaybillDetailSummaryLink(prop) {
+ if (this.isPublicWaybillView) return false;
+ const row = this.detailRow || {};
+ const value = this.formatDetailValue(row, prop);
+ if (!value || value === '-') return false;
+ if (prop === 'customerName') return Boolean(row.customerName);
+ if (prop === 'contractNo') return Boolean(row.contractId);
+ if (prop === 'projectName') return Boolean(row.projectId);
+ return false;
+ },
+ handleWaybillDetailSummaryClick(prop) {
+ if (prop === 'customerName') return this.openWaybillDetailCustomer();
+ if (prop === 'contractNo') return this.openWaybillDetailContract();
+ if (prop === 'projectName') return this.openWaybillDetailProject();
+ },
+ async resolveWaybillDetailCustomerId() {
+ const row = this.detailRow || {};
+ if (row.customerId) return row.customerId;
+ const name = String(row.customerName || '').trim();
+ if (!name) return '';
+ if (row.projectId) {
+ try {
+ const res = await getProjectDetail(row.projectId);
+ const customers = this.parseJsonArray(res.data?.data?.customerJson);
+ const matched =
+ customers.find(item => this.waybillDetailCustomerLabel(item) === name) ||
+ customers.find(item => {
+ const label = this.waybillDetailCustomerLabel(item);
+ return label && (name.includes(label) || label.includes(name));
+ });
+ const id = this.waybillDetailCustomerId(matched);
+ if (id) return id;
+ } catch (error) {
+ // 项目客户解析失败时回退到客商档案检索
+ }
+ }
+ try {
+ const res = await getCustomerArchiveList(1, 20, { fullName: name });
+ const records = res.data?.data?.records || [];
+ const exact =
+ records.find(item => item.fullName === name || item.shortName === name) || records[0];
+ return exact?.id || '';
+ } catch (error) {
+ return '';
+ }
+ },
+ async openWaybillDetailCustomer() {
+ if (!this.detailRow?.customerName) return;
+ const customerId = await this.resolveWaybillDetailCustomerId();
+ if (!customerId) {
+ this.$message.warning('未找到对应客商档案,无法打开');
+ return;
+ }
+ this.$router.push({
+ path: '/vehicle/customer-archive/form',
+ query: { id: String(customerId), name: '查看客商档案', view: '1' },
+ });
+ },
+ openWaybillDetailContract() {
+ if (!this.detailRow?.contractId) {
+ this.$message.warning('合同信息缺失,无法打开');
+ return;
+ }
+ this.$router.push({
+ path: '/business/contract-manage/detail',
+ query: { id: this.detailRow.contractId, name: '合同详情' },
+ });
+ },
+ openWaybillDetailProject() {
+ if (!this.detailRow?.projectId) {
+ this.$message.warning('项目信息缺失,无法打开');
+ return;
+ }
+ this.$router.push({
+ path: '/business/project-apply/form',
+ query: { mode: 'view', id: this.detailRow.projectId, name: '查看项目管理' },
+ });
},
loadWaybillDetailProcessNodes(projectId) {
this.waybillDetailProcessNodes = [];
@@ -4492,6 +4846,19 @@ export default {
})
.catch(() => []);
},
+ // 解析打卡时间(支持常见日期字符串格式),未打卡或无有效时间返回 null
+ waybillPunchRecordTimestamp(record = {}) {
+ if (record.punched === false || record.statusName === '未打卡') return null;
+ const raw = record.punchTime || record.punchAt || record.punchDateTime || '';
+ const text = String(raw).trim();
+ if (!text) return null;
+ const parsed = this.$dayjs ? this.$dayjs(text) : null;
+ if (parsed && typeof parsed.isValid === 'function' && parsed.isValid()) {
+ return parsed.valueOf();
+ }
+ const fallback = new Date(text.replace(/-/g, '/')).getTime();
+ return Number.isNaN(fallback) ? null : fallback;
+ },
loadWaybillPunchRecords() {
const waybillId = this.detailRow?.id;
if (!waybillId || this.waybillPunchRecordsLoading) return;
@@ -4622,7 +4989,7 @@ export default {
: [];
},
openWaybillRouteChangeDialog() {
- if (!this.detailRow.id) return;
+ if (!this.detailRow.id || !this.canChangeWaybillRoute(this.detailRow)) return;
this.restoreWaybillRouteChangeRecords();
this.waybillRouteChangeTab = 'change';
this.waybillRouteChangeRemark = '';
@@ -4639,22 +5006,11 @@ export default {
this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows);
this.waybillRouteChangeBox = true;
},
- waybillRouteChangeTypeText(index) {
- if (index === 0) return '起';
- if (index === this.waybillRouteChangeNodes.length - 1) return '终';
- return '经';
- },
- handleWaybillRouteChangeDragStart(index) {
- this.waybillRouteChangeDragIndex = index;
- },
- handleWaybillRouteChangeDrop(index) {
- if (this.waybillRouteChangeDragIndex < 0 || this.waybillRouteChangeDragIndex === index)
- return;
- const nodes = [...this.waybillRouteChangeNodes];
- const [node] = nodes.splice(this.waybillRouteChangeDragIndex, 1);
- nodes.splice(index, 0, node);
- this.waybillRouteChangeNodes = nodes;
- this.waybillRouteChangeDragIndex = -1;
+ canChangeWaybillRoute(row = {}) {
+ const status = String(this.statusValue(row) || row.businessStatus || row.status || '');
+ if (status === 'completed') return false;
+ const statusName = String(row.businessStatusName || row.statusName || '');
+ return !statusName.includes('已完成');
},
updateWaybillRouteChangeNodes() {
this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows);
@@ -4703,9 +5059,8 @@ export default {
row.departureAddress !== row.originalDepartureAddress ||
row.arrivalAddress !== row.originalArrivalAddress;
const routeJson = JSON.stringify(this.waybillRouteChangeNodes);
- const originalRouteJson = this.detailRow.routeJson || '';
- if (!changed && routeJson === originalRouteJson && !this.waybillRouteChangeRemark) {
- this.$message.warning('请修改地址、调整路线或填写变更备注');
+ if (!changed && !this.waybillRouteChangeRemark) {
+ this.$message.warning('请修改地址或填写变更备注');
return;
}
if (typeof this.api.changeRoute !== 'function') {
@@ -4713,11 +5068,9 @@ export default {
return;
}
- const content = changed
- ? `${row.waybillNo || '运单'}:发货地 ${row.originalDepartureAddress || '-'} → ${
- row.departureAddress || '-'
- };到货地 ${row.originalArrivalAddress || '-'} → ${row.arrivalAddress || '-'}`
- : '调整运输路线顺序';
+ const content = `${row.waybillNo || '运单'}:发货地 ${row.originalDepartureAddress || '-'} → ${
+ row.departureAddress || '-'
+ };到货地 ${row.originalArrivalAddress || '-'} → ${row.arrivalAddress || '-'}`;
const records = [
{
changeTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss'),
@@ -4768,7 +5121,262 @@ export default {
}
},
handleWaybillTrackAction(action) {
- this.$message.info(action === 'playback' ? '暂无可回放的物流轨迹' : '暂无车辆实时定位数据');
+ if (action === 'playback') {
+ this.destroyWaybillLocateMap();
+ this.waybillTrackMode = 'playback';
+ this.waybillLocateInfo = null;
+ this.waybillLocateHint = '';
+ if (!Array.isArray(this.waybillTrackDateRange) || this.waybillTrackDateRange.length !== 2) {
+ this.waybillTrackDateRange = this.buildDefaultTrackDateRange();
+ }
+ this.loadWaybillTrack();
+ return;
+ }
+ this.destroyWaybillTrackMap();
+ this.waybillTrackMode = 'locate';
+ this.waybillTrackInfo = null;
+ this.waybillTrackHint = '';
+ this.loadWaybillLocate();
+ },
+ buildDefaultTrackDateRange() {
+ const pad = value => String(value).padStart(2, '0');
+ const formatDate = date =>
+ `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
+ const today = new Date();
+ const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1);
+ return [formatDate(yesterday), formatDate(today)];
+ },
+ destroyWaybillLocateMap() {
+ if (this.waybillLocateMarker) {
+ this.waybillLocateMarker.setMap(null);
+ this.waybillLocateMarker = null;
+ }
+ if (this.waybillLocateInfoWindow) {
+ this.waybillLocateInfoWindow.close();
+ this.waybillLocateInfoWindow = null;
+ }
+ if (this.waybillLocateMapInstance) {
+ this.waybillLocateMapInstance.destroy();
+ this.waybillLocateMapInstance = null;
+ }
+ },
+ destroyWaybillTrackMap() {
+ if (this.waybillTrackPolyline) {
+ this.waybillTrackPolyline.setMap(null);
+ this.waybillTrackPolyline = null;
+ }
+ if (this.waybillTrackStartMarker) {
+ this.waybillTrackStartMarker.setMap(null);
+ this.waybillTrackStartMarker = null;
+ }
+ if (this.waybillTrackEndMarker) {
+ this.waybillTrackEndMarker.setMap(null);
+ this.waybillTrackEndMarker = null;
+ }
+ if (this.waybillTrackMapInstance) {
+ this.waybillTrackMapInstance.destroy();
+ this.waybillTrackMapInstance = null;
+ }
+ },
+ async loadWaybillLocate() {
+ const waybillId = this.detailRow?.id;
+ if (!waybillId) {
+ this.$message.warning('运单信息不完整');
+ return;
+ }
+ if (!this.detailRow.vehicleNo) {
+ this.$message.warning('运单未绑定车牌号,无法实时定位');
+ return;
+ }
+ this.waybillLocateLoading = true;
+ this.waybillLocateHint = '正在获取车辆实时定位...';
+ this.waybillLocateInfo = null;
+ try {
+ const locateApi =
+ typeof this.api.locateVehicle === 'function' ? this.api.locateVehicle : locateVehicle;
+ const res = await locateApi(waybillId);
+ const data = res?.data?.data || null;
+ if (!data || data.longitude == null || data.latitude == null) {
+ this.waybillLocateInfo = null;
+ this.waybillLocateHint = '暂无车辆实时定位数据';
+ this.$message.warning('暂无车辆实时定位数据');
+ return;
+ }
+ this.waybillLocateInfo = data;
+ this.waybillLocateHint = '';
+ await this.$nextTick();
+ await this.renderWaybillLocateMap(data);
+ } catch (error) {
+ this.waybillLocateInfo = null;
+ this.waybillLocateHint = '实时定位获取失败';
+ this.$message.error(error?.message || '实时定位获取失败');
+ } finally {
+ this.waybillLocateLoading = false;
+ }
+ },
+ async loadWaybillTrack() {
+ const waybillId = this.detailRow?.id;
+ if (!waybillId) {
+ this.$message.warning('运单信息不完整');
+ return;
+ }
+ if (!this.detailRow.vehicleNo) {
+ this.$message.warning('运单未绑定车牌号,无法查询历史轨迹');
+ return;
+ }
+ if (!Array.isArray(this.waybillTrackDateRange) || this.waybillTrackDateRange.length !== 2) {
+ this.waybillTrackDateRange = this.buildDefaultTrackDateRange();
+ }
+ const [startDate, endDate] = this.waybillTrackDateRange;
+ this.waybillTrackLoading = true;
+ this.waybillTrackHint = '正在获取历史轨迹...';
+ this.destroyWaybillTrackMap();
+ try {
+ const trackApi =
+ typeof this.api.trackVehicle === 'function' ? this.api.trackVehicle : trackVehicle;
+ const res = await trackApi(waybillId, startDate, endDate);
+ const data = res?.data?.data || null;
+ const points = Array.isArray(data?.points) ? data.points : [];
+ if (!data || points.length === 0) {
+ this.waybillTrackInfo = data || { total: 0, points: [] };
+ this.waybillTrackHint = '暂无可回放的物流轨迹';
+ this.$message.warning('暂无可回放的物流轨迹');
+ return;
+ }
+ this.waybillTrackInfo = data;
+ this.waybillTrackHint = '';
+ await this.$nextTick();
+ await this.renderWaybillTrackMap(points);
+ } catch (error) {
+ this.waybillTrackInfo = null;
+ this.waybillTrackHint = '历史轨迹获取失败';
+ this.$message.error(error?.message || '历史轨迹获取失败');
+ } finally {
+ this.waybillTrackLoading = false;
+ }
+ },
+ async renderWaybillLocateMap(locateInfo) {
+ const longitude = Number(locateInfo?.longitude);
+ const latitude = Number(locateInfo?.latitude);
+ if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
+ this.$message.warning('定位坐标无效,无法在地图上展示');
+ return;
+ }
+ try {
+ await this.loadAmap();
+ await this.$nextTick();
+ const container = this.$refs.waybillLocateMap;
+ if (!container || !window.AMap) {
+ this.$message.warning('高德地图容器未就绪');
+ return;
+ }
+ this.destroyWaybillLocateMap();
+ this.waybillLocateMapInstance = new window.AMap.Map(container, {
+ zoom: 15,
+ center: [longitude, latitude],
+ viewMode: '2D',
+ resizeEnable: true,
+ });
+ const content = [
+ `
`,
+ `
${locateInfo.vehicleNo || '车辆位置'}
`,
+ locateInfo.locateTime ? `
时间:${locateInfo.locateTime}
` : '',
+ locateInfo.address ? `
地址:${locateInfo.address}
` : '',
+ `
坐标:${longitude}, ${latitude}
`,
+ `
`,
+ ].join('');
+ this.waybillLocateInfoWindow = new window.AMap.InfoWindow({
+ content,
+ offset: new window.AMap.Pixel(0, -30),
+ });
+ this.waybillLocateMarker = new window.AMap.Marker({
+ position: [longitude, latitude],
+ title: locateInfo.vehicleNo || '车辆位置',
+ anchor: 'bottom-center',
+ });
+ this.waybillLocateMarker.on('click', () => {
+ this.waybillLocateInfoWindow.open(this.waybillLocateMapInstance, [longitude, latitude]);
+ });
+ this.waybillLocateMapInstance.add(this.waybillLocateMarker);
+ this.waybillLocateInfoWindow.open(this.waybillLocateMapInstance, [longitude, latitude]);
+ this.waybillLocateMapInstance.setFitView([this.waybillLocateMarker], false, [40, 40, 40, 40]);
+ setTimeout(() => {
+ this.waybillLocateMapInstance?.resize?.();
+ }, 80);
+ } catch (error) {
+ window.console.warn('实时定位地图渲染失败', error);
+ this.$message.error('高德地图渲染失败,请稍后重试');
+ }
+ },
+ async renderWaybillTrackMap(points) {
+ const path = (points || [])
+ .map(item => [Number(item.longitude), Number(item.latitude)])
+ .filter(item => Number.isFinite(item[0]) && Number.isFinite(item[1]));
+ if (!path.length) {
+ this.$message.warning('轨迹坐标无效,无法在地图上展示');
+ return;
+ }
+ try {
+ await this.loadAmap();
+ await this.$nextTick();
+ const container = this.$refs.waybillTrackMap;
+ if (!container || !window.AMap) {
+ this.$message.warning('高德地图容器未就绪');
+ return;
+ }
+ this.destroyWaybillTrackMap();
+ this.waybillTrackMapInstance = new window.AMap.Map(container, {
+ zoom: 12,
+ center: path[0],
+ viewMode: '2D',
+ resizeEnable: true,
+ });
+ this.waybillTrackPolyline = new window.AMap.Polyline({
+ path,
+ strokeColor: '#409eff',
+ strokeWeight: 6,
+ strokeOpacity: 0.9,
+ lineJoin: 'round',
+ lineCap: 'round',
+ showDir: path.length > 1,
+ });
+ this.waybillTrackStartMarker = new window.AMap.Marker({
+ position: path[0],
+ title: '起点',
+ anchor: 'bottom-center',
+ label: {
+ content: '起',
+ direction: 'top',
+ offset: new window.AMap.Pixel(0, -6),
+ },
+ });
+ this.waybillTrackEndMarker = new window.AMap.Marker({
+ position: path[path.length - 1],
+ title: '终点',
+ anchor: 'bottom-center',
+ label: {
+ content: '终',
+ direction: 'top',
+ offset: new window.AMap.Pixel(0, -6),
+ },
+ });
+ this.waybillTrackMapInstance.add([
+ this.waybillTrackPolyline,
+ this.waybillTrackStartMarker,
+ this.waybillTrackEndMarker,
+ ]);
+ this.waybillTrackMapInstance.setFitView(
+ [this.waybillTrackPolyline, this.waybillTrackStartMarker, this.waybillTrackEndMarker],
+ false,
+ [48, 48, 48, 48]
+ );
+ setTimeout(() => {
+ this.waybillTrackMapInstance?.resize?.();
+ }, 80);
+ } catch (error) {
+ window.console.warn('历史轨迹地图渲染失败', error);
+ this.$message.error('高德地图渲染失败,请稍后重试');
+ }
},
displayStatus(row, prop) {
const formatStatus = this.config.formatStatus;
@@ -4782,7 +5390,9 @@ export default {
: defaultText;
},
statusTagType(status) {
- if ([1, 'pending', 'processing', 'approved', 'change_approved'].includes(status)) {
+ if (
+ [1, 'pending', 'processing', 'running', 'approved', 'change_approved'].includes(status)
+ ) {
return 'success';
}
if ([2, 'cancelled', 'withdrawn', 'draft'].includes(status)) {
@@ -5053,11 +5663,23 @@ export default {
return;
}
this.getSaveRequest()(submitRow).then(
- () => {
- this.onLoad(this.page);
- this.$message({ type: 'success', message: '操作成功!' });
- done();
- if (this.isStandaloneWaybillPage) this.closeCrudDialog();
+ res => {
+ const data = res.data?.data || {};
+ const id = data.id || submitRow.id;
+ const after = id
+ ? submitMkApprovalFlow({
+ bizType: 'waybill-manage',
+ formInstanceId: id,
+ subjectName: data.waybillNo || submitRow.waybillNo || '',
+ approvalStatus: data.approvalStatus || submitRow.approvalStatus || '',
+ })
+ : Promise.resolve();
+ return after.then(() => {
+ this.onLoad(this.page);
+ this.$message({ type: 'success', message: '提交成功!' });
+ done();
+ if (this.isStandaloneWaybillPage) this.closeCrudDialog();
+ });
},
error => {
window.console.log(error);
@@ -5076,12 +5698,26 @@ export default {
? this.api.updateAttachments
: this.getSaveRequest();
request(submitRow).then(
- () => {
- this.attachmentUploadMode = false;
- this.onLoad(this.page);
- this.$message({ type: 'success', message: '操作成功!' });
- done();
- if (this.isStandaloneWaybillPage) this.closeCrudDialog();
+ res => {
+ const skipMk = this.attachmentUploadMode;
+ const data = res.data?.data || {};
+ const id = data.id || submitRow.id;
+ const after =
+ skipMk || !id
+ ? Promise.resolve()
+ : submitMkApprovalFlow({
+ bizType: 'waybill-manage',
+ formInstanceId: id,
+ subjectName: data.waybillNo || submitRow.waybillNo || '',
+ approvalStatus: data.approvalStatus || submitRow.approvalStatus || '',
+ });
+ return after.then(() => {
+ this.attachmentUploadMode = false;
+ this.onLoad(this.page);
+ this.$message({ type: 'success', message: skipMk ? '操作成功!' : '提交成功!' });
+ done();
+ if (this.isStandaloneWaybillPage) this.closeCrudDialog();
+ });
},
error => {
this.attachmentUploadMode = false;
@@ -5172,19 +5808,28 @@ export default {
if (type === 'add' && copyId) {
this.api.getDetail(copyId).then(res => {
const sourceData = res.data.data || {};
- // 清除不应该复制的字段
+ // 清除不应该复制的字段(业务状态与原运单保持一致)
delete sourceData.id;
delete sourceData.code;
delete sourceData.waybillNo;
delete sourceData.waybillStatus;
+ delete sourceData.driverAcceptStatus;
+ delete sourceData.driverAcceptTime;
+ delete sourceData.driverAcceptDriverId;
+ delete sourceData.driverRejectTime;
+ delete sourceData.driverRejectReason;
+ delete sourceData.loadingNo;
+ delete sourceData.masterNo;
delete sourceData.createTime;
delete sourceData.updateTime;
delete sourceData.createUser;
delete sourceData.updateUser;
delete sourceData.createDept;
+ delete sourceData.createUserName;
+ delete sourceData.updateUserName;
this.applyFormDetail(sourceData);
- // 标记为手工创建
+ // 标记为手工创建;业务状态沿用原运单
this.form.dataSource = '手工创建';
done();
});
@@ -5823,6 +6468,15 @@ export default {
this.form.carrierName = '';
this.form.carrierId = '';
this.form.carrierContractId = '';
+ this.form.driverId = '';
+ this.form.driverName = '';
+ this.form.driverPhone = '';
+ this.form.vehicleNo = '';
+ this.form.trailerVehicleNo = '';
+ this.form.escortName = '';
+ this.form.escortPhone = '';
+ this.taskDriverOptions = [];
+ this.taskEscortOptions = [];
if (nextValue === '承运商') {
this.form.trailerVehicleNo = '';
this.form.escortName = '';
@@ -6076,17 +6730,36 @@ export default {
this.form.carrierContractId = carrier?.carrierContractId || '';
this.form.carrierId = carrier?.carrierId || '';
this.form.carrierName = carrier?.carrierName || '';
- return;
+ } else {
+ this.form.carrierId = carrier?.carrierContractId ? '' : carrier?.id || '';
+ this.form.carrierName = value || '';
}
- this.form.carrierId = carrier?.carrierContractId ? '' : carrier?.id || '';
- this.form.carrierName = value || '';
+ // 承运商变更后清空司机/车辆,避免跨组织脏数据
+ this.form.driverId = '';
+ this.form.driverName = '';
+ this.form.driverPhone = '';
+ this.form.vehicleNo = '';
+ this.form.trailerVehicleNo = '';
+ this.form.escortName = '';
+ this.form.escortPhone = '';
+ this.taskDriverOptions = [];
+ this.taskEscortOptions = [];
+ },
+ getTaskCarrierFilter() {
+ return {
+ carrierId: this.form.carrierId || '',
+ carrierName: this.form.carrierName || '',
+ };
},
loadTaskDriverOptions() {
if (!this.taskInfoFormEnabled || this.taskDriverLoading) return Promise.resolve([]);
this.taskDriverLoading = true;
- return getDriverList(1, 9999, { posts: '司机' })
- .then(res => {
- this.taskDriverOptions = extractRecords(res);
+ return fetchDriversByCarrierOrganizations(
+ { size: 9999, posts: '司机' },
+ this.getTaskCarrierFilter()
+ )
+ .then(records => {
+ this.taskDriverOptions = records;
return this.taskDriverOptions;
})
.finally(() => {
@@ -6095,10 +6768,17 @@ export default {
},
fetchTaskDriverSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim();
+ const carrier = this.getTaskCarrierFilter();
+ if (!carrier.carrierId && !carrier.carrierName) {
+ callback([]);
+ return;
+ }
this.taskDriverLoading = true;
- getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
- .then(res => {
- const records = extractRecords(res);
+ fetchDriversByCarrierOrganizations(
+ { size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
+ carrier
+ )
+ .then(records => {
this.taskDriverOptions = records;
callback(
records.map(item => ({
@@ -6117,10 +6797,17 @@ export default {
},
fetchTaskEscortSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim();
+ const carrier = this.getTaskCarrierFilter();
+ if (!carrier.carrierId && !carrier.carrierName) {
+ callback([]);
+ return;
+ }
this.taskEscortLoading = true;
- getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '押运员' })
- .then(res => {
- const records = extractRecords(res);
+ fetchDriversByCarrierOrganizations(
+ { size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
+ carrier
+ )
+ .then(records => {
this.taskEscortOptions = records;
callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' })));
})
@@ -7260,12 +7947,7 @@ export default {
this.ensureTransportAmapGeocoder()
.then(() => this.runAmapGeocode('location', keyword))
.then(result => {
- this.transportMapSearchResults = (result.geocodes || []).map((item, index) => ({
- id: item.id || index,
- name: item.formattedAddress || keyword,
- address: item.formattedAddress || keyword,
- location: item.location,
- }));
+ this.transportMapSearchResults = normalizeMapSearchResults(result, keyword);
const point = this.resolveMapPoint(result);
if (!point) {
this.transportMapStatus = '未找到匹配地址';
@@ -7888,6 +8570,123 @@ export default {
this.page.currentPage = 1;
this.onLoad(this.page);
},
+ setupSearchAutocompletes() {
+ const fields = [
+ {
+ prop: 'projectName',
+ placeholder: '请选择或输入项目名称',
+ fetch: (queryString, callback) =>
+ this.fetchSearchNameSuggestions(queryString, callback, {
+ request: keyword =>
+ getProjectList(1, 20, keyword ? { projectName: keyword } : {}),
+ getValue: item => item.projectName || item.name || '',
+ }),
+ },
+ {
+ prop: 'customerName',
+ placeholder: '请选择或输入客户名称',
+ fetch: (queryString, callback) =>
+ this.fetchSearchNameSuggestions(queryString, callback, {
+ request: keyword =>
+ getCustomerArchiveList(1, 20, {
+ ...(keyword ? { fullName: keyword } : {}),
+ customerType: '客户',
+ }),
+ getValue: item => item.fullName || item.customerName || item.name || '',
+ }),
+ },
+ {
+ prop: 'cargoType',
+ placeholder: '请选择或输入货物类型',
+ fetch: (queryString, callback) =>
+ this.fetchSearchNameSuggestions(queryString, callback, {
+ request: keyword =>
+ getCargoTypeList(1, 20, keyword ? { cargoName: keyword } : {}),
+ getValue: item => item.cargoName || item.typeName || item.name || item.label || '',
+ }),
+ },
+ {
+ prop: 'carrierName',
+ placeholder: '请选择或输入承运商名称',
+ fetch: (queryString, callback) =>
+ this.fetchSearchNameSuggestions(queryString, callback, {
+ request: keyword =>
+ getCustomerArchiveList(1, 20, {
+ ...(keyword ? { fullName: keyword } : {}),
+ customerType: '承运商',
+ }),
+ getValue: item =>
+ item.fullName || item.carrierName || item.customerName || item.name || '',
+ }),
+ },
+ {
+ prop: 'driverName',
+ placeholder: '请选择或输入司机名称',
+ fetch: (queryString, callback) =>
+ this.fetchSearchNameSuggestions(queryString, callback, {
+ request: keyword =>
+ getDriverList(1, 20, {
+ ...(keyword ? { driverName: keyword } : {}),
+ posts: '司机',
+ }),
+ getValue: item => item.driverName || item.name || '',
+ }),
+ },
+ {
+ prop: 'planName',
+ placeholder: '请选择或输入计划名称',
+ fetch: (queryString, callback) =>
+ this.fetchSearchNameSuggestions(queryString, callback, {
+ request: keyword =>
+ getShippingPlanList(1, 20, keyword ? { planName: keyword } : {}),
+ getValue: item => item.planName || item.name || '',
+ }),
+ },
+ ];
+ fields.forEach(field => {
+ const column = this.findColumn?.(this.option.column, field.prop);
+ if (!column) return;
+ column.searchPlaceholder = field.placeholder;
+ column.renderSearch = scope =>
+ this.renderSearchAutocomplete(scope, field.prop, field.placeholder, field.fetch);
+ });
+ },
+ renderSearchAutocomplete(scope, prop, placeholder, fetchSuggestions) {
+ return h(ElAutocomplete, {
+ modelValue: scope.row?.[prop] ?? '',
+ 'onUpdate:modelValue': value => {
+ if (scope.row) scope.row[prop] = value ?? '';
+ },
+ fetchSuggestions,
+ debounce: 300,
+ clearable: true,
+ triggerOnFocus: true,
+ placeholder,
+ style: 'width: 100%',
+ valueKey: 'value',
+ fitInputWidth: true,
+ });
+ },
+ fetchSearchNameSuggestions(queryString, callback, { request, getValue }) {
+ const keyword = String(queryString || '').trim();
+ Promise.resolve(request(keyword))
+ .then(res => {
+ const records = extractRecords(res);
+ const seen = new Set();
+ const options = [];
+ records.forEach(item => {
+ const value = String(getValue(item) || '').trim();
+ if (!value || seen.has(value)) return;
+ seen.add(value);
+ options.push({ ...item, value });
+ });
+ callback(options);
+ })
+ .catch(error => {
+ window.console.log(error);
+ callback([]);
+ });
+ },
searchChange(params, done) {
this.query = {
...params,
@@ -8056,7 +8855,7 @@ export default {
};
this.mileageForm = {
id: row.id,
- mileage: row.mileage === null || row.mileage === undefined ? '' : String(row.mileage),
+ mileage: this.normalizeMileageValue(row.mileage),
mileageRemark: row.mileageRemark || '',
};
this.$nextTick(() => this.$refs.mileageFormRef?.clearValidate());
@@ -8153,10 +8952,21 @@ export default {
},
fetchReassignDriverSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim();
+ const row = this.reassignDrawer.row || {};
+ const carrier = {
+ carrierId: row.carrierId || '',
+ carrierName: row.carrierName || '',
+ };
+ if (!carrier.carrierId && !carrier.carrierName) {
+ callback([]);
+ return;
+ }
this.reassignDrawer.driverLoading = true;
- getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
- .then(res => {
- const records = extractRecords(res);
+ fetchDriversByCarrierOrganizations(
+ { size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
+ carrier
+ )
+ .then(records => {
this.reassignDriverOptions = records;
callback(
records.map(item => ({
@@ -9245,6 +10055,15 @@ export default {
strong {
font-size: 20px;
}
+
+ :deep(.el-tag) {
+ height: 28px;
+ padding: 0 12px;
+ border: 0;
+ border-radius: 8px;
+ font-size: 13px;
+ line-height: 28px;
+ }
}
&__waybill-route-change-link {
@@ -9281,7 +10100,12 @@ export default {
white-space: nowrap;
&.is-link {
+ display: inline;
+ justify-content: flex-start;
+ max-width: 100%;
color: #409eff;
+ font-weight: 500;
+ vertical-align: baseline;
}
}
@@ -9445,6 +10269,81 @@ export default {
border: 1px solid #eff1f7;
}
+ &__waybill-locate {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ &__waybill-locate-meta {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 8px 16px;
+ padding: 10px 12px;
+ border: 1px solid #eff1f7;
+ background: #fafafa;
+ color: #606266;
+ font-size: 13px;
+
+ span {
+ margin-right: 8px;
+ color: #909399;
+ }
+
+ strong {
+ color: #303133;
+ font-weight: 600;
+ }
+ }
+
+ &__waybill-locate-address {
+ grid-column: 1 / -1;
+ }
+
+ &__waybill-locate-map {
+ width: 100%;
+ min-height: 320px;
+ height: 320px;
+ border: 1px solid #eff1f7;
+ background: #f5f7fa;
+ }
+
+ &__waybill-locate-map--track {
+ min-height: 420px;
+ height: 420px;
+ }
+
+ &__waybill-map-tip {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ z-index: 2;
+ transform: translate(-50%, -50%);
+ padding: 8px 14px;
+ border-radius: 4px;
+ background: rgba(255, 255, 255, 0.92);
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+ color: #909399;
+ font-size: 13px;
+ text-align: center;
+ pointer-events: none;
+ }
+
+ &__waybill-track-toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ align-items: center;
+ margin-bottom: 10px;
+ color: #606266;
+ font-size: 13px;
+ }
+
+ &__waybill-track-total {
+ color: #909399;
+ }
+
&__waybill-track-actions {
display: flex;
gap: 8px;
diff --git a/src/views/business/contract-manage-change.vue b/src/views/business/contract-manage-change.vue
index c325a2f..5d78d9e 100644
--- a/src/views/business/contract-manage-change.vue
+++ b/src/views/business/contract-manage-change.vue
@@ -70,7 +70,7 @@
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
- placeholder="YYYY-MM-DD"
+ placeholder="请输入"
/>
至
@@ -92,12 +92,12 @@
-
@@ -189,10 +189,11 @@
title="合同文件"
description
attachment-type
- use-upload-dialog
+ :attachment-location-options="changeAttachmentLocationOptions"
:rows="contractFileRows"
:preview="previewAttachment"
@update:rows="contractFileRows = $event"
+ @upload-to-location="handleAttachmentUploadToLocation"
/>
@@ -218,7 +219,7 @@
-
+
@@ -256,11 +257,12 @@
title="其它附件"
description
attachment-type
- use-upload-dialog
attachment-location="其它附件"
+ :attachment-location-options="changeAttachmentLocationOptions"
:rows="attachments"
:preview="previewAttachment"
@update:rows="attachments = $event"
+ @upload-to-location="handleAttachmentUploadToLocation"
/>
@@ -281,11 +283,12 @@
title="变更材料"
description
attachment-type
- use-upload-dialog
attachment-location="变更材料"
+ :attachment-location-options="changeAttachmentLocationOptions"
:rows="changeMaterials"
:preview="previewAttachment"
@update:rows="changeMaterials = $event"
+ @upload-to-location="handleAttachmentUploadToLocation"
/>
@@ -478,7 +478,7 @@
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
- placeholder="YYYY-MM-DD"
+ placeholder="请输入"
/>
至
@@ -498,12 +498,12 @@
>天
-
@@ -592,12 +592,13 @@
title="合同文件"
description
attachment-type
- use-upload-dialog
+ :attachment-location-options="contractAttachmentLocationOptions"
:lock-approved="isAttachmentUploadMode || hasApprovedContractFiles"
:mark-approved-on-upload="isAttachmentUploadMode"
:rows="contractFileRows"
:preview="previewAttachment"
@update:rows="contractFileRows = $event"
+ @upload-to-location="handleAttachmentUploadToLocation"
/>