diff --git a/src/views/business/loading-manage.vue b/src/views/business/loading-manage.vue
index 459c6dc..7fc4461 100644
--- a/src/views/business/loading-manage.vue
+++ b/src/views/business/loading-manage.vue
@@ -609,31 +609,195 @@
show-overflow-tooltip />
-
+
+
+
+ 补录执行批量补录
-
{{ item.name || item.nodeName || item.label }}
+
+
+
+
+
+
+
+
+
+
+ {{ record.nodeName || record.nodeCode || '打卡' }}
+ {{
+ record.statusName || (record.punched ? '已打卡' : '未打卡')
+ }}
+ 在途
+ 异常
+
+
+
+ 地点:{{ record.address }}
+
+
+ 重量 {{ record.weight }}吨
+ 体积 {{ record.volume }}方
+ 数量 {{ record.quantity }}
+
+
+ 备注:{{ record.remark }}
+
+
+
+
+
+ {{ photo.label || `${record.nodeName}-凭证` }}
+
+
+
+
+
暂未打卡
+
+
+
+
+
+
+
+
+
+ 返回文件夹
+
+ {{ voucherFolderName(voucherFolder) }}
+
+
+
+
+
+ {{ voucherFolderName(image) }}
+
+
+
+
+
+
+
+
+
+
+ {{ photo.label || '凭证' }}
+
+
+
+
+
+
+
轨迹回放
-
+
+
-
原:{{ row.originalDepartureAddress || '-' }}
-
+
+
+ 常用
+
-
原:{{ row.originalArrivalAddress || '-' }}
-
+
+
+ 常用
+
@@ -1374,8 +1570,11 @@
{{
tag
}}
+ >
关闭
+
+
+
+
+ 搜索
+
+
+
+
+ {{ routeChangeMapStatus
+ }} 行政区划:{{ routeChangeMapSelected.regionName }}
+
+
+ 取消
+
+ 确定
+
+
+
+
@@ -1488,13 +1743,18 @@ import { getList as getPlanList } from '@/api/business/transport-plan';
import {
getList as getWaybillList,
getDetail as getWaybillDetail,
+ getPunchRecords as getWaybillPunchRecords,
} from '@/api/business/waybill-manage';
import { getList as getContractList } from '@/api/business/contract-manage';
import { getParentOptions as getCargoTypeOptions } from '@/api/base/cargo-type';
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
-import { getList as getProcessConfigList } from '@/api/business/process-config';
+import {
+ getList as getProcessConfigList,
+ getDetail as getProcessConfigDetail,
+ getVoucherImages as getProcessConfigVoucherImages,
+} from '@/api/business/process-config';
import { getList as getCommonAddressList } from '@/api/base/common-address';
import {
carrierTypeOptions,
@@ -1504,9 +1764,10 @@ import {
} from '@/option/business/loading-manage';
import { option as waybillOption } from '@/option/business/waybill-manage';
import { ElMessage, ElMessageBox } from 'element-plus';
-import { Location, Rank, Refresh, Setting } from '@element-plus/icons-vue';
+import { Location, Rank, Refresh, Search, Setting } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
import { isMobile } from '@/utils/validate';
+import { normalizeMapSearchResults } from '@/utils/map-search';
const defaultTransportType = '';
const defaultCandidateTransportType = '';
@@ -1598,6 +1859,21 @@ const normalizeLoadingMileage = value => {
return value;
};
+// 货物等业务字段用 -1 表示未填写,展示时留空
+const blankCargoSentinel = value => {
+ if (value === undefined || value === null || value === '') return '';
+ if (Number(value) === -1) return '';
+ return value;
+};
+
+const sanitizeCargoRow = (row = {}) => {
+ const result = { ...row };
+ Object.keys(result).forEach(key => {
+ result[key] = blankCargoSentinel(result[key]);
+ });
+ return result;
+};
+
// 提交时未填写里程传 null,禁止自动填充 -1
const toSubmitMileage = value => {
const normalized = normalizeLoadingMileage(value);
@@ -1611,6 +1887,14 @@ const createCopiedDialogForm = (detail = {}) => {
Object.keys(form).forEach(prop => {
if (Object.prototype.hasOwnProperty.call(detail, prop)) form[prop] = detail[prop];
});
+ let taskInfo = {};
+ try {
+ const parsed = JSON.parse(form.taskInfoJson || '{}');
+ taskInfo = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
+ } catch (error) {
+ taskInfo = {};
+ }
+ const { routeChangeRecords, ...copiedTaskInfo } = taskInfo;
return {
...form,
id: '',
@@ -1620,7 +1904,7 @@ const createCopiedDialogForm = (detail = {}) => {
dataSource: '手工创建',
currentProcessNode: '接单',
mileage: normalizeLoadingMileage(form.mileage),
- taskInfoJson: '',
+ taskInfoJson: JSON.stringify(copiedTaskInfo),
businessStatus: 'draft',
};
};
@@ -1680,12 +1964,14 @@ const uniqueText = (rows, prop) =>
export default {
components: {
Rank,
+ Search,
},
data() {
return {
Rank,
Location,
Refresh,
+ Search,
Setting,
loading: false,
tableSize: 'default',
@@ -1740,9 +2026,22 @@ export default {
statusOptions: loadingStatusOptions,
dataSourceOptions: loadingManageConfig.dataSourceOptions,
config: loadingManageConfig,
- processNodes: [],
- processProjectId: '',
- processProjectOptions: [],
+ processDetailTab: 'punch',
+ processWaybillId: '',
+ processWaybillOptions: [],
+ punchRecords: [],
+ punchRecordsLoading: false,
+ driverUploads: [],
+ hasRelatedVoucher: false,
+ voucherImages: [],
+ voucherFolders: [],
+ voucherFolder: null,
+ voucherFolderView: false,
+ voucherImagesLoading: false,
+ voucherImagesLoaded: false,
+ attachmentImagePreviewVisible: false,
+ attachmentImagePreviewUrls: [],
+ attachmentImagePreviewIndex: 0,
routeChangeVisible: false,
routeChangeTab: 'change',
routeChangeRows: [],
@@ -1750,12 +2049,24 @@ export default {
routeChangeRemark: '',
routeChangeRecords: [],
routeChangeDragIndex: -1,
+ routeChangeOriginalAddresses: [],
+ routeChangeOrderDirty: false,
commonAddressVisible: false,
commonAddressLoading: false,
commonAddressRows: [],
commonAddressQuery: { addressName: '' },
commonAddressPage: { currentPage: 1, pageSize: 10, total: 0 },
commonAddressTarget: null,
+ routeChangeMapVisible: false,
+ routeChangeMapLoading: false,
+ routeChangeMapKeyword: '',
+ routeChangeMapStatus: '可搜索地址或点击地图选点',
+ routeChangeMapSelected: {},
+ routeChangeMapSearchResults: [],
+ routeChangeMapTarget: null,
+ routeChangeMapInstance: null,
+ routeChangeMapMarker: null,
+ routeChangeMapGeocoder: null,
detailAmap: null,
};
},
@@ -1763,6 +2074,24 @@ export default {
dialogReadonly() {
return this.dialogMode === 'view';
},
+ // 打卡时间轴:已打卡按时间升序,未打卡保持原节点顺序排在其后
+ orderedPunchRecords() {
+ const records = Array.isArray(this.punchRecords) ? this.punchRecords : [];
+ return records
+ .map((record, index) => ({
+ record,
+ index,
+ punchedAt: this.punchRecordTimestamp(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);
+ },
taskCarrierPlaceholder() {
if (!this.selectedWaybillRows.length) return '请先选择运单';
if (!this.taskCarrierLoading && !this.taskCarrierOptions.length) return '暂无可用承运商合同';
@@ -2457,6 +2786,7 @@ export default {
if (status === 'draft') {
return [
{ type: 'edit', label: '编辑' },
+ { type: 'copy', label: '复制' },
{ type: 'delete', label: '删除' },
];
}
@@ -2468,24 +2798,36 @@ export default {
if (row.driverRejected) {
actions.push({ type: 'reassign', label: '重新派单' });
}
- actions.push({ type: 'edit', label: '编辑' }, { type: 'cancel', label: '取消' });
+ actions.push(
+ { type: 'edit', label: '编辑' },
+ { type: 'copy', label: '复制' },
+ { type: 'cancel', label: '取消' }
+ );
return actions;
}
if (status === 'running') {
return [
{ type: 'view', label: '详情' },
{ type: 'complete', label: '完成' },
+ { type: 'copy', label: '复制' },
];
}
if (status === 'completed' || status === 'cancelled') {
- return [{ type: 'view', label: '详情' }];
+ return [
+ { type: 'view', label: '详情' },
+ { type: 'copy', label: '复制' },
+ ];
}
- return [{ type: 'view', label: '详情' }];
+ return [
+ { type: 'view', label: '详情' },
+ { type: 'copy', label: '复制' },
+ ];
},
handleRowAction(type, row) {
const actionMap = {
view: () => this.gotoLoadingDetail(row),
edit: () => this.openLoadingDialog('edit', row),
+ copy: () => this.gotoLoadingCopy(row),
reassign: () => this.gotoLoadingReassign(row),
delete: () => this.deleteLoading(row),
start: () => this.startLoading(row),
@@ -2507,6 +2849,18 @@ export default {
query: { id, name: '配载单详情' },
});
},
+ // 复制:进入新增页并带回原配载数据
+ gotoLoadingCopy(row = {}) {
+ const id = row.id || row.loadingId || row.loadingManageId;
+ if (!id) {
+ ElMessage.warning('配载单不存在,无法复制');
+ return;
+ }
+ this.$router.push({
+ path: '/business/loading-manage/form',
+ query: { mode: 'add', copyId: id, name: '新增配载管理' },
+ });
+ },
// 重新派单改为独立页面
gotoLoadingReassign(row = {}) {
if (!row.id) {
@@ -2585,10 +2939,11 @@ export default {
this.dialogForm.waybillIdsJson,
this.dialogForm.loadingSubNos
);
- this.syncProcessProjectOptions();
+ this.syncProcessWaybillOptions();
this.restoreRouteAndCargo();
this.restoreRouteChangeRecords();
- await this.loadProcessNodes();
+ this.loadPunchRecords();
+ this.probeHasRelatedVoucher();
this.$nextTick(() => this.initDetailAmap());
await this.loadCandidateWaybills();
} else {
@@ -2622,23 +2977,38 @@ export default {
this.candidatePage.currentPage = 1;
this.candidatePage.total = 0;
this.candidateSearchVisible = false;
- this.processNodes = [];
- this.processProjectId = '';
- this.processProjectOptions = [];
+ this.processDetailTab = 'punch';
+ this.processWaybillId = '';
+ this.processWaybillOptions = [];
+ this.punchRecords = [];
+ this.driverUploads = [];
+ this.hasRelatedVoucher = false;
+ this.voucherImages = [];
+ this.voucherFolders = [];
+ this.voucherFolder = null;
+ this.voucherFolderView = false;
+ this.voucherImagesLoading = false;
+ this.voucherImagesLoaded = false;
+ this.attachmentImagePreviewVisible = false;
+ this.attachmentImagePreviewUrls = [];
+ this.attachmentImagePreviewIndex = 0;
},
// 离开本页(标签缓存/卸载)时收起页内子弹窗,避免 append-to-body 的弹窗 DOM 残留在 body 上
closeInnerDialogs() {
this.routeChangeVisible = false;
this.commonAddressVisible = false;
+ this.routeChangeMapVisible = false;
this.candidateSearchVisible = false;
},
restoreRouteAndCargo() {
const routeRows = parseJsonArray(this.dialogForm.routeJson);
const cargoRows = parseJsonArray(this.dialogForm.goodsJson);
this.routeNodes = routeRows.length
- ? routeRows
+ ? this.syncRouteNodeLockedFlags(routeRows, this.selectedWaybillRows)
: this.buildRouteNodes(this.selectedWaybillRows);
- this.cargoRows = cargoRows.length ? cargoRows : this.buildCargoRows(this.selectedWaybillRows);
+ this.cargoRows = (cargoRows.length ? cargoRows : this.buildCargoRows(this.selectedWaybillRows)).map(
+ row => sanitizeCargoRow(row)
+ );
},
restoreRouteChangeRecords() {
try {
@@ -2652,70 +3022,177 @@ export default {
this.routeChangeRecords = [];
}
},
- async loadProcessNodes() {
- if (!this.processProjectId) {
- this.processNodes = [];
+ syncProcessWaybillOptions() {
+ this.processWaybillOptions = (this.selectedWaybillRows || [])
+ .filter(row => row?.id)
+ .map(row => ({
+ id: row.id,
+ waybillNo: row.waybillNo || String(row.id),
+ }));
+ const currentExists = this.processWaybillOptions.some(
+ item => String(item.id) === String(this.processWaybillId)
+ );
+ this.processWaybillId = currentExists
+ ? this.processWaybillId
+ : this.processWaybillOptions[0]?.id || '';
+ this.processDetailTab = 'punch';
+ this.resetVoucherState();
+ },
+ resetVoucherState() {
+ this.hasRelatedVoucher = false;
+ this.voucherImages = [];
+ this.voucherFolders = [];
+ this.voucherFolder = null;
+ this.voucherFolderView = false;
+ this.voucherImagesLoading = false;
+ this.voucherImagesLoaded = false;
+ },
+ handleProcessWaybillChange() {
+ this.processDetailTab = 'punch';
+ this.resetVoucherState();
+ this.loadPunchRecords();
+ this.probeHasRelatedVoucher();
+ },
+ handleProcessDetailTabChange(tab) {
+ if (tab === 'punch' || tab === 'driverUpload') this.loadPunchRecords();
+ if (tab === 'batchSupplement') this.loadVoucherImages();
+ },
+ probeHasRelatedVoucher() {
+ const waybillId = this.processWaybillId;
+ const waybill = (this.selectedWaybillRows || []).find(
+ row => String(row.id) === String(waybillId)
+ );
+ const projectId = waybill?.projectId;
+ if (!waybillId || !projectId) {
+ this.hasRelatedVoucher = false;
return;
}
- try {
- const projectId = String(this.processProjectId);
- const rows = unwrapRecords(
- await getProcessConfigList(1, 100, { projectIds: projectId, status: 1 })
- );
- const config =
- rows.find(item => {
- const projectIds = String(item.projectIds || '')
+ getProcessConfigList(1, 100, { projectIds: projectId, status: 1 })
+ .then(res => {
+ const config = unwrapRecords(res).find(row => {
+ const projectIds = String(row.projectIds || '')
.split(',')
- .map(value => value.trim())
- .filter(Boolean);
- return projectIds.includes(projectId) && item.nodeConfigJson;
- }) || {};
- this.processNodes = this.parseProcessConfigNodes(config);
- } catch (error) {
- this.processNodes = [];
- }
- },
- syncProcessProjectOptions() {
- const projectMap = new Map();
- this.selectedWaybillRows.forEach(row => {
- const id = row.projectId;
- if (!id || projectMap.has(String(id))) return;
- projectMap.set(String(id), {
- id,
- projectName: row.projectName || row.projectNames || String(id),
+ .map(item => item.trim());
+ return projectIds.includes(String(projectId));
+ });
+ if (!config?.id) {
+ this.hasRelatedVoucher = false;
+ return null;
+ }
+ return getProcessConfigDetail(config.id, waybillId);
+ })
+ .then(detailRes => {
+ if (!detailRes) return;
+ const detail = detailRes?.data?.data || detailRes?.data || {};
+ this.hasRelatedVoucher = Boolean(detail.hasRelatedVoucher);
+ })
+ .catch(() => {
+ this.hasRelatedVoucher = false;
});
- });
- if (!projectMap.size && this.dialogForm.projectId) {
- projectMap.set(String(this.dialogForm.projectId), {
- id: this.dialogForm.projectId,
- projectName: this.dialogForm.projectName || String(this.dialogForm.projectId),
- });
- }
- this.processProjectOptions = [...projectMap.values()];
- this.processProjectId = this.processProjectOptions[0]?.id || '';
},
- parseProcessConfigNodes(config = {}) {
- let nodes = [];
- try {
- const parsed =
- typeof config.nodeConfigJson === 'string'
- ? JSON.parse(config.nodeConfigJson)
- : config.nodeConfigJson;
- nodes = Array.isArray(parsed) ? parsed : parsed?.nodes || [];
- } catch (error) {
- nodes = [];
- }
- const includedNodes = String(config.includedNodes || '')
- .split(',')
- .map(value => value.trim())
- .filter(Boolean);
- return nodes.filter(
- node =>
- node.enabled !== false && (!includedNodes.length || includedNodes.includes(node.name))
+ punchRecordTimestamp(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 = dayjs(text);
+ if (parsed.isValid()) return parsed.valueOf();
+ const fallback = new Date(text.replace(/-/g, '/')).getTime();
+ return Number.isNaN(fallback) ? null : fallback;
+ },
+ loadPunchRecords() {
+ const waybillId = this.processWaybillId;
+ if (!waybillId || this.punchRecordsLoading) return;
+ this.punchRecordsLoading = true;
+ getWaybillPunchRecords(waybillId)
+ .then(res => {
+ const data = res?.data?.data || res?.data || {};
+ this.punchRecords = Array.isArray(data.records) ? data.records : [];
+ this.driverUploads = Array.isArray(data.driverUploads) ? data.driverUploads : [];
+ if (data.hasRelatedVoucher) this.hasRelatedVoucher = true;
+ })
+ .catch(() => {
+ this.punchRecords = [];
+ this.driverUploads = [];
+ })
+ .finally(() => {
+ this.punchRecordsLoading = false;
+ });
+ },
+ previewDriverUploadPhoto(photos, index) {
+ const list = (photos || []).map(item => item?.url).filter(Boolean);
+ if (!list.length) return;
+ this.attachmentImagePreviewUrls = list;
+ this.attachmentImagePreviewIndex = Math.min(
+ Math.max(Number(index) || 0, 0),
+ list.length - 1
);
+ this.attachmentImagePreviewVisible = true;
},
- async handleProcessProjectChange() {
- await this.loadProcessNodes();
+ loadVoucherImages() {
+ if (!this.processWaybillId || this.voucherImagesLoading || this.voucherImagesLoaded) {
+ return;
+ }
+ this.voucherImagesLoading = true;
+ getProcessConfigVoucherImages(this.processWaybillId)
+ .then(res => {
+ this.voucherImages = unwrapRecords(res);
+ this.voucherFolders = this.voucherImages.filter(item => this.isVoucherFolder(item));
+ this.voucherFolder = null;
+ this.voucherFolderView = false;
+ if (this.voucherImages.length) this.hasRelatedVoucher = true;
+ this.voucherImagesLoaded = true;
+ })
+ .finally(() => {
+ this.voucherImagesLoading = false;
+ });
+ },
+ isVoucherFolder(item) {
+ return Boolean(item && (item.isFolder === true || item.type === 'folder'));
+ },
+ voucherFolderName(folder) {
+ return folder?.folderName || folder?.name || folder?.plateNo || '凭证文件夹';
+ },
+ openVoucherFolder(folder) {
+ if (!this.processWaybillId || !this.isVoucherFolder(folder)) return;
+ if (!folder.voucherId || !folder.folderName) {
+ ElMessage.warning('凭证文件夹信息不完整');
+ return;
+ }
+ this.voucherImagesLoading = true;
+ getProcessConfigVoucherImages(this.processWaybillId, {
+ voucherId: folder.voucherId,
+ folderName: folder.folderName,
+ })
+ .then(res => {
+ this.voucherFolder = folder;
+ this.voucherFolderView = true;
+ this.voucherImages = unwrapRecords(res).filter(item => !this.isVoucherFolder(item));
+ })
+ .finally(() => {
+ this.voucherImagesLoading = false;
+ });
+ },
+ backToVoucherFolders() {
+ this.voucherFolder = null;
+ this.voucherFolderView = false;
+ this.voucherImages = this.voucherFolders;
+ },
+ previewVoucherImage(index) {
+ const list = (this.voucherImages || [])
+ .filter(item => !this.isVoucherFolder(item))
+ .map(item => item?.url)
+ .filter(Boolean);
+ if (!list.length) return;
+ const current = this.voucherImages[index];
+ const currentUrl = current?.url;
+ const currentIndex = Math.max(
+ 0,
+ list.findIndex(url => url === currentUrl)
+ );
+ this.attachmentImagePreviewUrls = list;
+ this.attachmentImagePreviewIndex = currentIndex >= 0 ? currentIndex : 0;
+ this.attachmentImagePreviewVisible = true;
},
previewAttachment(file) {
if (!file.url) {
@@ -2760,9 +3237,6 @@ export default {
handleTrackAction(action) {
ElMessage.info(action === 'playback' ? '暂无可回放的物流轨迹' : '暂无车辆实时定位数据');
},
- handleProcessAction(action) {
- ElMessage.info(action === 'supplement' ? '补录执行功能暂未接入' : '批量补录功能暂未接入');
- },
openRouteChangeDialog() {
this.routeChangeTab = 'change';
this.routeChangeRemark = '';
@@ -2773,7 +3247,18 @@ export default {
departureAddress: row.departureAddress || row.departureName || '',
arrivalAddress: row.arrivalAddress || row.arrivalName || '',
}));
- this.routeChangeNodes = this.buildRouteNodes(this.routeChangeRows);
+ const builtNodes = this.buildRouteNodes(this.routeChangeRows);
+ this.routeChangeNodes = this.routeNodes.length
+ ? this.syncRouteNodeLockedFlags(
+ this.routeNodes.map(node => ({
+ ...node,
+ tags: [...(node.tags || [])],
+ })),
+ this.routeChangeRows
+ )
+ : builtNodes;
+ this.routeChangeOriginalAddresses = this.routeChangeNodes.map(node => node.address);
+ this.routeChangeOrderDirty = false;
this.routeChangeVisible = true;
},
routeChangeTypeText(index) {
@@ -2787,26 +3272,255 @@ export default {
return 'middle';
},
handleRouteChangeDragStart(index) {
+ if (this.isRouteChangeNodeLocked(this.routeChangeNodes[index])) {
+ this.routeChangeDragIndex = -1;
+ return;
+ }
this.routeChangeDragIndex = index;
},
handleRouteChangeDrop(index) {
if (this.routeChangeDragIndex < 0 || this.routeChangeDragIndex === index) return;
+ if (
+ this.isRouteChangeNodeLocked(this.routeChangeNodes[this.routeChangeDragIndex]) ||
+ this.isRouteChangeNodeLocked(this.routeChangeNodes[index])
+ ) {
+ this.routeChangeDragIndex = -1;
+ return;
+ }
const nodes = [...this.routeChangeNodes];
const [node] = nodes.splice(this.routeChangeDragIndex, 1);
nodes.splice(index, 0, node);
+ const originalLockedKeys = this.routeChangeNodes.map(item =>
+ this.isRouteChangeNodeLocked(item) ? item.key : null
+ );
+ const nextLockedKeys = nodes.map(item =>
+ this.isRouteChangeNodeLocked(item) ? item.key : null
+ );
+ if (
+ originalLockedKeys.some((key, lockedIndex) => key && key !== nextLockedKeys[lockedIndex])
+ ) {
+ ElMessage.warning('已完成运单对应的路线不可调整');
+ this.routeChangeDragIndex = -1;
+ return;
+ }
this.routeChangeNodes = nodes;
+ this.routeChangeOrderDirty = true;
this.routeChangeDragIndex = -1;
},
updateRouteChangeNodes() {
- this.routeChangeNodes = this.buildRouteNodes(this.routeChangeRows);
+ const previousAddresses = this.routeChangeNodes.map(node => node.address);
+ const builtNodes = this.buildRouteNodes(this.routeChangeRows);
+ const remaining = [...builtNodes];
+ const orderedNodes = [];
+ previousAddresses.forEach(address => {
+ const index = remaining.findIndex(node => node.address === address);
+ if (index >= 0) {
+ orderedNodes.push(remaining.splice(index, 1)[0]);
+ }
+ });
+ this.routeChangeNodes = [...orderedNodes, ...remaining];
+ },
+ isRouteChangeOrderChanged() {
+ if (!this.routeChangeOrderDirty) return false;
+ const currentAddresses = this.routeChangeNodes.map(node => node.address);
+ if (currentAddresses.length !== this.routeChangeOriginalAddresses.length) return true;
+ return currentAddresses.some(
+ (address, index) => address !== this.routeChangeOriginalAddresses[index]
+ );
+ },
+ formatRouteChangeOrderText(addresses = []) {
+ return addresses.filter(Boolean).join(' → ') || '-';
+ },
+ isWaybillCompleted(row = {}) {
+ const status = String(row.businessStatus || row.status || row.waybillStatus || '');
+ if (status === 'completed') return true;
+ const statusName = String(row.businessStatusName || row.statusName || '');
+ return statusName.includes('已完成');
+ },
+ isRouteChangeNodeLocked(node = {}) {
+ return Boolean(node?.locked);
+ },
+ syncRouteNodeLockedFlags(nodes = [], rows = this.selectedWaybillRows) {
+ return (nodes || []).map(node => {
+ const locked = (node.tags || []).some(tag => {
+ const match = String(tag).match(/^(装|卸)(\d+)$/);
+ if (!match) return false;
+ const row = rows[Number(match[2]) - 1];
+ return row ? this.isWaybillCompleted(row) : false;
+ });
+ return { ...node, locked };
+ });
},
openCommonAddressDialog(row, field) {
+ if (this.isWaybillCompleted(row)) return;
this.commonAddressTarget = { row, field };
this.commonAddressQuery = { addressName: '' };
this.commonAddressPage.currentPage = 1;
this.commonAddressVisible = true;
this.loadCommonAddresses();
},
+ openRouteChangeMapDialog(row, field) {
+ if (this.isWaybillCompleted(row)) return;
+ this.routeChangeMapTarget = { row, field };
+ this.routeChangeMapKeyword = row[`${field}Address`] || row[`${field}Name`] || '';
+ this.routeChangeMapSelected = {
+ longitude: row[`${field}Longitude`] || '',
+ latitude: row[`${field}Latitude`] || '',
+ detailAddress: this.routeChangeMapKeyword,
+ regionName: row[`${field}Name`] || '',
+ regionCode: row[`${field}RegionCode`] || '',
+ };
+ this.routeChangeMapSearchResults = [];
+ this.routeChangeMapStatus = this.routeChangeMapSelected.longitude
+ ? '已加载当前选点,可重新选点'
+ : '可搜索地址或点击地图选点';
+ this.routeChangeMapVisible = true;
+ },
+ initRouteChangeMap() {
+ this.loadDetailAmap()
+ .then(() =>
+ this.$nextTick(() => {
+ if (!this.$refs.routeChangeMap) return;
+ const center = this.routeChangeMapSelected.longitude
+ ? [
+ Number(this.routeChangeMapSelected.longitude),
+ Number(this.routeChangeMapSelected.latitude),
+ ]
+ : [116.40769, 39.89945];
+ if (!this.routeChangeMapInstance) {
+ this.routeChangeMapInstance = new window.AMap.Map(this.$refs.routeChangeMap, {
+ center,
+ zoom: this.routeChangeMapSelected.longitude ? 14 : 11,
+ });
+ window.AMap.plugin(['AMap.Geocoder'], () => {
+ this.routeChangeMapGeocoder = new window.AMap.Geocoder();
+ });
+ this.routeChangeMapInstance.on('click', event =>
+ this.pickRouteChangeMap(event.lnglat)
+ );
+ } else {
+ this.routeChangeMapInstance.resize();
+ }
+ if (this.routeChangeMapSelected.longitude) {
+ this.renderRouteChangeMapMarker(center);
+ }
+ })
+ )
+ .catch(() => ElMessage.error('高德地图组件加载失败'));
+ },
+ destroyRouteChangeMap() {
+ this.routeChangeMapMarker?.setMap?.(null);
+ this.routeChangeMapMarker = null;
+ this.routeChangeMapInstance?.destroy?.();
+ this.routeChangeMapInstance = null;
+ this.routeChangeMapGeocoder = null;
+ this.routeChangeMapSearchResults = [];
+ this.routeChangeMapTarget = null;
+ },
+ ensureRouteChangeMapGeocoder() {
+ if (this.routeChangeMapGeocoder) return Promise.resolve(this.routeChangeMapGeocoder);
+ return new Promise(resolve => {
+ window.AMap.plugin(['AMap.Geocoder'], () => {
+ this.routeChangeMapGeocoder = new window.AMap.Geocoder();
+ resolve(this.routeChangeMapGeocoder);
+ });
+ });
+ },
+ searchRouteChangeMap() {
+ const keyword = String(this.routeChangeMapKeyword || '').trim();
+ if (!keyword) {
+ ElMessage.warning('请输入地址关键词');
+ return;
+ }
+ this.routeChangeMapLoading = true;
+ this.loadDetailAmap()
+ .then(() => this.ensureRouteChangeMapGeocoder())
+ .then(
+ () =>
+ new Promise((resolve, reject) => {
+ this.routeChangeMapGeocoder.getLocation(keyword, (status, result) => {
+ if (status === 'complete') resolve(result);
+ else reject(result);
+ });
+ })
+ )
+ .then(result => {
+ this.routeChangeMapSearchResults = normalizeMapSearchResults(result, keyword);
+ const point = this.routeChangeMapSearchResults[0]?.location;
+ if (!point) {
+ this.routeChangeMapStatus = '未找到匹配地址';
+ ElMessage.warning('地图搜索无匹配地址');
+ return;
+ }
+ this.pickRouteChangeMap(point, keyword);
+ })
+ .catch(() => {
+ this.routeChangeMapStatus = '地图搜索失败';
+ ElMessage.error('地图搜索失败');
+ })
+ .finally(() => {
+ this.routeChangeMapLoading = false;
+ });
+ },
+ selectRouteChangeMapSearchResult(item) {
+ if (item?.location) {
+ this.pickRouteChangeMap(item.location, item.address || item.name || '');
+ }
+ },
+ pickRouteChangeMap(point, keyword) {
+ const longitude = point.getLng ? point.getLng() : point.lng;
+ const latitude = point.getLat ? point.getLat() : point.lat;
+ this.routeChangeMapSelected = {
+ longitude,
+ latitude,
+ detailAddress: keyword || this.routeChangeMapKeyword,
+ regionName: '',
+ regionCode: '',
+ };
+ this.renderRouteChangeMapMarker([longitude, latitude]);
+ this.routeChangeMapStatus = '已选点,可确认回填';
+ this.ensureRouteChangeMapGeocoder().then(() => {
+ this.routeChangeMapGeocoder.getAddress([longitude, latitude], (status, result) => {
+ if (status !== 'complete') return;
+ const component = result.regeocode?.addressComponent || {};
+ this.routeChangeMapSelected = {
+ ...this.routeChangeMapSelected,
+ detailAddress:
+ result.regeocode?.formattedAddress || this.routeChangeMapSelected.detailAddress,
+ regionName: [component.province, component.city, component.district]
+ .filter(Boolean)
+ .join(''),
+ regionCode: component.adcode || '',
+ };
+ this.routeChangeMapKeyword = this.routeChangeMapSelected.detailAddress;
+ this.routeChangeMapStatus = '已解析地址,可确认回填';
+ });
+ });
+ },
+ renderRouteChangeMapMarker(position) {
+ if (!this.routeChangeMapInstance) return;
+ this.routeChangeMapMarker?.setMap?.(null);
+ this.routeChangeMapMarker = new window.AMap.Marker({ position });
+ this.routeChangeMapMarker.setMap(this.routeChangeMapInstance);
+ this.routeChangeMapInstance.setCenter(position);
+ },
+ confirmRouteChangeMap() {
+ const target = this.routeChangeMapTarget;
+ if (!target || !this.routeChangeMapSelected.longitude) return;
+ const { row, field } = target;
+ row[`${field}Address`] =
+ this.routeChangeMapSelected.detailAddress || this.routeChangeMapKeyword || '';
+ if (this.routeChangeMapSelected.regionName) {
+ row[`${field}Name`] = this.routeChangeMapSelected.regionName;
+ }
+ row[`${field}Longitude`] = this.routeChangeMapSelected.longitude;
+ row[`${field}Latitude`] = this.routeChangeMapSelected.latitude;
+ if (this.routeChangeMapSelected.regionCode) {
+ row[`${field}RegionCode`] = this.routeChangeMapSelected.regionCode;
+ }
+ this.updateRouteChangeNodes();
+ this.routeChangeMapVisible = false;
+ },
async loadCommonAddresses() {
this.commonAddressLoading = true;
try {
@@ -2841,19 +3555,34 @@ export default {
row.departureAddress !== row.originalDepartureAddress ||
row.arrivalAddress !== row.originalArrivalAddress
);
- if (!changeRows.length && !this.routeChangeRemark) {
- ElMessage.warning('请修改地址或填写变更备注');
+ const orderChanged = this.isRouteChangeOrderChanged();
+ if (!changeRows.length && !orderChanged && !this.routeChangeRemark) {
+ ElMessage.warning('请修改地址、调整路线顺序或填写变更备注');
return;
}
- const content =
- changeRows
- .map(
- row =>
- `${row.waybillNo || '运单'}:发货地 ${row.originalDepartureAddress} → ${
- row.departureAddress
- };到货地 ${row.originalArrivalAddress} → ${row.arrivalAddress}`
- )
- .join(';') || '调整运输路线顺序';
+ const contentParts = [];
+ if (changeRows.length) {
+ contentParts.push(
+ changeRows
+ .map(
+ row =>
+ `${row.waybillNo || '运单'}:发货地 ${row.originalDepartureAddress} → ${
+ row.departureAddress
+ };到货地 ${row.originalArrivalAddress} → ${row.arrivalAddress}`
+ )
+ .join(';')
+ );
+ }
+ if (orderChanged) {
+ contentParts.push(
+ `路线顺序:${this.formatRouteChangeOrderText(
+ this.routeChangeOriginalAddresses
+ )} 调整为 ${this.formatRouteChangeOrderText(
+ this.routeChangeNodes.map(node => node.address)
+ )}`
+ );
+ }
+ const content = contentParts.join(';') || '调整运输路线顺序';
this.routeChangeRecords.unshift({
changeTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
changeUser: '当前用户',
@@ -3039,18 +3768,34 @@ export default {
},
buildRouteNodes(rows) {
const nodeMap = new Map();
- const appendNode = (address, displayAddress, tag) => {
+ const appendNode = (address, displayAddress, tag, locked) => {
if (!address) return;
- if (!nodeMap.has(address)) nodeMap.set(address, { address, displayAddress, tags: [] });
- nodeMap.get(address).tags.push(tag);
+ if (!nodeMap.has(address)) {
+ nodeMap.set(address, { address, displayAddress, tags: [], locked: false });
+ }
+ const node = nodeMap.get(address);
+ node.tags.push(tag);
+ if (locked) node.locked = true;
};
rows.forEach((row, rowIndex) => {
+ const locked = this.isWaybillCompleted(row);
const departure = row.departureAddress || row.departureName;
- appendNode(departure, this.formatCandidateAddress(row, 'departure'), `装${rowIndex + 1}`);
+ appendNode(
+ departure,
+ this.formatCandidateAddress(row, 'departure'),
+ `装${rowIndex + 1}`,
+ locked
+ );
});
rows.forEach((row, rowIndex) => {
+ const locked = this.isWaybillCompleted(row);
const arrival = row.arrivalAddress || row.arrivalName;
- appendNode(arrival, this.formatCandidateAddress(row, 'arrival'), `卸${rowIndex + 1}`);
+ appendNode(
+ arrival,
+ this.formatCandidateAddress(row, 'arrival'),
+ `卸${rowIndex + 1}`,
+ locked
+ );
});
return [...nodeMap.values()].map((node, index) => ({
...node,
@@ -3068,13 +3813,37 @@ export default {
return 'middle';
},
handleRouteDragStart(index) {
+ if (this.dialogReadonly || this.isRouteChangeNodeLocked(this.routeNodes[index])) {
+ this.routeDragIndex = -1;
+ return;
+ }
this.routeDragIndex = index;
},
handleRouteDrop(index) {
- if (this.routeDragIndex < 0 || this.routeDragIndex === index) return;
+ if (this.dialogReadonly || this.routeDragIndex < 0 || this.routeDragIndex === index) return;
+ if (
+ this.isRouteChangeNodeLocked(this.routeNodes[this.routeDragIndex]) ||
+ this.isRouteChangeNodeLocked(this.routeNodes[index])
+ ) {
+ this.routeDragIndex = -1;
+ return;
+ }
const rows = [...this.routeNodes];
const [target] = rows.splice(this.routeDragIndex, 1);
rows.splice(index, 0, target);
+ const originalLockedKeys = this.routeNodes.map(item =>
+ this.isRouteChangeNodeLocked(item) ? item.key : null
+ );
+ const nextLockedKeys = rows.map(item =>
+ this.isRouteChangeNodeLocked(item) ? item.key : null
+ );
+ if (
+ originalLockedKeys.some((key, lockedIndex) => key && key !== nextLockedKeys[lockedIndex])
+ ) {
+ ElMessage.warning('已完成运单对应的路线不可调整');
+ this.routeDragIndex = -1;
+ return;
+ }
this.routeDragIndex = -1;
if (!this.validateRouteEndpoints(rows)) return;
this.routeNodes = rows;
@@ -3112,7 +3881,7 @@ export default {
});
},
normalizeCargoRow(waybill, item = {}) {
- return {
+ return sanitizeCargoRow({
projectName: item.projectName || waybill.projectName || '',
cargoName: item.cargoName || item.goodsName || waybill.cargoName || '',
cargoType: item.cargoType || item.goodsType || waybill.cargoType || '',
@@ -3147,7 +3916,7 @@ export default {
waybill.specificationModel ||
'',
remark: item.remark || '',
- };
+ });
},
applyLoadingMileageFromDetail(form = this.dialogForm) {
let taskMileage = '';
@@ -3302,6 +4071,8 @@ export default {
};
this.applyLoadingMileageFromDetail(this.dialogForm);
ElMessage.success('暂存成功');
+ sessionStorage.setItem(loadingManageListRefreshKey, '1');
+ this.closeLoadingDialog();
this.loadTable();
} finally {
this.dialogSaving = '';
@@ -3944,6 +4715,12 @@ export default {
border: 1px solid #eff1f7;
background: #fff;
cursor: move;
+
+ &.is-locked {
+ cursor: not-allowed;
+ background: #f5f7fa;
+ color: #909399;
+ }
}
.loading-manage-dialog__route-handle {
@@ -4193,7 +4970,74 @@ export default {
gap: 8px;
}
.loading-detail__process-actions .el-select {
- width: 180px;
+ width: 200px;
+ }
+ .loading-detail__punch-record {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ }
+ .loading-detail__punch-record-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 14px;
+ font-weight: 600;
+ color: #303133;
+ }
+ .loading-detail__punch-record-line {
+ font-size: 13px;
+ color: #606266;
+
+ span + span {
+ margin-left: 12px;
+ }
+ }
+ .loading-detail__punch-record-photos,
+ .loading-detail__driver-upload-grid,
+ .loading-detail__voucher-image-grid {
+ display: grid;
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+ gap: 10px;
+ margin-top: 4px;
+ min-height: 80px;
+ }
+ .loading-detail__driver-upload-item,
+ .loading-detail__voucher-image-item {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ cursor: pointer;
+
+ .el-image {
+ display: block;
+ width: 100%;
+ aspect-ratio: 1;
+ border: 1px solid #eff1f7;
+ border-radius: 4px;
+ overflow: hidden;
+ background: #f5f7fa;
+ }
+ }
+ .loading-detail__voucher-image-item--folder .el-image {
+ aspect-ratio: auto;
+ padding: 12px;
+ background: transparent;
+ border: none;
+ }
+ .loading-detail__driver-upload-label,
+ .loading-detail__voucher-folder-name {
+ font-size: 12px;
+ color: #606266;
+ line-height: 18px;
+ text-align: center;
+ word-break: break-all;
+ }
+ .loading-detail__voucher-folder-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
}
.loading-detail__map {
width: 100%;
@@ -4201,12 +5045,25 @@ export default {
border: 1px solid #eff1f7;
}
}
+:global(.loading-route-change-dialog .el-dialog__body) {
+ background: #fff;
+}
.loading-route-change-dialog {
.loading-route-change-dialog__original {
color: #a8abb2;
line-height: 20px;
margin-bottom: 4px;
}
+ .loading-route-change-dialog__address-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ .el-input {
+ flex: 1;
+ min-width: 0;
+ }
+ }
.loading-route-change-dialog__location {
cursor: pointer;
color: #909399;
@@ -4227,6 +5084,16 @@ export default {
padding: 10px 12px;
background: #f5f6f7;
cursor: move;
+
+ &.is-locked {
+ cursor: not-allowed;
+ background: #eef0f3;
+ color: #909399;
+ }
+ }
+ .loading-route-change-dialog__route-handle-placeholder {
+ width: 14px;
+ height: 14px;
}
.loading-route-change-dialog__route-type {
width: 30px;
@@ -4255,6 +5122,27 @@ export default {
line-height: 22px;
}
}
+.loading-route-change-map-dialog {
+ .loading-route-change-map-dialog__toolbar {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 12px;
+
+ .el-input {
+ flex: 1;
+ min-width: 0;
+ }
+ }
+ .loading-route-change-map-dialog__map {
+ width: 100%;
+ min-width: 0;
+ height: 440px;
+ }
+ .loading-route-change-map-dialog__status {
+ margin-top: 8px;
+ color: #606266;
+ }
+}
.loading-common-address-dialog__toolbar {
display: flex;
gap: 8px;