Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/option/business/shipping-template.js
#	src/option/business/transport-plan.js
#	src/option/business/waybill-manage.js
#	src/views/business/components/business-crud-page.vue
#	src/views/business/components/master-order-detail.vue
#	src/views/business/components/master-order-editor.vue
#	src/views/business/components/waybill-import-dialog.vue
This commit is contained in:
2026-08-13 19:11:34 +08:00
39 changed files with 9092 additions and 1841 deletions
+201 -51
View File
@@ -46,7 +46,7 @@
/>
</el-select>
</el-form-item>
<el-form-item label="运输类型">
<el-form-item label="运输方式">
<el-select
v-model="searchForm.transportType"
clearable
@@ -262,22 +262,20 @@
</el-table-column>
<el-table-column label="配载子单号" prop="loadingSubNos" min-width="220">
<template #default="{ row }">
<div class="loading-manage-page__multi-line">
<el-link
v-for="item in splitText(row.loadingSubNos)"
:key="item"
type="primary"
class="loading-manage-page__sub-link"
>{{ item }}</el-link>
</div>
<span class="loading-manage-page__sub-nos">
{{ splitText(row.loadingSubNos).join('') || '-' }}
</span>
</template>
</el-table-column>
<el-table-column
label="车牌号/航班号/船号/班列号"
prop="vehicleNo"
min-width="210"
min-width="170"
show-overflow-tooltip
/>
>
<template #header>
<span class="loading-manage-page__vehicle-header">车牌号/航班号/船号<br />班列号</span>
</template>
</el-table-column>
<el-table-column label="司机" prop="driverName" min-width="120" show-overflow-tooltip />
<el-table-column label="联系电话" prop="driverPhone" min-width="140" show-overflow-tooltip />
<el-table-column label="承运类型" prop="carrierType" min-width="120" show-overflow-tooltip />
@@ -285,17 +283,32 @@
label="发货地"
prop="departureAddress"
min-width="180"
show-overflow-tooltip
/>
<el-table-column label="途经地" prop="transitAddress" min-width="190" show-overflow-tooltip />
<el-table-column label="到货地" prop="arrivalAddress" min-width="180" show-overflow-tooltip />
>
<template #default="{ row }">
<div class="loading-manage-page__address-cell">{{ formatRouteAddress(row, 'departure') }}</div>
</template>
</el-table-column>
<el-table-column label="途经地" prop="transitAddress" min-width="190">
<template #default="{ row }">
<div class="loading-manage-page__address-cell">{{ formatRouteAddress(row, 'transit') }}</div>
</template>
</el-table-column>
<el-table-column label="到货地" prop="arrivalAddress" min-width="180">
<template #default="{ row }">
<div class="loading-manage-page__address-cell">{{ formatRouteAddress(row, 'arrival') }}</div>
</template>
</el-table-column>
<el-table-column label="承运商" prop="carrierName" min-width="160" show-overflow-tooltip />
<el-table-column
label="运输类型"
label="运输方式"
prop="transportType"
min-width="150"
show-overflow-tooltip
/>
>
<template #default="{ row }">
{{ transportTypeLabel(row.transportType) }}
</template>
</el-table-column>
<el-table-column label="数据来源" prop="dataSource" min-width="130" show-overflow-tooltip />
<el-table-column label="创建时间" prop="createTime" min-width="170" show-overflow-tooltip sortable/>
<el-table-column label="更新时间" prop="updateTime" min-width="170" show-overflow-tooltip sortable/>
@@ -369,7 +382,7 @@
</section-card>
<section-card title="货物信息"><el-table :data="cargoRows" border height="220"><el-table-column type="index" label="序号" width="70"/><el-table-column prop="projectName" label="项目名称" min-width="140"/><el-table-column prop="cargoName" label="货物名称" min-width="140"/><el-table-column prop="cargoType" label="货物类型" min-width="120"/><el-table-column prop="packageType" label="包装" min-width="100"/><el-table-column prop="weight" label="重量" min-width="110"/><el-table-column prop="volume" label="体积" min-width="110"/><el-table-column prop="quantity" label="数量" min-width="90"/><el-table-column prop="materialCode" label="物料编码" min-width="120"/><el-table-column prop="equipmentCode" label="设备编码" min-width="120"/><el-table-column prop="brand" label="品牌" min-width="120"/><el-table-column prop="specificationModel" label="规格型号" min-width="140"/><el-table-column prop="remark" label="备注" min-width="160"/><el-table-column prop="unitPrice" label="货物单价" min-width="140"/><el-table-column prop="planDate" label="计划日期" min-width="130"/></el-table></section-card>
<section-card title="关联运单信息"><el-table :data="selectedWaybillRows" border><el-table-column type="index" label="序号" width="70"/><el-table-column prop="waybillNo" label="运单号" min-width="150"><template #default="{ row }"><el-link type="primary">{{ row.waybillNo || '-' }}</el-link></template></el-table-column><el-table-column label="配载单号" min-width="150"><template #default>{{ dialogForm.loadingNo || '-' }}</template></el-table-column><el-table-column prop="projectName" label="项目名称" min-width="150"/><el-table-column prop="customerName" label="客户" min-width="140"/><el-table-column prop="departureAddress" label="发货地址" min-width="220" show-overflow-tooltip/><el-table-column prop="arrivalAddress" label="到货地址" min-width="220" show-overflow-tooltip/></el-table></section-card>
<div class="loading-detail__bottom-grid"><section-card title="运输节点"><template #extra><div class="loading-detail__process-actions"><el-select v-model="processProjectId" filterable remote clearable placeholder="切换项目" :remote-method="loadProjectOptions" :loading="projectLoading" @visible-change="visible => visible && loadProjectOptions()" @change="handleProcessProjectChange"><el-option v-for="item in projectOptions" :key="item.id" :label="item.projectName || item.name" :value="item.id" /></el-select><el-button plain @click="handleProcessAction('supplement')">补录执行</el-button><el-button plain @click="handleProcessAction('batch')">批量补录</el-button></div></template><el-timeline v-if="processNodes.length"><el-timeline-item v-for="(item, index) in processNodes" :key="item.id || index" :timestamp="item.time || ''">{{ item.name || item.nodeName || item.label }}</el-timeline-item></el-timeline><el-empty v-else description="暂无运输节点" :image-size="50"/></section-card><section-card title="物流轨迹"><template #extra><div class="loading-detail__track-actions"><el-button plain @click="handleTrackAction('playback')">轨迹回放</el-button><el-button plain @click="handleTrackAction('locate')">实时定位</el-button></div></template><div ref="detailAmap" class="loading-detail__map"></div></section-card></div>
<div class="loading-detail__bottom-grid"><section-card title="运输节点"><template #extra><div class="loading-detail__process-actions"><el-select v-model="processProjectId" filterable placeholder="切换项目" @change="handleProcessProjectChange"><el-option v-for="item in processProjectOptions" :key="item.id" :label="item.projectName" :value="item.id" /></el-select><el-button plain @click="handleProcessAction('supplement')">补录执行</el-button><el-button plain @click="handleProcessAction('batch')">批量补录</el-button></div></template><el-timeline v-if="processNodes.length"><el-timeline-item v-for="(item, index) in processNodes" :key="item.id || index" :timestamp="item.time || ''">{{ item.name || item.nodeName || item.label }}</el-timeline-item></el-timeline><el-empty v-else description="暂无运输节点" :image-size="50"/></section-card><section-card title="物流轨迹"><template #extra><div class="loading-detail__track-actions"><el-button plain @click="handleTrackAction('playback')">轨迹回放</el-button><el-button plain @click="handleTrackAction('locate')">实时定位</el-button></div></template><div ref="detailAmap" class="loading-detail__map"></div></section-card></div>
</div>
<el-form
v-else
@@ -384,7 +397,7 @@
<template #extra>
<el-link v-if="!dialogReadonly" type="primary" @click="openCandidateSearch">筛选</el-link>
</template>
<div class="loading-manage-dialog__candidate-search" v-if="candidateSearchVisible">
<div class="loading-manage-dialog__candidate-search">
<el-row :gutter="16">
<el-col :span="6">
<el-form-item label="运单号">
@@ -424,6 +437,7 @@
/>
</el-form-item>
</el-col>
<template v-if="candidateSearchVisible">
<el-col :span="6">
<el-form-item label="货物名称">
<el-input v-model="candidateQuery.cargoName" clearable placeholder="请输入" />
@@ -545,7 +559,7 @@
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="运输类型">
<el-form-item label="运输方式">
<el-select
v-model="candidateQuery.transportType"
clearable
@@ -562,14 +576,24 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="loading-manage-dialog__candidate-actions">
</template>
<el-col
:span="candidateSearchVisible ? 24 : 6"
class="loading-manage-dialog__candidate-actions"
>
<el-button type="primary" @click="handleCandidateSearch">查询</el-button>
<el-button @click="handleCandidateReset">重置</el-button>
<el-link type="primary" @click="candidateSearchVisible = false"
>收起</el-link>
<el-link
type="primary"
@click="candidateSearchVisible = !candidateSearchVisible"
>{{ candidateSearchVisible ? '收起' : '展开' }}</el-link>
</el-col>
</el-row>
</div>
</section-card>
<section-card title="配载信息" class="loading-manage-dialog__load-info-card">
<section-card title="待配载运单">
<el-table
ref="candidateTableRef"
v-loading="candidateLoading"
@@ -601,10 +625,14 @@
/>
<el-table-column
prop="transportType"
label="运输类型"
label="运输方式"
min-width="130"
show-overflow-tooltip
/>
>
<template #default="{ row }">
{{ transportTypeLabel(row.transportType) }}
</template>
</el-table-column>
<el-table-column
prop="cargoName"
label="货物名称"
@@ -736,9 +764,8 @@
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
<el-table-column prop="cargoType" label="货物类型" min-width="140" />
<el-table-column prop="packageType" label="包装" min-width="100" />
<el-table-column prop="weight" label="重量" min-width="120" />
<el-table-column prop="volume" label="体积" min-width="120" />
<el-table-column prop="quantity" label="数量" min-width="100" />
<el-table-column prop="quantityUnit" label="单位" min-width="100" />
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
<el-table-column prop="equipmentCode" label="设备编码" min-width="130" />
<el-table-column prop="brand" label="品牌" min-width="120" />
@@ -759,7 +786,7 @@
/>
</div>
<div class="loading-manage-dialog__grid">
<el-form-item v-if="dialogForm.carrierType !== '自运'" label="承运商" required>
<el-form-item v-if="!['自运', '网货平台'].includes(dialogForm.carrierType)" label="承运商" required>
<el-select
v-model="dialogForm.carrierName"
clearable
@@ -779,7 +806,7 @@
/>
</el-select>
</el-form-item>
<el-form-item label="司机" :required="dialogForm.carrierType === '自运'">
<el-form-item label="司机" :required="['自运', '网货平台'].includes(dialogForm.carrierType)">
<el-select
v-model="dialogForm.driverName"
clearable
@@ -802,7 +829,7 @@
/>
</el-select>
</el-form-item>
<el-form-item label="手机号" :required="dialogForm.carrierType === '自运'">
<el-form-item label="手机号" :required="['自运', '网货平台'].includes(dialogForm.carrierType)">
<el-input v-model="dialogForm.driverPhone" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="车牌号" required>
@@ -855,6 +882,7 @@
</el-form-item>
</div>
</section-card>
</section-card>
</el-form>
</div>
@@ -927,6 +955,7 @@ import {
import { ElMessage, ElMessageBox } from 'element-plus';
import { Location, Rank, Refresh, Setting } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
import { isMobile } from '@/utils/validate';
const defaultTransportType = '';
const defaultCandidateTransportType = '';
@@ -1111,6 +1140,7 @@ export default {
config: loadingManageConfig,
processNodes: [],
processProjectId: '',
processProjectOptions: [],
routeChangeVisible: false,
routeChangeTab: 'change',
routeChangeRows: [],
@@ -1229,6 +1259,34 @@ export default {
.map(item => item.trim())
.filter(Boolean);
},
formatRouteAddress(row, type) {
const prefix = type === 'departure' ? 'departure' : type === 'arrival' ? 'arrival' : 'transit';
const values = [
row[`${prefix}RegionName`],
row[`${prefix}DistrictName`],
row[`${prefix}Address`],
]
.filter(Boolean)
.flatMap(value => this.splitText(value))
.map(value => this.formatCityDistrict(value))
.filter(Boolean);
return [...new Set(values)].join(',') || '-';
},
formatCityDistrict(value) {
const text = String(value || '').trim();
if (!text) return '';
const cityIndex = text.indexOf('市');
const provinceIndex = Math.max(text.lastIndexOf('省', cityIndex), text.lastIndexOf('自治区', cityIndex));
const cityStart = provinceIndex > -1 ? provinceIndex + 1 : 0;
const districtStart = cityIndex > -1 ? cityIndex + 1 : cityStart;
const districtMatch = text
.slice(districtStart)
.match(/(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/);
if (districtMatch) {
return text.slice(cityStart, districtStart + districtMatch.index + districtMatch[0].length);
}
return cityIndex > -1 ? text.slice(cityStart, cityIndex + 1) : text;
},
rowActions(row) {
const status = row.businessStatus;
if (status === 'draft') {
@@ -1286,12 +1344,11 @@ export default {
...createDialogForm(),
...(res.data?.data || res.data || {}),
};
this.processProjectId = this.dialogForm.projectId || '';
await this.loadProjectOptions(this.dialogForm.projectName || '');
await this.restoreWaybillRows(
this.dialogForm.waybillIdsJson,
this.dialogForm.loadingSubNos
);
this.syncProcessProjectOptions();
this.restoreRouteAndCargo();
this.restoreRouteChangeRecords();
await this.loadProcessNodes();
@@ -1326,6 +1383,7 @@ export default {
this.candidateSearchVisible = false;
this.processNodes = [];
this.processProjectId = '';
this.processProjectOptions = [];
},
clearLoadingDialog() {
this.resetDialogData();
@@ -1355,13 +1413,62 @@ export default {
return;
}
try {
const rows = unwrapRecords(await getProcessConfigList(1, 20, { projectId: this.processProjectId, status: 1 }));
const config = rows.find(item => item.nodeConfigJson) || {};
this.processNodes = parseJsonArray(config.nodeConfigJson);
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 || '')
.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),
});
});
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))
);
},
async handleProcessProjectChange() {
await this.loadProcessNodes();
},
@@ -1605,7 +1712,7 @@ export default {
this.dialogForm.loadingSubNos = rows
.map(row => row.waybillNo)
.filter(Boolean)
.join('\n');
.join('');
this.dialogForm.waybillIdsJson = JSON.stringify(rows.map(row => row.id).filter(Boolean));
this.dialogForm.projectName = uniqueText(rows, 'projectName');
this.dialogForm.customerName = uniqueText(rows, 'customerName');
@@ -1697,10 +1804,21 @@ export default {
weight: item.weight || item.cargoWeight || '',
volume: item.volume || item.cargoVolume || '',
quantity: item.quantity || item.cargoQuantity || waybill.quantity || '',
quantityUnit:
item.quantityUnit ||
item.goodsQuantityUnit ||
item.cargoUnit ||
item.unit ||
waybill.quantityUnit ||
waybill.goodsQuantityUnit ||
waybill.cargoUnit ||
waybill.unit ||
'',
planDate: item.planDate || waybill.startDate || '',
unitPrice: item.unitPrice || waybill.unitPrice || '',
materialCode: item.materialCode || '',
equipmentCode: item.equipmentCode || '',
equipmentCode:
item.equipmentCode || item.deviceCode || waybill.equipmentCode || waybill.deviceCode || '',
brand: item.brand || item.brandName || waybill.brand || '',
specificationModel:
item.specificationModel || item.specModel || item.specification || waybill.specificationModel || '',
@@ -1736,7 +1854,15 @@ export default {
return false;
}
if (!this.dialogForm.transportType) {
ElMessage.warning('请选择运输类型');
ElMessage.warning('请选择运输方式');
return false;
}
const invalidPhone = [
[this.dialogForm.driverPhone, '手机号'],
[this.dialogForm.escortPhone, '押运人手机号'],
].find(([value]) => String(value || '').trim() && !isMobile(value));
if (invalidPhone) {
ElMessage.warning(`${invalidPhone[1]}格式不正确`);
return false;
}
if (draftMode) {
@@ -1754,7 +1880,7 @@ export default {
ElMessage.warning('请输入车牌号');
return false;
}
if (this.dialogForm.carrierType === '自运') {
if (['自运', '网货平台'].includes(this.dialogForm.carrierType)) {
if (!this.dialogForm.driverName || !this.dialogForm.driverPhone) {
ElMessage.warning('请输入司机和司机手机号');
return false;
@@ -1855,6 +1981,7 @@ export default {
{
...this.buildQueryParams(this.searchForm),
ids,
exportColumns: JSON.stringify(loadingManageConfig.exportColumns),
},
{
feedback: true,
@@ -1864,7 +1991,7 @@ export default {
});
},
handleCarrierTypeChange() {
if (this.dialogForm.carrierType === '自运') {
if (['自运', '网货平台'].includes(this.dialogForm.carrierType)) {
this.dialogForm.carrierName = '';
}
},
@@ -1991,17 +2118,27 @@ export default {
width: 100%;
}
.loading-manage-page__link,
.loading-manage-page__sub-link {
.loading-manage-page__link {
padding: 0;
min-height: auto;
}
.loading-manage-page__multi-line {
display: flex;
flex-direction: column;
align-items: flex-start;
line-height: 1.6;
.loading-manage-page__sub-nos {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.loading-manage-page__address-cell {
line-height: 20px;
white-space: pre-line;
word-break: break-word;
}
.loading-manage-page__vehicle-header {
line-height: 20px;
white-space: nowrap;
}
.loading-manage-page__status {
@@ -2069,12 +2206,12 @@ export default {
.loading-manage-dialog__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0 24px;
}
.loading-manage-dialog__span-2 {
grid-column: span 2;
grid-column: 1 / -1;
}
.loading-manage-dialog__title-action {
@@ -2118,6 +2255,10 @@ export default {
}
@media (max-width: 1200px) {
.loading-manage-dialog__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.loading-manage-dialog__waybill-route-layout {
grid-template-columns: 1fr;
}
@@ -2233,5 +2374,14 @@ export default {
}
.loading-route-change-dialog { .loading-route-change-dialog__original { color: #a8abb2; line-height: 20px; margin-bottom: 4px; }.loading-route-change-dialog__location { cursor: pointer; color: #909399; }.loading-route-change-dialog__hint { color: #909399; }.loading-route-change-dialog__route-list { display: flex; flex-direction: column; gap: 8px; }.loading-route-change-dialog__route-item { display: grid; grid-template-columns: 34px minmax(0, 1fr) auto 24px; align-items: center; gap: 12px; padding: 10px 12px; background: #f5f6f7; cursor: move; }.loading-route-change-dialog__route-type { width: 30px; height: 30px; border-radius: 50%; background: #dcdfe6; text-align: center; line-height: 30px; }.loading-route-change-dialog__tags { display: flex; gap: 4px; }.loading-route-change-dialog__record-content { white-space: normal; overflow-wrap: anywhere; line-height: 22px; } }
.loading-common-address-dialog__toolbar { display: flex; gap: 8px; margin-bottom: 12px; }.loading-common-address-dialog__toolbar .el-input { flex: 1; }
@media (max-width: 900px) { .loading-detail__task-row, .loading-detail__bottom-grid { grid-template-columns: 1fr; } }
@media (max-width: 900px) {
.loading-manage-dialog__grid {
grid-template-columns: 1fr;
}
.loading-detail__task-row,
.loading-detail__bottom-grid {
grid-template-columns: 1fr;
}
}
</style>