调整业务模块

This commit is contained in:
2026-09-04 05:23:34 +08:00
parent c63c7c3e33
commit e789f1647a
10 changed files with 2082 additions and 591 deletions
@@ -1,3 +1,4 @@
<!-- 该文件已废弃 -->
<template> <template>
<basic-container <basic-container
:class="[ :class="[
@@ -3267,7 +3268,11 @@
</template> </template>
</el-dialog> </el-dialog>
<waybill-import-dialog v-if="config.enableWaybillImport" v-model="waybillImportBox" /> <waybill-import-dialog
v-if="config.enableWaybillImport"
v-model="waybillImportBox"
@closed="refreshChange"
/>
<flow-design <flow-design
v-if="website.design.designMode" v-if="website.design.designMode"
@@ -15494,7 +15499,6 @@ export default {
&__waybill-route-addr { &__waybill-route-addr {
overflow: hidden; overflow: hidden;
color: #606266; color: #606266;
font-size: 12px;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
@@ -3,7 +3,7 @@
<section v-if="master" class="detail-overview"> <section v-if="master" class="detail-overview">
<div class="detail-heading"><div><h2>多联总单详情 <span>|</span> {{ master.masterNo }}</h2><el-tag :type="statusType(master.businessStatus)" class="status-text">{{ statusName(master.businessStatus) }}</el-tag><el-tag v-if="transportFlowLabel" type="info" class="transport-flow-tag">{{ transportFlowLabel }}</el-tag></div><el-button @click="$emit('back')">取消</el-button></div> <div class="detail-heading"><div><h2>多联总单详情 <span>|</span> {{ master.masterNo }}</h2><el-tag :type="statusType(master.businessStatus)" class="status-text">{{ statusName(master.businessStatus) }}</el-tag><el-tag v-if="transportFlowLabel" type="info" class="transport-flow-tag">{{ transportFlowLabel }}</el-tag></div><el-button @click="$emit('back')">取消</el-button></div>
<dl class="detail-meta"><div><dt>客户</dt><dd>{{ master.customerName || '-' }}</dd></div><div><dt>合同编号</dt><dd>{{ master.contractNo || '-' }}</dd></div><div><dt>项目</dt><dd>{{ master.projectName || '-' }}</dd></div><div class="detail-meta__attachments"><dt>附件</dt><dd><template v-if="attachments.length"><el-link v-for="file in attachments" :key="file.url || file.link || file.name || file.originalName" type="primary" @click="previewAttachment(file)">{{ attachmentName(file) }}</el-link></template><span v-else>-</span></dd></div></dl> <dl class="detail-meta"><div><dt>客户</dt><dd>{{ master.customerName || '-' }}</dd></div><div><dt>合同编号</dt><dd>{{ master.contractNo || '-' }}</dd></div><div><dt>项目</dt><dd>{{ master.projectName || '-' }}</dd></div><div class="detail-meta__attachments"><dt>附件</dt><dd><template v-if="attachments.length"><el-link v-for="file in attachments" :key="file.url || file.link || file.name || file.originalName" type="primary" @click="previewAttachment(file)">{{ attachmentName(file) }}</el-link></template><span v-else>-</span></dd></div></dl>
<div class="route-map"><template v-for="(route, index) in segments" :key="route.segmentNo"><div class="route-map__node"><span :class="['route-badge', index ? 'middle' : 'start']">{{ index ? '经' : '起' }}</span><strong>{{ route.departureName || '-' }}</strong><small>{{ route.departureAddress || '-' }}</small><small class="route-map__progress">{{ index ? `已到达 ${quantity(segments[index - 1].arrivedQuantity)}/${quantity(master.totalQuantity)}` : `已调度 ${quantity(route.dispatchedQuantity)}/${quantity(master.totalQuantity)}` }}</small><small v-if="index" class="route-map__progress">已调度 {{ quantity(route.dispatchedQuantity) }}/{{ quantity(master.totalQuantity) }}</small></div><div class="route-map__line"></div><div v-if="index === segments.length - 1" class="route-map__node"><span class="route-badge end">终</span><strong>{{ route.arrivalName || '-' }}</strong><small>{{ route.arrivalAddress || '-' }}</small><small class="route-map__progress">已到达 {{ quantity(route.arrivedQuantity) }}/{{ quantity(master.totalQuantity) }}</small></div></template></div> <div class="route-map"><template v-for="(route, index) in segments" :key="route.segmentNo"><div class="route-map__node"><span :class="['route-badge', index ? 'middle' : 'start']">{{ index ? '经' : '起' }}</span><strong>{{ segmentLocationName(route, 'departure') }}</strong><small>{{ route.departureAddress || '-' }}</small><small class="route-map__progress">{{ index ? `已到达 ${quantity(segments[index - 1].arrivedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(segments[index - 1])}` : `已调度 ${quantity(route.dispatchedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(route)}` }}</small><small v-if="index" class="route-map__progress">已调度 {{ quantity(route.dispatchedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div><div class="route-map__line"></div><div v-if="index === segments.length - 1" class="route-map__node"><span class="route-badge end">终</span><strong>{{ segmentLocationName(route, 'arrival') }}</strong><small>{{ route.arrivalAddress || '-' }}</small><small class="route-map__progress">已到达 {{ quantity(route.arrivedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div></template></div>
</section> </section>
<el-dialog v-model="documentPreviewVisible" :title="previewFile.name || '附件预览'" append-to-body destroy-on-close width="90%" top="4vh"> <el-dialog v-model="documentPreviewVisible" :title="previewFile.name || '附件预览'" append-to-body destroy-on-close width="90%" top="4vh">
@@ -14,10 +14,10 @@
<section v-if="master" class="execution-detail-card"> <section v-if="master" class="execution-detail-card">
<div class="execution-detail-heading"><h3>分段执行明细</h3></div> <div class="execution-detail-heading"><h3>分段执行明细</h3></div>
<section v-for="(route, index) in segments" :key="route.segmentNo" class="segment-detail"> <section v-for="(route, index) in segments" :key="route.segmentNo" class="segment-detail">
<header @click="toggleSegment(route.segmentNo)"><h3><span class="segment-index">{{ segmentNumber(route, index) }}</span><span>{{ route.departureName || '-' }} {{ route.arrivalName || '-' }}</span><span class="segment-detail__transport-type">{{ segmentTransportType(route) || '-' }}</span><span class="segment-detail__progress-bar"><span :style="{ width: `${segmentProgress(route)}%` }"></span></span><span class="segment-detail__progress">已调度 <em>{{ quantity(route.dispatchedQuantity) }}</em> / {{ quantity(plannedQuantity(route)) }} {{ plannedUnit(route) }}</span></h3><div class="segment-detail__header-right"><el-tooltip :content="isSegmentExpanded(route.segmentNo) ? '折叠' : '展开'" placement="top"><el-button circle :icon="isSegmentExpanded(route.segmentNo) ? ArrowUp : ArrowDown" @click.stop="toggleSegment(route.segmentNo)" /></el-tooltip></div></header> <header @click="toggleSegment(route.segmentNo)"><h3><span class="segment-index">{{ segmentNumber(route, index) }}</span><span>{{ segmentLocationName(route, 'departure') }} {{ segmentLocationName(route, 'arrival') }}</span><span class="segment-detail__transport-type">{{ segmentTransportType(route) || '-' }}</span><span class="segment-detail__progress-bar"><span :style="{ width: `${segmentProgress(route)}%` }"></span></span><span class="segment-detail__progress">已调度 <em>{{ quantity(route.dispatchedQuantity) }}</em> / {{ quantity(plannedQuantity(route)) }} {{ plannedUnit(route) }}</span><el-button text class="segment-toggle" @click.stop="toggleSegment(route.segmentNo)"><span>{{ isSegmentExpanded(route.segmentNo) ? '收起' : '展开' }}</span><el-icon class="segment-toggle__icon"><component :is="isSegmentExpanded(route.segmentNo) ? CaretTop : CaretBottom" /></el-icon></el-button></h3></header>
<div v-show="isSegmentExpanded(route.segmentNo)" class="segment-execution"> <div v-show="isSegmentExpanded(route.segmentNo)" class="segment-execution">
<div class="segment-stats"><div><span>总量</span><strong>{{ quantity(master.totalQuantity) }}<small></small></strong></div><div><span>已调度</span><strong class="dispatched">{{ quantity(route.dispatchedQuantity) }}<small></small></strong></div><div><span>剩余</span><strong class="remaining">{{ quantity(remainingQuantity(route)) }}<small></small></strong></div></div> <div class="segment-stats"><div><span>总量</span><strong>{{ quantity(master.totalQuantity) }}<small></small></strong></div><div><span>已调度</span><strong class="dispatched">{{ quantity(route.dispatchedQuantity) }}<small></small></strong></div><div><span>剩余</span><strong class="remaining">{{ quantity(remainingQuantity(route)) }}<small></small></strong></div></div>
<el-table :data="route.waybills || []" border class="waybill-table"><el-table-column label="运单号" min-width="180"><template #default="{ row }"><el-link type="primary" @click="openWaybill(row)">{{ row.waybillNo }}</el-link></template></el-table-column><el-table-column prop="carrierName" label="承运商" min-width="190" /><el-table-column label="司机/车牌" min-width="180"><template #default="{ row }">{{ driverVehicle(row) }}</template></el-table-column><el-table-column prop="cargoType" label="货物类型" min-width="120" /><el-table-column label="数量(吨)" width="130"><template #default="{ row }">{{ quantity(row.quantity) }}</template></el-table-column><el-table-column label="状态" width="130"><template #default="{ row }"><el-tag :type="waybillStatusType(row.businessStatus)" size="small" class="status-text">{{ waybillStatusName(row.businessStatus) }}</el-tag></template></el-table-column><el-table-column prop="createTime" label="创建时间" min-width="180" /></el-table> <el-table :data="route.waybills || []" border class="waybill-table"><el-table-column label="运单号" min-width="180"><template #default="{ row }"><el-link type="primary" @click="openWaybill(row)">{{ row.waybillNo }}</el-link></template></el-table-column><el-table-column prop="carrierName" label="承运商" min-width="190" /><el-table-column :label="isRoadTransport(route) ? '司机/车牌' : '船长/船号'" min-width="180"><template #default="{ row }">{{ driverVehicle(row, route) }}</template></el-table-column><el-table-column prop="cargoType" label="货物类型" min-width="120" /><el-table-column label="数量(吨)" width="130"><template #default="{ row }">{{ quantity(row.quantity) }}</template></el-table-column><el-table-column label="状态" width="130"><template #default="{ row }"><el-tag :type="waybillStatusType(row.businessStatus)" effect="dark" size="small">{{ waybillStatusName(row.businessStatus) }}</el-tag></template></el-table-column><el-table-column prop="createTime" label="创建时间" min-width="180" /></el-table>
<el-empty v-if="!(route.waybills || []).length" description="暂无运单明细" :image-size="56" /> <el-empty v-if="!(route.waybills || []).length" description="暂无运单明细" :image-size="56" />
<div v-if="(route.transportPlans || []).length" class="transport-plan-detail"><h4>关联计划单</h4><el-table :data="route.transportPlans" border><el-table-column label="计划单号" min-width="160"><template #default="{ row }"><el-link type="primary" @click="openPlan(row)">{{ row.planNo }}</el-link></template></el-table-column><el-table-column prop="planName" label="计划名称" min-width="180" /><el-table-column prop="transportType" label="运输方式" width="130" /><el-table-column prop="planStartDate" label="计划开始日期" width="130" /><el-table-column prop="planEndDate" label="计划结束日期" width="130" /><el-table-column prop="businessStatus" label="状态" width="110" /></el-table></div> <div v-if="(route.transportPlans || []).length" class="transport-plan-detail"><h4>关联计划单</h4><el-table :data="route.transportPlans" border><el-table-column label="计划单号" min-width="160"><template #default="{ row }"><el-link type="primary" @click="openPlan(row)">{{ row.planNo }}</el-link></template></el-table-column><el-table-column prop="planName" label="计划名称" min-width="180" /><el-table-column prop="transportType" label="运输方式" width="130" /><el-table-column prop="planStartDate" label="计划开始日期" width="130" /><el-table-column prop="planEndDate" label="计划结束日期" width="130" /><el-table-column prop="businessStatus" label="状态" width="110" /></el-table></div>
</div> </div>
@@ -26,20 +26,14 @@
<section v-if="master" class="master-goods-card"> <section v-if="master" class="master-goods-card">
<div class="master-goods-card__heading"><h3>货物信息</h3></div> <div class="master-goods-card__heading"><h3>货物信息</h3></div>
<el-table :data="master.goods || []" border> <el-table :data="master.goods || []" border>
<el-table-column type="index" label="序号" width="64" />
<el-table-column prop="cargoName" label="货物名称" min-width="150" /> <el-table-column prop="cargoName" label="货物名称" min-width="150" />
<el-table-column prop="cargoType" label="货物类型" min-width="130" /> <el-table-column prop="cargoType" label="类型" min-width="130" />
<el-table-column prop="packageType" label="包装" min-width="120" /> <el-table-column prop="packageType" label="包装" min-width="120" />
<el-table-column label="重量(吨)" min-width="110"><template #default="{ row }">{{ row.weight ?? row.weightTon ?? (row.quantityUnit === '吨' ? row.quantity : '-') }}</template></el-table-column>
<el-table-column label="体积(方)" min-width="110"><template #default="{ row }">{{ row.volume ?? row.volumeCubic ?? '-' }}</template></el-table-column>
<el-table-column label="数量" min-width="100"><template #default="{ row }">{{ row.quantity ?? '-' }}{{ row.quantityUnit ? ` ${row.quantityUnit}` : '' }}</template></el-table-column> <el-table-column label="数量" min-width="100"><template #default="{ row }">{{ row.quantity ?? '-' }}{{ row.quantityUnit ? ` ${row.quantityUnit}` : '' }}</template></el-table-column>
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
<el-table-column prop="deviceCode" label="设备编码" min-width="130" />
<el-table-column prop="brand" label="品牌" min-width="120" /> <el-table-column prop="brand" label="品牌" min-width="120" />
<el-table-column label="规格型号" min-width="150"><template #default="{ row }">{{ [row.specification, row.model].filter(Boolean).join('/') || '-' }}</template></el-table-column> <el-table-column prop="specification" label="规格" min-width="150" />
<el-table-column prop="remark" label="备注" min-width="160" /> <el-table-column prop="model" label="型号" min-width="150" />
<el-table-column label="货物单价(元)" min-width="130"><template #default="{ row }">{{ row.unitPrice ?? '-' }}</template></el-table-column> <el-table-column prop="materialCode" label="物料编码" min-width="130" />
<el-table-column label="计划日期" min-width="130"><template #default="{ row }">{{ row.planDate || row.planStartDate || '-' }}</template></el-table-column>
</el-table> </el-table>
<el-empty v-if="!(master.goods || []).length" description="暂无货物信息" :image-size="56" /> <el-empty v-if="!(master.goods || []).length" description="暂无货物信息" :image-size="56" />
</section> </section>
@@ -48,7 +42,7 @@
<script> <script>
import * as api from '@/api/business/master-order'; import * as api from '@/api/business/master-order';
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue'; import { CaretBottom, CaretTop } from '@element-plus/icons-vue';
import { ElImageViewer } from 'element-plus'; import { ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue'; import { OpenFileViewer } from '@open-file-viewer/vue';
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core'; import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
@@ -61,7 +55,7 @@ export default {
components: { ElImageViewer, OpenFileViewer }, components: { ElImageViewer, OpenFileViewer },
props: { id: [String, Number] }, props: { id: [String, Number] },
emits: ['back'], emits: ['back'],
data() { return { loading: false, master: null, expandedSegments: {}, ArrowDown, ArrowUp, imagePreviewVisible: false, imagePreviewUrls: [], imagePreviewIndex: 0, documentPreviewVisible: false, previewFile: {}, viewerPlugins, viewerToolbar: { download: true, fullscreen: true, print: true, rotate: true, zoom: true } }; }, data() { return { loading: false, master: null, expandedSegments: {}, CaretBottom, CaretTop, imagePreviewVisible: false, imagePreviewUrls: [], imagePreviewIndex: 0, documentPreviewVisible: false, previewFile: {}, viewerPlugins, viewerToolbar: { download: true, fullscreen: true, print: true, rotate: true, zoom: true } }; },
computed: { computed: {
segments() { segments() {
const routes = this.master?.routeProgress || this.master?.routes || []; const routes = this.master?.routeProgress || this.master?.routes || [];
@@ -94,6 +88,36 @@ export default {
toggleSegment(segmentNo) { this.expandedSegments = { ...this.expandedSegments, [segmentNo]: !this.isSegmentExpanded(segmentNo) }; }, toggleSegment(segmentNo) { this.expandedSegments = { ...this.expandedSegments, [segmentNo]: !this.isSegmentExpanded(segmentNo) }; },
remainingQuantity(route) { return Math.max(0, Number(this.master?.totalQuantity || 0) - Number(route.dispatchedQuantity || 0)); }, remainingQuantity(route) { return Math.max(0, Number(this.master?.totalQuantity || 0) - Number(route.dispatchedQuantity || 0)); },
segmentTransportType(route = {}) { return route.transportTypeName || route.transportType || ''; }, segmentTransportType(route = {}) { return route.transportTypeName || route.transportType || ''; },
isRoadTransport(route = {}) {
const type = String(this.segmentTransportType(route)).trim().toLowerCase();
return type === 'road' || type.includes('公路');
},
formatRoadLocation(value) {
const text = String(value || '').replace(/\s+/g, '');
if (!text) return '';
const withoutProvince = text.replace(/^.*?(?:省|自治区|特别行政区)/, '');
const cityMatch = withoutProvince.match(/.+?(?:市|州(?!市)|盟(?!市))/);
if (!cityMatch) return text;
const city = cityMatch[0];
const districtSource = withoutProvince.slice(city.length);
const districtMatch = districtSource.match(/^[^市州盟]*?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/);
if (!districtMatch) return city;
const district = `${districtMatch[0]}${districtSource.slice(districtMatch[0].length).startsWith('区') ? '区' : ''}`;
return `${city} ${district}`;
},
segmentLocationName(route = {}, field) {
const name = String(route[`${field}Name`] || '').trim();
const address = String(route[`${field}Address`] || '').trim();
const formattedName = this.formatRoadLocation(name);
if (/(?:市|州|盟)/.test(formattedName)) return formattedName;
const regionalName = /(?:省|自治区|特别行政区|市|州|盟|区|县|旗)/.test(name);
if (name && !regionalName) return name;
const formattedAddress = this.formatRoadLocation(address);
if (/(?:市|州|盟)/.test(formattedAddress)) return formattedAddress;
const city = String(route[`${field}CityName`] || '').trim();
const district = String(route[`${field}DistrictName`] || '').trim();
return [city, district].filter(Boolean).join(' ') || formattedName || formattedAddress || '-';
},
plannedQuantity(route = {}) { return route.planQuantity || route.totalQuantity || this.master?.totalQuantity || 0; }, plannedQuantity(route = {}) { return route.planQuantity || route.totalQuantity || this.master?.totalQuantity || 0; },
plannedUnit(route = {}) { return route.quantityUnit || this.master?.goods?.[0]?.quantityUnit || '吨'; }, plannedUnit(route = {}) { return route.quantityUnit || this.master?.goods?.[0]?.quantityUnit || '吨'; },
segmentProgress(route = {}) { const planned = Number(this.plannedQuantity(route)); return planned > 0 ? Math.min(100, Math.max(0, (Number(route.dispatchedQuantity || 0) / planned) * 100)) : 0; }, segmentProgress(route = {}) { const planned = Number(this.plannedQuantity(route)); return planned > 0 ? Math.min(100, Math.max(0, (Number(route.dispatchedQuantity || 0) / planned) * 100)) : 0; },
@@ -113,11 +137,17 @@ export default {
this.previewFile = { name: this.attachmentName(file), url, mimeType: file.mimeType || file.contentType || '' }; this.previewFile = { name: this.attachmentName(file), url, mimeType: file.mimeType || file.contentType || '' };
this.documentPreviewVisible = true; this.documentPreviewVisible = true;
}, },
driverVehicle(row) { return [row.driverName, row.vehicleNo].filter(Boolean).join(' / ') || '-'; }, driverVehicle(row = {}, route = {}) {
const vehicle = row.vehicleNo || row.vehicleNumber || row.shipNo || row.flightNo || row.trainNo;
const values = this.isRoadTransport(route)
? [row.driverName, vehicle]
: [row.captainName, vehicle];
return values.filter(Boolean).join(' / ') || '-';
},
openWaybill(row) { this.$router.push({ path: '/business/waybill-manage', query: { detailId: row.id } }); }, openWaybill(row) { this.$router.push({ path: '/business/waybill-manage', query: { detailId: row.id } }); },
openPlan(row) { this.$router.push({ path: '/business/transport-plan', query: { detailId: row.id } }); }, openPlan(row) { this.$router.push({ path: '/business/transport-plan', query: { detailId: row.id } }); },
waybillStatusName(value) { return ({ pending: '待执行', running: '进行中', completed: '已完成', cancelled: '已取消' })[value] || value || '-'; }, waybillStatusName(value) { return ({ pending: '待执行', waiting: '待执行', running: '进行中', processing: '进行中', completed: '已完成', cancelled: '已取消' })[value] || value || '-'; },
waybillStatusType(value) { return ({ pending: 'info', running: 'warning', completed: 'success', cancelled: 'danger' })[value] || 'info'; }, waybillStatusType(value) { return ({ pending: 'info', waiting: 'info', running: 'warning', processing: 'warning', completed: 'success', cancelled: 'danger' })[value] || 'info'; },
statusName(value) { return ({ waiting_dispatch: '待调度', dispatching: '调度中', completed: '调度完成', closed: '调度关闭' })[value] || value || '-'; }, statusName(value) { return ({ waiting_dispatch: '待调度', dispatching: '调度中', completed: '调度完成', closed: '调度关闭' })[value] || value || '-'; },
statusType(value) { return ({ waiting_dispatch: 'warning', dispatching: 'success', completed: 'primary', closed: 'danger' })[value] || 'info'; }, statusType(value) { return ({ waiting_dispatch: 'warning', dispatching: 'success', completed: 'primary', closed: 'danger' })[value] || 'info'; },
}, },
@@ -132,19 +162,20 @@ export default {
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #409eff; font-size: 14px; word-break: break-all; } } .detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #409eff; font-size: 14px; word-break: break-all; } }
.route-map { display: flex; align-items: flex-start; padding: 16px 24px 20px; border-top: 1px solid #eff1f7; overflow-x: auto; } .route-map { display: flex; align-items: flex-start; padding: 16px 24px 20px; border-top: 1px solid #eff1f7; overflow-x: auto; }
.route-map__node { display: grid; flex: 0 0 150px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } } .route-map__node { display: grid; flex: 0 0 150px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } }
.route-map__progress { color: #409eff !important; } .route-map__progress { color: #303133 !important; }
.route-badge { display: inline-flex; width: 32px; height: 32px; align-items: center; justify-content: center; border-radius: 4px; background: #67c23a; color: #fff; font-weight: 600; &.start { background: #409eff; } &.end { background: #e6a23c; } } .route-badge { display: inline-flex; width: 32px; height: 32px; align-items: center; justify-content: center; border-radius: 4px; background: #67c23a; color: #fff; font-weight: 600; &.start { background: #409eff; } &.end { background: #e6a23c; } }
.route-map__line { flex: 1 0 64px; min-width: 64px; height: 32px; border-bottom: 6px solid #e4e7ed; } .route-map__line { flex: 1 0 64px; min-width: 64px; height: 32px; border-bottom: 6px solid #e4e7ed; }
.execution-detail-heading { padding: 20px 24px 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } } .execution-detail-heading { padding: 20px 24px 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
.segment-detail { padding: 16px 24px; border-top: 1px solid #eff1f7; header { display: flex; align-items: center; justify-content: space-between; margin: -16px -24px 8px; padding: 12px 24px; cursor: pointer; background: #fafbfc; h3 { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: 12px; margin: 0; font-size: 16px; } em { color: #409eff; font-style: normal; } } } .segment-detail { padding: 16px 24px; border-top: 1px solid #eff1f7; header { display: flex; align-items: center; justify-content: space-between; margin: -16px -24px 8px; padding: 12px 24px; cursor: pointer; background: #fafbfc; h3 { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: 12px; margin: 0; font-size: 16px; } em { color: #67c23a; font-style: normal; } } }
.segment-detail__progress { color: #606266; font-size: 14px; font-weight: 400; } .segment-detail__progress { color: #606266; font-size: 14px; font-weight: 400; }
.segment-detail__transport-type { color: #409eff; font-size: 14px; font-weight: 500; } .segment-detail__transport-type { color: #409eff; font-size: 14px; font-weight: 500; }
.segment-detail__progress-bar { display: inline-flex; width: 144px; height: 6px; overflow: hidden; border-radius: 3px; background: #e4e7ed; } .segment-detail__progress-bar { display: inline-flex; width: 144px; height: 6px; overflow: hidden; border-radius: 3px; background: #e4e7ed; }
.segment-detail__progress-bar span { display: block; height: 100%; background: #409eff; transition: width 0.2s ease; } .segment-detail__progress-bar span { display: block; height: 100%; background: #409eff; transition: width 0.2s ease; }
.segment-detail__header-right { display: flex; align-items: center; gap: 12px; } .segment-toggle { gap: 4px; padding: 4px 0; }
.segment-toggle__icon { font-size: 12px; }
.segment-index { display: inline-flex; width: 36px; height: 36px; align-items: center; justify-content: center; border-radius: 50%; background: #2088ee; color: #fff; font-size: 20px; font-weight: 500; } .segment-index { display: inline-flex; width: 36px; height: 36px; align-items: center; justify-content: center; border-radius: 50%; background: #2088ee; color: #fff; font-size: 20px; font-weight: 500; }
.segment-execution { padding-top: 12px; } .segment-execution { padding-top: 12px; }
.segment-stats { display: flex; gap: 72px; padding: 4px 0 24px; div { display: flex; flex-direction: column; gap: 8px; } span { color: #8a9bb8; font-size: 14px; } strong { color: #1f2d3d; font-size: 32px; line-height: 1; } small { margin-left: 6px; color: #8a9bb8; font-size: 16px; font-weight: 400; } .dispatched { color: #409eff; } .remaining { color: #f56c6c; } } .segment-stats { display: flex; gap: 72px; padding: 4px 0 24px; div { display: flex; flex-direction: column; gap: 8px; } span { color: #8a9bb8; font-size: 14px; } strong { color: #1f2d3d; font-size: 32px; line-height: 1; } small { margin-left: 6px; color: #8a9bb8; font-size: 16px; font-weight: 400; } .dispatched { color: #67c23a; } .remaining { color: #e6a23c; } }
.waybill-table { :deep(th.el-table__cell) { background: #f5f7fa; color: #60738f; } } .waybill-table { :deep(th.el-table__cell) { background: #f5f7fa; color: #60738f; } }
.transport-plan-detail { margin-top: 20px; h4 { margin: 0 0 12px; padding-left: 12px; border-left: 4px solid #409eff; font-size: 15px; } } .transport-plan-detail { margin-top: 20px; h4 { margin: 0 0 12px; padding-left: 12px; border-left: 4px solid #409eff; font-size: 15px; } }
.master-goods-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; } .master-goods-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
@@ -55,17 +55,17 @@
<span class="segment-checkbox-label"> <span class="segment-checkbox-label">
<span>{{ route.segmentNo }}</span> <span>{{ route.segmentNo }}</span>
<el-tooltip <el-tooltip
:content="`${route.departureName || '-'} → ${route.arrivalName || '-'}`" :content="`${dispatchLocationName(route, 'departure')} → ${dispatchLocationName(route, 'arrival')}`"
placement="top" placement="top"
> >
<span class="segment-checkbox-path" <span class="segment-checkbox-path"
>{{ route.departureName || '-' }} {{ route.arrivalName || '-' }}</span >{{ dispatchLocationName(route, 'departure') }} {{ dispatchLocationName(route, 'arrival') }}</span
> >
</el-tooltip> </el-tooltip>
</span> </span>
</el-checkbox> </el-checkbox>
</div> </div>
<div>总量 {{ formatQuantity(totalQuantity) }}已调度 <em>{{ formatQuantity(dispatchedQuantity(route)) }}</em>剩余 <em>{{ formatQuantity(remaining(route)) }}</em></div> <div>总量 {{ formatQuantity(totalQuantity) }} {{ quantityUnitLabel }}已调度 <em>{{ formatQuantity(dispatchedQuantity(route)) }} {{ quantityUnitLabel }}</em>剩余 <em>{{ formatQuantity(remaining(route)) }} {{ quantityUnitLabel }}</em></div>
</header> </header>
<div v-show="route.selected" class="segment-content"> <div v-show="route.selected" class="segment-content">
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form"> <el-form :model="route" label-position="right" label-width="auto" class="dispatch-form">
@@ -95,11 +95,11 @@
<div class="goods-heading"><h3>货物信息</h3><div><el-link type="primary" @click="addGoods(route)">新增货物</el-link></div></div> <div class="goods-heading"><h3>货物信息</h3><div><el-link type="primary" @click="addGoods(route)">新增货物</el-link></div></div>
<el-table :data="route.goods" border class="goods-table"> <el-table :data="route.goods" border class="goods-table">
<el-table-column type="index" label="序号" width="64" /> <el-table-column type="index" label="序号" width="64" />
<el-table-column label="货物类型" min-width="180"><template #default="{ row }"><el-cascader v-model="row.cargoTypePath" class="goods-table__cargo-type" :options="cargoTypeOptions" :props="cargoTypeCascaderProps" placeholder="请选择货物类型" clearable filterable @change="value => handleCargoTypeChange(route, row, value)" /></template></el-table-column> <el-table-column min-width="180"><template #header><span>货物类型<span class="goods-required-mark">*</span></span></template><template #default="{ row }"><el-cascader v-model="row.cargoTypePath" class="goods-table__cargo-type" :options="cargoTypeOptions" :props="cargoTypeCascaderProps" placeholder="请选择货物类型" clearable filterable @change="value => handleCargoTypeChange(route, row, value)" /></template></el-table-column>
<el-table-column label="货物名称" min-width="180"><template #default="{ row }"><el-select v-model="row.sourceIndex" class="goods-table__cargo-name" placeholder="请选择总单货物" @change="selectGoods(route, row)"><el-option v-for="item in masterGoodsByCargoType(row)" :key="item.sourceIndex" :label="item.cargoName" :value="item.sourceIndex" /></el-select></template></el-table-column> <el-table-column min-width="180"><template #header><span>货物名称<span class="goods-required-mark">*</span></span></template><template #default="{ row }"><el-select v-model="row.sourceIndex" class="goods-table__cargo-name" placeholder="请选择总单货物" @change="selectGoods(route, row)"><el-option v-for="item in masterGoodsByCargoType(row)" :key="item.sourceIndex" :label="item.cargoName" :value="item.sourceIndex" /></el-select></template></el-table-column>
<el-table-column prop="remainingQuantity" column-key="remainingQuantity" label="剩余数量" width="150" class-name="goods-table__remaining"><template #default="{ row }">{{ formatQuantity(goodsRemainingQuantity(route, row)) }}</template></el-table-column> <el-table-column prop="remainingQuantity" column-key="remainingQuantity" label="剩余数量" width="150" class-name="goods-table__remaining"><template #default="{ row }">{{ formatQuantity(goodsRemainingQuantity(route, row)) }}</template></el-table-column>
<el-table-column prop="dispatchQuantity" column-key="dispatchQuantity" label="本次数量" width="200" class-name="goods-table__dispatch"><template #default="{ row }"><el-input :model-value="row.dispatchQuantity" inputmode="decimal" placeholder="请输入" @input="value => handleDispatchQuantityInput(route, row, value)" /></template></el-table-column> <el-table-column prop="dispatchQuantity" column-key="dispatchQuantity" width="200" class-name="goods-table__dispatch"><template #header><span>本次数量<span class="goods-required-mark">*</span></span></template><template #default="{ row }"><el-input :model-value="row.dispatchQuantity" inputmode="decimal" placeholder="请输入" @input="value => handleDispatchQuantityInput(route, row, value)" /></template></el-table-column>
<el-table-column prop="quantityUnit" label="数量单位" width="110" /> <el-table-column prop="quantityUnit" width="110"><template #header><span>数量单位<span class="goods-required-mark">*</span></span></template></el-table-column>
<el-table-column prop="packageType" label="包装" min-width="110" /> <el-table-column prop="packageType" label="包装" min-width="110" />
<el-table-column prop="brand" label="品牌" min-width="110" /> <el-table-column prop="brand" label="品牌" min-width="110" />
<el-table-column prop="specification" label="规格" min-width="110" /> <el-table-column prop="specification" label="规格" min-width="110" />
@@ -108,6 +108,33 @@
<el-table-column label="操作" width="90" fixed="right"><template #default="{ $index }"><el-link type="danger" @click="removeGoods(route, $index)">删除</el-link></template></el-table-column> <el-table-column label="操作" width="90" fixed="right"><template #default="{ $index }"><el-link type="danger" @click="removeGoods(route, $index)">删除</el-link></template></el-table-column>
</el-table> </el-table>
<template v-if="route.documentType === '运单'">
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form carrier-type-form"><el-form-item label="承运类型" required><el-radio-group v-model="route.carrierType" @change="value => handleCarrierTypeChange(route, value)"><el-radio-button label="承运商" /><el-radio-button label="自运" /><el-radio-button label="网货平台" /></el-radio-group></el-form-item></el-form>
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form carrier-form">
<el-row :gutter="16">
<template v-if="isRoad(route)">
<el-col :span="6"><el-form-item label="承运商" :required="route.carrierType === '承运商'"><el-select :model-value="route.carrierType === '承运商' ? route.carrierContractId : route.carrierName" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="carrierOptionValue(item)" :label="carrierOptionLabel(item)" :value="route.carrierType === '承运商' ? carrierOptionValue(item) : item.carrierName" /></el-select></el-form-item></el-col>
<el-col :span="6"><el-form-item label="司机" :required="route.carrierType !== '承运商'"><el-autocomplete :ref="element => setDriverInput(route.segmentNo, element)" v-model="route.driverName" :debounce="300" :fetch-suggestions="fetchDriverSuggestions" clearable placeholder="请输入司机" :loading="driverLoading" @select="item => handleDriverSuggestionSelect(route, item)" /></el-form-item></el-col>
<el-col :span="6"><el-form-item :label="route.carrierType === '承运商' ? '手机号' : '司机手机号'" :required="route.carrierType !== '承运商'"><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="车牌号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="挂车车牌号"><el-input v-model="route.trailerVehicleNo" placeholder="请输入" /></el-form-item></el-col>
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人"><el-autocomplete v-model="route.escortName" :debounce="300" :fetch-suggestions="fetchEscortSuggestions" clearable placeholder="请输入" :loading="escortLoading" @select="item => handleEscortSuggestionSelect(route, item)" /></el-form-item></el-col>
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人手机号"><el-input v-model="route.escortPhone" placeholder="请输入" /></el-form-item></el-col>
</template>
<template v-else>
<el-col :span="6"><el-form-item label="承运商" :required="route.carrierType === '承运商'"><el-select :model-value="route.carrierType === '承运商' ? route.carrierContractId : route.carrierName" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="carrierOptionValue(item)" :label="carrierOptionLabel(item)" :value="route.carrierType === '承运商' ? carrierOptionValue(item) : item.carrierName" /></el-select></el-form-item></el-col>
<el-col :span="6"><el-form-item label="船/航/班列号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="船长"><el-input v-model="route.captainName" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="联系电话" required><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="箱号"><el-input v-model="route.containerNo" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="舱位"><el-input v-model="route.cabinNo" placeholder="请输入" /></el-form-item></el-col>
</template>
<el-col :span="6"><el-form-item label="里程(km"><el-input :model-value="route.mileage" inputmode="numeric" maxlength="10" placeholder="请输入" @input="value => handleMileageInput(route, value)" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="备注"><el-input v-model="route.remark" maxlength="200" show-word-limit placeholder="请输入" /></el-form-item></el-col>
</el-row>
</el-form>
</template>
<div class="freight-heading"><h3>运费信息</h3></div> <div class="freight-heading"><h3>运费信息</h3></div>
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form freight-form"> <el-form :model="route" label-position="right" label-width="auto" class="dispatch-form freight-form">
<el-row v-for="(group, index) in freightGroups(route)" :key="group.key" :gutter="16"> <el-row v-for="(group, index) in freightGroups(route)" :key="group.key" :gutter="16">
@@ -136,33 +163,6 @@
<el-col :span="6"><el-form-item label="其他费用合计"><el-input :model-value="route.otherFeeTotal" inputmode="decimal" placeholder="请输入" @input="value => handleOtherFeeTotalInput(route, value)" /></el-form-item></el-col> <el-col :span="6"><el-form-item label="其他费用合计"><el-input :model-value="route.otherFeeTotal" inputmode="decimal" placeholder="请输入" @input="value => handleOtherFeeTotalInput(route, value)" /></el-form-item></el-col>
</el-row> </el-row>
</el-form> </el-form>
<template v-if="route.documentType === '运单'">
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form carrier-type-form"><el-form-item label="承运类型" required><el-radio-group v-model="route.carrierType" @change="value => handleCarrierTypeChange(route, value)"><el-radio-button label="承运商" /><el-radio-button label="自运" /><el-radio-button label="网货平台" /></el-radio-group></el-form-item></el-form>
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form carrier-form">
<el-row :gutter="16">
<template v-if="isRoad(route)">
<el-col :span="6"><el-form-item label="承运商" :required="route.carrierType === '承运商'"><el-select :model-value="route.carrierType === '承运商' ? route.carrierContractId : route.carrierName" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="carrierOptionValue(item)" :label="carrierOptionLabel(item)" :value="route.carrierType === '承运商' ? carrierOptionValue(item) : item.carrierName" /></el-select></el-form-item></el-col>
<el-col :span="6"><el-form-item label="司机" :required="route.carrierType !== '承运商'"><el-autocomplete :ref="element => setDriverInput(route.segmentNo, element)" v-model="route.driverName" :debounce="300" :fetch-suggestions="fetchDriverSuggestions" clearable placeholder="请输入司机" :loading="driverLoading" @select="item => handleDriverSuggestionSelect(route, item)" /></el-form-item></el-col>
<el-col :span="6"><el-form-item :label="route.carrierType === '承运商' ? '手机号' : '司机手机号'" :required="route.carrierType !== '承运商'"><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="车牌号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="挂车车牌号"><el-input v-model="route.trailerVehicleNo" placeholder="请输入" /></el-form-item></el-col>
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人"><el-autocomplete v-model="route.escortName" :debounce="300" :fetch-suggestions="fetchEscortSuggestions" clearable placeholder="请输入" :loading="escortLoading" @select="item => handleEscortSuggestionSelect(route, item)" /></el-form-item></el-col>
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人手机号"><el-input v-model="route.escortPhone" placeholder="请输入" /></el-form-item></el-col>
</template>
<template v-else>
<el-col :span="6"><el-form-item label="承运商" :required="route.carrierType === '承运商'"><el-select :model-value="route.carrierType === '承运商' ? route.carrierContractId : route.carrierName" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="carrierOptionValue(item)" :label="carrierOptionLabel(item)" :value="route.carrierType === '承运商' ? carrierOptionValue(item) : item.carrierName" /></el-select></el-form-item></el-col>
<el-col :span="6"><el-form-item label="船/航/班列号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="船长"><el-input v-model="route.captainName" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="联系电话" required><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="箱号"><el-input v-model="route.containerNo" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="舱位"><el-input v-model="route.cabinNo" placeholder="请输入" /></el-form-item></el-col>
</template>
<el-col :span="6"><el-form-item label="里程(km"><el-input :model-value="route.mileage" inputmode="numeric" maxlength="10" placeholder="请输入" @input="value => handleMileageInput(route, value)" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="备注"><el-input v-model="route.remark" maxlength="200" show-word-limit placeholder="请输入" /></el-form-item></el-col>
</el-row>
</el-form>
</template>
<div class="segment-actions"><el-button plain @click="addToPending(route, true)">加入并继续调度</el-button><el-button plain @click="addToPending(route, false)">加入调度清单</el-button></div> <div class="segment-actions"><el-button plain @click="addToPending(route, true)">加入并继续调度</el-button><el-button plain @click="addToPending(route, false)">加入调度清单</el-button></div>
</div> </div>
</section> </section>
@@ -178,7 +178,7 @@
</div> </div>
<div v-show="pendingExpanded" class="pending-list"> <div v-show="pendingExpanded" class="pending-list">
<template v-for="(items, segmentNo) in pendingGroups" :key="segmentNo"> <template v-for="(items, segmentNo) in pendingGroups" :key="segmentNo">
<h3>{{ segmentNo }}{{ items[0].departureName }} {{ items[0].arrivalName }}</h3> <h3>{{ segmentNo }}{{ dispatchLocationName(items[0], 'departure') }} {{ dispatchLocationName(items[0], 'arrival') }}</h3>
<el-table :data="items" border><el-table-column type="index" label="序号" width="70" /><el-table-column label="单据类型" prop="documentType" width="100" /><el-table-column label="货物" min-width="220"><template #default="{ row }">{{ row.cargoName }}{{ row.cargoType ? `${row.cargoType}` : '' }}</template></el-table-column><el-table-column label="本次数量" width="150"><template #default="{ row }">{{ formatQuantity(row.quantity) }} {{ row.quantityUnit }}</template></el-table-column><el-table-column prop="carrierName" label="承运商" min-width="180" /><el-table-column prop="driverName" label="司机" width="130" /><el-table-column prop="vehicleNo" label="车牌" width="130" /><el-table-column label="操作" width="120" fixed="right"><template #default="{ row }"><div class="table-actions"><el-link type="primary" @click="editPending(row)">编辑</el-link><el-link type="danger" @click="removePending(row.id)">删除</el-link></div></template></el-table-column></el-table> <el-table :data="items" border><el-table-column type="index" label="序号" width="70" /><el-table-column label="单据类型" prop="documentType" width="100" /><el-table-column label="货物" min-width="220"><template #default="{ row }">{{ row.cargoName }}{{ row.cargoType ? `${row.cargoType}` : '' }}</template></el-table-column><el-table-column label="本次数量" width="150"><template #default="{ row }">{{ formatQuantity(row.quantity) }} {{ row.quantityUnit }}</template></el-table-column><el-table-column prop="carrierName" label="承运商" min-width="180" /><el-table-column prop="driverName" label="司机" width="130" /><el-table-column prop="vehicleNo" label="车牌" width="130" /><el-table-column label="操作" width="120" fixed="right"><template #default="{ row }"><div class="table-actions"><el-link type="primary" @click="editPending(row)">编辑</el-link><el-link type="danger" @click="removePending(row.id)">删除</el-link></div></template></el-table-column></el-table>
</template> </template>
<el-empty v-if="!pending.length" description="暂无待提交调度条目" :image-size="60" /> <el-empty v-if="!pending.length" description="暂无待提交调度条目" :image-size="60" />
@@ -211,6 +211,7 @@ export default {
}, },
computed: { computed: {
totalQuantity() { return (this.master?.goods || []).reduce((sum, item) => sum + Number(item.quantity || 0), 0); }, totalQuantity() { return (this.master?.goods || []).reduce((sum, item) => sum + Number(item.quantity || 0), 0); },
quantityUnitLabel() { return this.master?.goods?.[0]?.quantityUnit || '吨'; },
goodsNames() { return (this.master?.goods || []).map(item => item.cargoName).filter(Boolean).join('、') || '-'; }, goodsNames() { return (this.master?.goods || []).map(item => item.cargoName).filter(Boolean).join('、') || '-'; },
goodsTypes() { return [...new Set((this.master?.goods || []).map(item => item.cargoType).filter(Boolean))].join('、') || '-'; }, goodsTypes() { return [...new Set((this.master?.goods || []).map(item => item.cargoType).filter(Boolean))].join('、') || '-'; },
masterGoods() { return (this.master?.goods || []).map((item, sourceIndex) => ({ ...item, sourceIndex, label: [item.cargoName, item.cargoType].filter(Boolean).join(' / ') })); }, masterGoods() { return (this.master?.goods || []).map((item, sourceIndex) => ({ ...item, sourceIndex, label: [item.cargoName, item.cargoType].filter(Boolean).join(' / ') })); },
@@ -293,7 +294,9 @@ export default {
otherFeeTotal: '', otherFeeTotal: '',
freightItems: [], freightItems: [],
departureName: previous.departureName || '', departureAddress: previous.departureAddress || '', departureContact: previous.departureContact || '', departurePhone: previous.departurePhone || '', departureName: previous.departureName || '', departureAddress: previous.departureAddress || '', departureContact: previous.departureContact || '', departurePhone: previous.departurePhone || '',
departureCityName: previous.departureCityName || previous.arrivalCityName || '', departureDistrictName: previous.departureDistrictName || previous.arrivalDistrictName || '', departureSiteCode: previous.departureSiteCode || previous.arrivalSiteCode || '',
arrivalName: node.departureName || this.master.arrivalName || '', arrivalAddress: node.departureAddress || this.master.arrivalAddress || '', arrivalContact: node.departureContact || '', arrivalPhone: node.departurePhone || '', arrivalName: node.departureName || this.master.arrivalName || '', arrivalAddress: node.departureAddress || this.master.arrivalAddress || '', arrivalContact: node.departureContact || '', arrivalPhone: node.departurePhone || '',
arrivalCityName: node.departureCityName || node.arrivalCityName || this.master.arrivalCityName || '', arrivalDistrictName: node.departureDistrictName || node.arrivalDistrictName || this.master.arrivalDistrictName || '', arrivalSiteCode: node.departureSiteCode || node.arrivalSiteCode || this.master.arrivalSiteCode || '',
goods: (this.master.goods || []).map((goods, sourceIndex) => this.createGoodsRow(goods, sourceIndex)), goods: (this.master.goods || []).map((goods, sourceIndex) => this.createGoodsRow(goods, sourceIndex)),
}; };
this.syncFreightItems(route); this.syncFreightItems(route);
@@ -341,7 +344,52 @@ export default {
else Object.assign(row, { sourceIndex: undefined, cargoName: '', quantity: '', quantityUnit: '' }); else Object.assign(row, { sourceIndex: undefined, cargoName: '', quantity: '', quantityUnit: '' });
this.syncFreightItems(route); this.syncFreightItems(route);
}, },
isRoad(route) { const type = String(route.transportType || '').toLowerCase(); return type.includes('公路') || type === 'road'; }, isRoad(route) {
const values = [
route.transportTypeName,
route.transportTypeLabel,
route.transportModeName,
route.transportMode,
route.transportType,
];
const type = values
.map(value => {
if (value && typeof value === 'object') {
return value.label || value.name || value.dictValue || value.value || value.code || '';
}
return value || '';
})
.join(' ')
.trim()
.toLowerCase();
return type.includes('公路') || type.includes('道路') || type.includes('陆运') || type.includes('汽车') || type === 'road';
},
formatRoadLocation(value) {
const text = String(value || '').replace(/\s+/g, '');
if (!text) return '';
const withoutProvince = text.replace(/^.*?(?:省|自治区|特别行政区)/, '');
const cityMatch = withoutProvince.match(/.+?(?:市|州(?!市)|盟(?!市))/);
if (!cityMatch) return text;
const city = cityMatch[0];
const districtSource = withoutProvince.slice(city.length);
const districtMatch = districtSource.match(/^[^市州盟]*?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/);
if (!districtMatch) return city;
const district = `${districtMatch[0]}${districtSource.slice(districtMatch[0].length).startsWith('区') ? '区' : ''}`;
return `${city} ${district}`;
},
dispatchLocationName(route = {}, field) {
const name = String(route[`${field}Name`] || '').trim();
const address = String(route[`${field}Address`] || '').trim();
const formattedName = this.formatRoadLocation(name);
if (/(?:市|州|盟)/.test(formattedName)) return formattedName;
const regionalName = /(?:省|自治区|特别行政区|市|州|盟|区|县|旗)/.test(name);
if (name && !regionalName) return name;
const city = String(route[`${field}CityName`] || '').trim();
const district = String(route[`${field}DistrictName`] || '').trim();
const formattedAddress = this.formatRoadLocation(address);
if (/(?:市|州|盟)/.test(formattedAddress)) return formattedAddress;
return [city, district].filter(Boolean).join(' ') || formattedName || formattedAddress || '-';
},
isWater(route) { isWater(route) {
const type = String(route.transportType || '').trim().toLowerCase(); const type = String(route.transportType || '').trim().toLowerCase();
return type === 'river' || type === 'water' || type === 'sl' || type.includes('水路') || type.includes('水运'); return type === 'river' || type === 'water' || type === 'sl' || type.includes('水路') || type.includes('水运');
@@ -529,6 +577,23 @@ export default {
} }
return true; return true;
}, },
validateDispatchGoods(route, goods) {
for (const item of goods) {
const index = (route.goods || []).indexOf(item) + 1;
const fields = [
[!String(item.cargoName || '').trim() || item.sourceIndex === undefined || item.sourceIndex === null || item.sourceIndex === '', '货物名称'],
[!String(item.cargoType || '').trim() && !(item.cargoTypePath || []).length, '货物类型'],
[item.dispatchQuantity === undefined || item.dispatchQuantity === null || item.dispatchQuantity === '' || Number(item.dispatchQuantity) <= 0, '本次数量'],
[!String(item.quantityUnit || '').trim(), '数量单位'],
];
const emptyField = fields.find(([isEmpty]) => isEmpty);
if (emptyField) {
this.$message.warning(`${index}条货物的${emptyField[1]}不能为空`);
return false;
}
}
return true;
},
handleFreightQuantityUnitChange(route, freightItem, value) { handleFreightQuantityUnitChange(route, freightItem, value) {
const goods = (route.goods || []).find(item => String(item.sourceIndex) === String(freightItem.sourceIndex)); const goods = (route.goods || []).find(item => String(item.sourceIndex) === String(freightItem.sourceIndex));
if (goods) goods.quantityUnit = value; if (goods) goods.quantityUnit = value;
@@ -731,6 +796,7 @@ export default {
addToPending(route, continueDispatch) { addToPending(route, continueDispatch) {
const goods = route.goods.filter(item => Number(item.dispatchQuantity) > 0); const goods = route.goods.filter(item => Number(item.dispatchQuantity) > 0);
if (!goods.length) return this.$message.warning('请填写本次数量'); if (!goods.length) return this.$message.warning('请填写本次数量');
if (!this.validateDispatchGoods(route, goods)) return;
if (goods.some(item => Number(item.dispatchQuantity) > this.availableDispatchQuantity(route, item))) return this.$message.warning('本次数量不能超过剩余数量'); if (goods.some(item => Number(item.dispatchQuantity) > this.availableDispatchQuantity(route, item))) return this.$message.warning('本次数量不能超过剩余数量');
this.syncFreightItems(route); this.syncFreightItems(route);
if (!this.validateFreightItems(route)) return; if (!this.validateFreightItems(route)) return;
@@ -748,11 +814,11 @@ export default {
id: `${route.segmentNo}-${item.cargoName}-${Date.now()}-${Math.random()}`, id: `${route.segmentNo}-${item.cargoName}-${Date.now()}-${Math.random()}`,
batchNo, batchNo,
segmentNo: route.segmentNo, relationNo: route.segmentNo, documentType: route.documentType, transportType: route.transportType, carrierType: route.carrierType, segmentNo: route.segmentNo, relationNo: route.segmentNo, documentType: route.documentType, transportType: route.transportType, carrierType: route.carrierType,
carrierContractId: route.carrierContractId, carrierName: route.carrierName, driverName: route.driverName, driverPhone: route.driverPhone, vehicleNo: route.vehicleNo, captainName: route.captainName, cabinNo: route.cabinNo, containerNo: route.containerNo, trailerVehicleNo: route.trailerVehicleNo, escortName: route.escortName, escortPhone: route.escortPhone, mileage: this.normalizeMileage(route.mileage), unitPrice: this.freightItemsForGoods(route, item).unitPrice || '', priceUnit: this.freightItemsForGoods(route, item).priceUnit || '', currency: route.currency || 'RMB', freightAmount: this.freightAmount(this.freightItemsForGoods(route, item)), freightTotal: this.freightTotal(route), otherFeeTotal: route.otherFeeTotal || '', freightJson: this.buildFreightJson(route), remark: route.remark, carrierContractId: route.carrierContractId, carrierName: route.carrierName, driverName: route.driverName, driverPhone: route.driverPhone, vehicleNo: route.vehicleNo, captainName: route.captainName, cabinNo: route.cabinNo, containerNo: route.containerNo, trailerVehicleNo: route.trailerVehicleNo, escortName: route.escortName, escortPhone: route.escortPhone, mileage: this.normalizeMileage(route.mileage), unitPrice: this.freightItemsForGoods(route, item).unitPrice || '', priceUnit: this.freightItemsForGoods(route, item).priceUnit || '', currency: route.currency || 'RMB', freightAmount: this.freightAmount(this.freightItemsForGoods(route, item)), freightTotal: this.freightTotal(route), otherFeeTotal: route.otherFeeTotal || '', freightJson: this.buildFreightJson(route), routeRemark: route.remark,
departureName: route.departureName, departureAddress: route.departureAddress, departureContact: route.departureContact, departurePhone: route.departurePhone, departureName: route.departureName, departureAddress: route.departureAddress, departureContact: route.departureContact, departurePhone: route.departurePhone,
arrivalName: route.arrivalName, arrivalAddress: route.arrivalAddress, arrivalContact: route.arrivalContact, arrivalPhone: route.arrivalPhone, arrivalName: route.arrivalName, arrivalAddress: route.arrivalAddress, arrivalContact: route.arrivalContact, arrivalPhone: route.arrivalPhone,
estimatedStartTime: route.estimatedStartTime, estimatedEndTime: route.estimatedEndTime, estimatedStartTime: route.estimatedStartTime, estimatedEndTime: route.estimatedEndTime,
sourceIndex: this.masterGoodsIndex(item), cargoName: item.cargoName, cargoType: item.cargoType, quantity: item.dispatchQuantity, quantityUnit: item.quantityUnit, packageType: item.packageType, brand: item.brand, specification: item.specification, model: item.model, materialCode: item.materialCode, sourceIndex: this.masterGoodsIndex(item), cargoName: item.cargoName, cargoType: item.cargoType, quantity: item.dispatchQuantity, quantityUnit: item.quantityUnit, packageType: item.packageType, brand: item.brand, specification: item.specification, model: item.model, materialCode: item.materialCode, deviceCode: item.deviceCode, remark: item.remark,
})); }));
this.editingId = null; this.editingId = null;
this.pendingExpanded = true; this.pendingExpanded = true;
@@ -838,6 +904,7 @@ export default {
.goods-heading { display: flex; align-items: center; justify-content: space-between; margin: 0 -24px 12px; padding: 14px 24px; border-top: 1px solid #eff1f7; border-bottom: 1px solid #eff1f7; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } span { color: #909399; font-size: 13px; } } .goods-heading { display: flex; align-items: center; justify-content: space-between; margin: 0 -24px 12px; padding: 14px 24px; border-top: 1px solid #eff1f7; border-bottom: 1px solid #eff1f7; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } span { color: #909399; font-size: 13px; } }
.freight-heading { margin: 16px 0 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } } .freight-heading { margin: 16px 0 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
.goods-table { :deep(th.el-table__cell), :deep(td.el-table__cell) { border-color: #eff1f7; } :deep(.el-table__row--striped td.el-table__cell) { background: #fafafa; } :deep(.goods-table__remaining .cell) { white-space: nowrap; } :deep(.goods-table__dispatch .el-input) { width: 100%; min-width: 0; } :deep(.goods-table__cargo-type .el-input__inner), :deep(.goods-table__cargo-name .el-select__selected-item) { color: #303133; } } .goods-table { :deep(th.el-table__cell), :deep(td.el-table__cell) { border-color: #eff1f7; } :deep(.el-table__row--striped td.el-table__cell) { background: #fafafa; } :deep(.goods-table__remaining .cell) { white-space: nowrap; } :deep(.goods-table__dispatch .el-input) { width: 100%; min-width: 0; } :deep(.goods-table__cargo-type .el-input__inner), :deep(.goods-table__cargo-name .el-select__selected-item) { color: #303133; } }
.goods-required-mark { margin-left: 2px; color: #f56c6c; }
.freight-form { :deep(.freight-unit-select) { width: 92px; flex: 0 0 92px; min-width: 0; } } .freight-form { :deep(.freight-unit-select) { width: 92px; flex: 0 0 92px; min-width: 0; } }
.carrier-type-form { margin-top: 16px; } .carrier-type-form { margin-top: 16px; }
.carrier-form { padding-top: 8px; } .carrier-form { padding-top: 8px; }
@@ -34,7 +34,7 @@
:model-value="form.masterNo || '系统自动生成'" :model-value="form.masterNo || '系统自动生成'"
readonly /></el-form-item></el-col readonly /></el-form-item></el-col
><el-col :span="6" ><el-col :span="6"
><el-form-item label="运输组织类型" required ><el-form-item label="运输类型" required
><el-select v-model="form.transportOrganizationType" disabled ><el-select v-model="form.transportOrganizationType" disabled
><el-option label="多式联运" value="多式联运" /></el-select></el-form-item></el-col ><el-option label="多式联运" value="多式联运" /></el-select></el-form-item></el-col
><el-col :span="24" ><el-col :span="24"
@@ -54,75 +54,77 @@
<el-link type="primary" @click="openRouteDialog">选择线路</el-link> <el-link type="primary" @click="openRouteDialog">选择线路</el-link>
</div> </div>
</div> </div>
<el-form :model="form" label-position="right" label-width="auto" <el-form class="route-info-form" :model="form" label-position="right" label-width="96px"
><div class="route-steps"> ><div class="route-flow">
<el-steps direction="vertical" :active="form.routes.length + 1"> <div class="route-flow-row">
<el-step> <span class="route-flow-marker route-flow-marker--start"></span>
<template #icon><span class="route-step-icon route-step-icon--start"></span></template> <div class="route-flow-body">
<template #description>
<div class="route-node-form"> <div class="route-node-form">
<el-form-item label="发货地" required class="route-address-form-item" <el-form-item label="发货地" required class="route-address-form-item"
><div class="route-address-inputs" ><div class="route-address-inputs"
><el-cascader v-model="addressRegionPaths.departure" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('departure', value)" /><el-input v-model="form.departureAddress" class="route-address-detail" readonly placeholder="请输入详细地址" @click="openAddressMap('departure')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" @click="openCommonAddress('departure')" /></el-tooltip></div ><el-input v-if="isNonRoadAddress('departure')" v-model="form.departureName" class="route-address-name" readonly placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths.departure" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('departure', value)" /><el-input v-model="form.departureAddress" class="route-address-detail" readonly placeholder="请输入详细地址" @click="handleAddressDetailClick('departure')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" @click="openCommonAddress('departure')" /></el-tooltip></div
></el-form-item> ></el-form-item>
<el-form-item label="联系人" <el-form-item label="联系人" class="route-contact-form-item"
><el-input v-model="form.departureContact" placeholder="请输入" ><el-input v-model="form.departureContact" placeholder="请输入"
/></el-form-item> /></el-form-item>
<el-form-item label="联系方式" <el-form-item label="联系方式" class="route-phone-form-item"
><el-input v-model="form.departurePhone" placeholder="请输入" ><el-input v-model="form.departurePhone" placeholder="请输入"
/></el-form-item> /></el-form-item>
<el-form-item label="计划开始日期" <el-form-item label="计划开始" class="route-date-form-item"
><el-date-picker ><el-date-picker
v-model="form.planStartTime" v-model="form.planStartTime"
type="date" type="date"
format="YYYY-MM-DD" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="请选择" placeholder="请选择"
style="width: 200px"
/></el-form-item> /></el-form-item>
</div> </div>
</template> </div>
</el-step> </div>
<el-step v-for="(route, index) in form.routes" :key="index"> <template v-for="(route, index) in form.routes" :key="index">
<template #icon><span class="route-step-icon route-step-icon--middle"></span></template> <div class="route-flow-segment">
<template #title><div class="route-step-title"><span class="route-segment-label">{{ index + 1 }}</span><el-tooltip :content="getRouteTitle(index).replace(`段${index + 1}`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getRouteTitle(index), index + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式</span><el-select v-model="route.transportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template> <div class="route-step-title"><span class="route-segment-label">{{ circledSegmentNumber(index + 1) }} {{ index + 1 }}</span><el-tooltip :content="getRouteTitle(index).replace(`段${index + 1}`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getRouteTitle(index), index + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式<span class="required-asterisk">*</span></span><el-select v-model="route.transportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div>
<template #description> </div>
<div class="route-flow-row">
<span class="route-flow-marker route-flow-marker--middle"></span>
<div class="route-flow-body">
<div class="route-node-form"> <div class="route-node-form">
<el-form-item label="途经地" required class="route-address-form-item" <el-form-item label="途经地" required class="route-address-form-item"
><div class="route-address-inputs" ><div class="route-address-inputs"
><el-cascader v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!route.transportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="!route.transportType || !isRoadTransportType(route.transportType)" placeholder="请先选择运输方式" @click="openAddressMap(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="!route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div ><el-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!route.transportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="!route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
></el-form-item> ></el-form-item>
<el-form-item label="联系人"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item> <el-form-item label="联系人" class="route-contact-form-item"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item>
<el-form-item label="联系方式"><div class="route-contact-actions"><el-input v-model="route.departurePhone" placeholder="请输入" /><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></div></el-form-item> <el-form-item label="联系方式" class="route-phone-form-item"><div class="route-contact-actions"><el-input v-model="route.departurePhone" placeholder="请输入" /><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></div></el-form-item>
</div>
</div>
</div> </div>
</template> </template>
</el-step> <div class="route-flow-segment">
<el-step> <div class="route-step-title"><span class="route-segment-label">{{ circledSegmentNumber(form.routes.length + 1) }} {{ form.routes.length + 1 }}</span><el-tooltip :content="getFinalRouteTitle().replace(`段${form.routes.length + 1}`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getFinalRouteTitle(), form.routes.length + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式<span class="required-asterisk">*</span></span><el-select v-model="form.finalTransportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div>
<template #icon><span class="route-step-icon route-step-icon--end"></span></template> </div>
<template #title><div class="route-step-title"><span class="route-segment-label">{{ form.routes.length + 1 }}</span><el-tooltip :content="getFinalRouteTitle().replace(`段${form.routes.length + 1}`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getFinalRouteTitle(), form.routes.length + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式</span><el-select v-model="form.finalTransportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template> <div class="route-flow-row">
<template #description> <span class="route-flow-marker route-flow-marker--end"></span>
<div class="route-flow-body">
<div class="route-node-form"> <div class="route-node-form">
<el-form-item label="收货地" required class="route-address-form-item" <el-form-item label="收货地" required class="route-address-form-item"
><div class="route-address-inputs" ><div class="route-address-inputs"
><el-cascader v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!form.finalTransportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="!form.finalTransportType || !isRoadTransportType(form.finalTransportType)" placeholder="请选择省市区" @click="openAddressMap('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="!form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div ><el-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!form.finalTransportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="!form.finalTransportType" placeholder="请选择运输方式" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="!form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
></el-form-item> ></el-form-item>
<el-form-item label="联系人" <el-form-item label="联系人" class="route-contact-form-item"
><el-input v-model="form.arrivalContact" placeholder="请输入" /></el-form-item> ><el-input v-model="form.arrivalContact" placeholder="请输入" /></el-form-item>
<el-form-item label="联系方式" <el-form-item label="联系方式" class="route-phone-form-item"
><el-input v-model="form.arrivalPhone" placeholder="请输入" /></el-form-item> ><el-input v-model="form.arrivalPhone" placeholder="请输入" /></el-form-item>
<el-form-item label="计划结束日期" <el-form-item label="计划结束" class="route-date-form-item"
><el-date-picker ><el-date-picker
v-model="form.planEndTime" v-model="form.planEndTime"
type="date" type="date"
format="YYYY-MM-DD" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="请选择" placeholder="请选择"
style="width: 200px"
/></el-form-item> /></el-form-item>
</div> </div>
</template> </div>
</el-step> </div>
</el-steps>
</div></el-form </div></el-form
> >
</section> </section>
@@ -157,7 +159,8 @@
</template></el-autocomplete </template></el-autocomplete
></template></el-table-column ></template></el-table-column
><el-table-column label="货物类型" min-width="130" ><el-table-column label="货物类型" min-width="130"
><template #default="{ row }" ><template #header><span>货物类型<span class="required-asterisk">*</span></span></template>
<template #default="{ row }"
><el-cascader ><el-cascader
v-model="row.cargoTypePath" v-model="row.cargoTypePath"
:options="cargoTypeOptions" :options="cargoTypeOptions"
@@ -170,14 +173,16 @@
@change="value => handleCargoTypeChange(row, value)" @change="value => handleCargoTypeChange(row, value)"
/></template></el-table-column /></template></el-table-column
><el-table-column label="数量" width="180" class-name="master-editor__quantity-cell" ><el-table-column label="数量" width="180" class-name="master-editor__quantity-cell"
><template #default="{ row }" ><template #header><span>数量<span class="required-asterisk">*</span></span></template>
<template #default="{ row }"
><el-input ><el-input
v-model="row.quantity" v-model="row.quantity"
inputmode="decimal" inputmode="decimal"
placeholder="请输入" placeholder="请输入"
@input="value => handleQuantityInput(row, value)" /></template></el-table-column @input="value => handleQuantityInput(row, value)" /></template></el-table-column
><el-table-column label="数量单位" width="120" ><el-table-column label="数量单位" width="120"
><template #default="{ row }" ><template #header><span>数量单位<span class="required-asterisk">*</span></span></template>
<template #default="{ row }"
><el-select v-model="row.quantityUnit" ><el-select v-model="row.quantityUnit"
><el-option v-for="unit in quantityUnitOptions" :key="unit" :label="unit" :value="unit" /></el-select></template></el-table-column ><el-option v-for="unit in quantityUnitOptions" :key="unit" :label="unit" :value="unit" /></el-select></template></el-table-column
><el-table-column label="包装" width="130" ><el-table-column label="包装" width="130"
@@ -194,6 +199,15 @@
><el-table-column label="型号" min-width="120" ><el-table-column label="型号" min-width="120"
><template #default="{ row }" ><template #default="{ row }"
><el-input v-model="row.model" placeholder="请输入" /></template></el-table-column ><el-input v-model="row.model" placeholder="请输入" /></template></el-table-column
><el-table-column label="物料编码" min-width="140"
><template #default="{ row }"
><el-input v-model="row.materialCode" placeholder="请输入" /></template></el-table-column
><el-table-column label="设备编码" min-width="140"
><template #default="{ row }"
><el-input v-model="row.deviceCode" placeholder="请输入" /></template></el-table-column
><el-table-column label="备注" min-width="180"
><template #default="{ row }"
><el-input v-model="row.remark" maxlength="200" show-word-limit placeholder="请输入" /></template></el-table-column
><el-table-column label="操作" width="140" ><el-table-column label="操作" width="140"
><template #default="{ $index }" ><template #default="{ $index }"
><div class="goods-actions" ><div class="goods-actions"
@@ -717,6 +731,11 @@ export default {
responseData(response) { responseData(response) {
return response?.data?.data || response?.data || response || {}; return response?.data?.data || response?.data || response || {};
}, },
circledSegmentNumber(number) {
const value = Number(number);
const circledNumbers = '①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳';
return circledNumbers[value - 1] || String(value);
},
async loadRegionOptions() { async loadRegionOptions() {
if (this.regionOptions.length) return this.regionOptions; if (this.regionOptions.length) return this.regionOptions;
if (this.regionRequest) return this.regionRequest; if (this.regionRequest) return this.regionRequest;
@@ -767,9 +786,19 @@ export default {
}, },
extractChinaRegionOptions(regions = []) { extractChinaRegionOptions(regions = []) {
const china = regions.find(item => ['中国', '中华人民共和国'].includes(item.title)); const china = regions.find(item => ['中国', '中华人民共和国'].includes(item.title));
if (china?.children?.length) return china.children; const chinaChildren = china?.children || [];
if (regions.length === 1 && regions[0].children?.length) return regions[0].children; // 部分行政区划数据的省级节点 parentId 为 0,另一部分挂在“中国(+86)”下,需合并两类根节点。
return regions; const rootRegions = regions.filter(
item => item !== china && ['0', '', 'null', 'undefined'].includes(String(item.parentId ?? ''))
);
const merged = [...chinaChildren, ...rootRegions];
const seen = new Set();
return merged.filter(item => {
const key = String(item.id ?? item.value ?? item.title ?? '');
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}, },
findRegionPathByName(options = [], regionName = '', parents = []) { findRegionPathByName(options = [], regionName = '', parents = []) {
const target = String(regionName || '').replace(/\s+/g, ''); const target = String(regionName || '').replace(/\s+/g, '');
@@ -805,6 +834,32 @@ export default {
}); });
return labels; return labels;
}, },
setAddressRegionNames(field, labels = []) {
if (!field) return;
const normalized = labels.map(label => String(label || '').trim()).filter(Boolean);
const province = normalized.find(label => /省|自治区|特别行政区/.test(label)) || normalized[0] || '';
const city = normalized.filter(label => /市|州|盟/.test(label)).at(-1) || '';
const district = normalized.filter(label => /区|县|旗/.test(label)).at(-1) || '';
if (field.provinceName) field.model[field.provinceName] = province;
if (field.cityName) field.model[field.cityName] = city;
if (field.districtName) field.model[field.districtName] = district;
},
setAddressRegionNamesFromRow(field, row = {}) {
const labels = [row.provinceName, row.cityName, row.districtName].filter(Boolean);
if (labels.length) {
this.setAddressRegionNames(field, labels);
return;
}
const text = String(row.regionName || '').replace(/\s+/g, '');
if (!text) return;
const provinceMatch = text.match(/^.*?(?:省|自治区|特别行政区)/);
const rest = provinceMatch ? text.slice(provinceMatch[0].length) : text;
const cityMatch = rest.match(/.+?(?:市|州|盟)/);
const districtMatch = cityMatch
? rest.slice(cityMatch[0].length).match(/^[^市州盟]*?(?:区|县|旗)/)
: rest.match(/.+?(?:区|县|旗)/);
this.setAddressRegionNames(field, [provinceMatch?.[0], cityMatch?.[0], districtMatch?.[0]]);
},
handleAddressRegionChange(target, value) { handleAddressRegionChange(target, value) {
const field = this.addressModel(target); const field = this.addressModel(target);
const path = Array.isArray(value) ? value.map(item => String(item)) : []; const path = Array.isArray(value) ? value.map(item => String(item)) : [];
@@ -813,6 +868,8 @@ export default {
const labels = this.regionLabels(path); const labels = this.regionLabels(path);
field.model[field.name] = labels.join(''); field.model[field.name] = labels.join('');
field.model[field.regionCode] = path.at(-1) || ''; field.model[field.regionCode] = path.at(-1) || '';
field.model[field.siteCode] = '';
this.setAddressRegionNames(field, labels);
}, },
syncAddressRegionPath(target) { syncAddressRegionPath(target) {
const field = this.addressModel(target); const field = this.addressModel(target);
@@ -822,6 +879,7 @@ export default {
? codePath ? codePath
: this.findRegionPathByName(this.regionOptions, field.model[field.name]); : this.findRegionPathByName(this.regionOptions, field.model[field.name]);
this.addressRegionPaths = { ...this.addressRegionPaths, [target]: path }; this.addressRegionPaths = { ...this.addressRegionPaths, [target]: path };
if (path.length) this.setAddressRegionNames(field, this.regionLabels(path));
}, },
syncAddressRegionPaths() { syncAddressRegionPaths() {
if (!this.regionOptions.length) return; if (!this.regionOptions.length) return;
@@ -880,19 +938,27 @@ export default {
const date = this.normalizeDateValue(value); const date = this.normalizeDateValue(value);
return date ? `${date} ${endOfDay ? '23:59:59' : '00:00:00'}` : ''; return date ? `${date} ${endOfDay ? '23:59:59' : '00:00:00'}` : '';
}, },
getRegionDisplay(target, fallback) { getRoadRegionDisplay(target, fallback) {
const labels = this.regionLabels(this.addressRegionPaths[target] || []); const labels = this.regionLabels(this.addressRegionPaths[target] || []);
return labels.length ? labels.join('/') : fallback; return labels.at(-1) || fallback;
},
getAddressDisplay(target, fallback) {
const field = this.addressModel(target);
const name = field?.model?.[field.name] || fallback;
return this.isNonRoadAddress(target) ? name : this.getRoadRegionDisplay(target, name);
}, },
getRouteTitle(index) { getRouteTitle(index) {
const departureName = const departureName =
index === 0 index === 0
? this.getRegionDisplay('departure', this.form.departureName || '发货地址') ? this.getAddressDisplay(
: this.getRegionDisplay( 'departure',
this.form.departureName || '发货地址'
)
: this.getAddressDisplay(
`route-${index - 1}`, `route-${index - 1}`,
this.form.routes[index - 1].departureName || '途经地' this.form.routes[index - 1].departureName || '途经地'
); );
const arrivalName = this.getRegionDisplay( const arrivalName = this.getAddressDisplay(
`route-${index}`, `route-${index}`,
this.form.routes[index].departureName || '途经地' this.form.routes[index].departureName || '途经地'
); );
@@ -912,11 +978,14 @@ export default {
return `${this.truncateRouteAddress(departure)}${separator}${this.truncateRouteAddress(arrival)}`; return `${this.truncateRouteAddress(departure)}${separator}${this.truncateRouteAddress(arrival)}`;
}, },
getFinalRouteTitle() { getFinalRouteTitle() {
const departureName = this.getRegionDisplay( const departureName = this.getAddressDisplay(
`route-${this.form.routes.length - 1}`, `route-${this.form.routes.length - 1}`,
this.lastRouteName this.lastRouteName
); );
const arrivalName = this.getRegionDisplay('arrival', this.form.arrivalName || '收货地址'); const arrivalName = this.getAddressDisplay(
'arrival',
this.form.arrivalName || '收货地址'
);
return `段${this.form.routes.length + 1}${departureName} → ${arrivalName}`; return `段${this.form.routes.length + 1}${departureName} → ${arrivalName}`;
}, },
responseRecords(response) { responseRecords(response) {
@@ -965,6 +1034,9 @@ export default {
latitude: 'departureLatitude', latitude: 'departureLatitude',
regionCode: 'departureRegionCode', regionCode: 'departureRegionCode',
siteCode: 'departureSiteCode', siteCode: 'departureSiteCode',
provinceName: 'departureProvinceName',
cityName: 'departureCityName',
districtName: 'departureDistrictName',
} }
: null; : null;
} }
@@ -979,13 +1051,16 @@ export default {
latitude: `${prefix}Latitude`, latitude: `${prefix}Latitude`,
regionCode: `${prefix}RegionCode`, regionCode: `${prefix}RegionCode`,
siteCode: `${prefix}SiteCode`, siteCode: `${prefix}SiteCode`,
provinceName: `${prefix}ProvinceName`,
cityName: `${prefix}CityName`,
districtName: `${prefix}DistrictName`,
}; };
}, },
addressTransportType(target) { addressTransportType(target) {
if (target.startsWith('route-')) { if (target.startsWith('route-')) {
return this.form.routes[Number(target.slice(6))]?.transportType || ''; return this.form.routes[Number(target.slice(6))]?.transportType || '';
} }
return target === 'arrival' ? this.form.finalTransportType : ''; return target === 'arrival' ? this.form.finalTransportType : this.form.routes[0]?.transportType || '';
}, },
addressTransportMode(target) { addressTransportMode(target) {
const type = this.addressTransportType(target); const type = this.addressTransportType(target);
@@ -998,6 +1073,17 @@ export default {
const value = String(type || '').trim().toLowerCase(); const value = String(type || '').trim().toLowerCase();
return value === 'road' || value.includes('公路'); return value === 'road' || value.includes('公路');
}, },
isNonRoadAddress(target) {
const field = this.addressModel(target);
if (!field) return false;
const siteCode = String(field.model[field.siteCode] || '').trim();
// 地址展示模式由实际选择的数据决定:有站点编码表示非公路站点名称,
// 没有站点编码则保留省市区级联,避免切换运输方式时改写已有地址。
return Boolean(siteCode && siteCode !== '/');
},
resolveAddressRegionCode(row = {}) {
return row.regionCode || row.districtCode || row.areaCode || row.adcode || '';
},
openCommonAddress(target) { openCommonAddress(target) {
this.addressTarget = target; this.addressTarget = target;
if (this.addressTransportMode(target) !== 'road') { if (this.addressTransportMode(target) !== 'road') {
@@ -1054,14 +1140,15 @@ export default {
const target = this.addressModel(this.addressTarget); const target = this.addressModel(this.addressTarget);
if (!target) return; if (!target) return;
const station = this.normalizeStation(row); const station = this.normalizeStation(row);
target.model[target.name] = station.regionName || station.stationName; target.model[target.name] = station.stationName || station.regionName;
target.model[target.address] = station.detailAddress; target.model[target.address] = station.detailAddress;
target.model[target.longitude] = station.longitude || ''; target.model[target.longitude] = station.longitude || '';
target.model[target.latitude] = station.latitude || ''; target.model[target.latitude] = station.latitude || '';
target.model[target.regionCode] = station.regionCode || ''; target.model[target.regionCode] = this.resolveAddressRegionCode(station);
target.model[target.siteCode] = station.stationCode; target.model[target.siteCode] = station.stationCode;
this.setAddressRegionNamesFromRow(target, station);
this.stationDialogVisible = false; this.stationDialogVisible = false;
this.syncAddressRegionPath(this.addressTarget); this.loadRegionOptions().then(() => this.syncAddressRegionPath(this.addressTarget));
}, },
async loadAddressList() { async loadAddressList() {
this.addressLoading = true; this.addressLoading = true;
@@ -1096,12 +1183,21 @@ export default {
target.model[target.phone] = row.contactPhone || target.model[target.phone] || ''; target.model[target.phone] = row.contactPhone || target.model[target.phone] || '';
target.model[target.longitude] = row.longitude || ''; target.model[target.longitude] = row.longitude || '';
target.model[target.latitude] = row.latitude || ''; target.model[target.latitude] = row.latitude || '';
target.model[target.regionCode] = row.regionCode || ''; target.model[target.regionCode] = this.resolveAddressRegionCode(row);
target.model[target.siteCode] = '';
this.setAddressRegionNamesFromRow(target, row);
this.addressDialogVisible = false; this.addressDialogVisible = false;
this.syncAddressRegionPath(this.addressTarget); this.loadRegionOptions().then(() => this.syncAddressRegionPath(this.addressTarget));
},
handleAddressDetailClick(target) {
if (this.addressTransportMode(target) !== 'road') {
this.openCommonAddress(target);
return;
}
this.openAddressMap(target);
}, },
openAddressMap(target) { openAddressMap(target) {
if ((target.startsWith('route-') || target === 'arrival') && !this.isRoadTransportType(this.addressTransportType(target))) { if (this.addressTransportMode(target) !== 'road') {
this.$message.info('非公路运输地址不能使用地图选择'); this.$message.info('非公路运输地址不能使用地图选择');
return; return;
} }
@@ -1203,6 +1299,9 @@ export default {
const component = result.regeocode?.addressComponent || {}; const component = result.regeocode?.addressComponent || {};
this.mapSelected.detailAddress = this.mapSelected.detailAddress =
result.regeocode?.formattedAddress || this.mapSelected.detailAddress; result.regeocode?.formattedAddress || this.mapSelected.detailAddress;
this.mapSelected.provinceName = component.province || '';
this.mapSelected.cityName = component.city || '';
this.mapSelected.districtName = component.district || '';
this.mapSelected.regionName = [component.province, component.city, component.district] this.mapSelected.regionName = [component.province, component.city, component.district]
.filter(Boolean) .filter(Boolean)
.join(''); .join('');
@@ -1228,6 +1327,8 @@ export default {
target.model[target.longitude] = this.mapSelected.longitude; target.model[target.longitude] = this.mapSelected.longitude;
target.model[target.latitude] = this.mapSelected.latitude; target.model[target.latitude] = this.mapSelected.latitude;
target.model[target.regionCode] = this.mapSelected.regionCode || ''; target.model[target.regionCode] = this.mapSelected.regionCode || '';
target.model[target.siteCode] = '';
this.setAddressRegionNamesFromRow(target, this.mapSelected);
this.mapDialogVisible = false; this.mapDialogVisible = false;
this.syncAddressRegionPath(this.mapTarget); this.syncAddressRegionPath(this.mapTarget);
}, },
@@ -1445,6 +1546,8 @@ export default {
'brand', 'brand',
'specification', 'specification',
'model', 'model',
'materialCode',
'deviceCode',
'remark', 'remark',
].some(key => firstGoods[key]); ].some(key => firstGoods[key]);
@@ -1489,6 +1592,8 @@ export default {
brand: row.brand || '', brand: row.brand || '',
specification: row.specification || row.spec || '', specification: row.specification || row.spec || '',
model: row.model || '', model: row.model || '',
materialCode: row.materialCode || '',
deviceCode: row.deviceCode || '',
remark: row.remark || row.descriptionOne || '', remark: row.remark || row.descriptionOne || '',
}; };
}, },
@@ -1590,7 +1695,12 @@ export default {
this.syncAddressRegionPaths(); this.syncAddressRegionPaths();
}, },
addGoods(index) { addGoods(index) {
this.form.goods.splice(index + 1, 0, { quantityUnit: quantityUnitOptions[0] }); this.form.goods.splice(index + 1, 0, {
quantityUnit: quantityUnitOptions[0],
materialCode: '',
deviceCode: '',
remark: '',
});
}, },
removeGoods(index) { removeGoods(index) {
if (this.form.goods.length > 1) this.form.goods.splice(index, 1); if (this.form.goods.length > 1) this.form.goods.splice(index, 1);
@@ -1707,13 +1817,20 @@ export default {
await this.$refs.form.validate(); await this.$refs.form.validate();
if (!this.form.departureAddress || !this.form.arrivalAddress) if (!this.form.departureAddress || !this.form.arrivalAddress)
return this.$message.warning('请填写收发货地址'); return this.$message.warning('请填写收发货地址');
if (!this.form.goods.some(item => item.cargoType && item.quantity > 0 && item.quantityUnit)) const invalidGoodsIndex = this.form.goods.findIndex(
return this.$message.warning('请至少填写一条有效货物'); item =>
if ( !String(item.cargoType || '').trim() ||
this.form.routes.some(item => !item.departureName || !item.transportType) || !(Number(item.quantity) > 0) ||
!this.form.finalTransportType !String(item.quantityUnit || '').trim()
) );
return this.$message.warning('请完整填写途经地及运输方式'); if (invalidGoodsIndex >= 0)
return this.$message.warning(`请完整填写第${invalidGoodsIndex + 1}条货物的类型、数量和数量单位`);
const invalidRouteIndex = this.form.routes.findIndex(
item => !item.departureName || !item.transportType
);
if (invalidRouteIndex >= 0)
return this.$message.warning(`请完整填写第${invalidRouteIndex + 1}段途经地及运输方式`);
if (!this.form.finalTransportType) return this.$message.warning('请填写最后一段运输方式');
} }
const payload = { const payload = {
...this.form, ...this.form,
@@ -1721,7 +1838,17 @@ export default {
planEndTime: this.normalizeDateTimeValue(this.form.planEndTime, true), planEndTime: this.normalizeDateTimeValue(this.form.planEndTime, true),
attachmentsJson: JSON.stringify(this.attachmentRows), attachmentsJson: JSON.stringify(this.attachmentRows),
routes: [ routes: [
...this.form.routes.map((route, index) => ({ ...route, segmentNo: `段${index + 1}` })), ...this.form.routes.map((route, index) => ({
...route,
segmentNo: `段${index + 1}`,
...(index === 0
? {
departureProvinceName: this.form.departureProvinceName,
departureCityName: this.form.departureCityName,
departureDistrictName: this.form.departureDistrictName,
}
: {}),
})),
{ {
segmentNo: `段${this.form.routes.length + 1}`, segmentNo: `段${this.form.routes.length + 1}`,
departureName: this.form.arrivalName, departureName: this.form.arrivalName,
@@ -1732,6 +1859,9 @@ export default {
departureLatitude: this.form.arrivalLatitude, departureLatitude: this.form.arrivalLatitude,
departureRegionCode: this.form.arrivalRegionCode, departureRegionCode: this.form.arrivalRegionCode,
departureSiteCode: this.form.arrivalSiteCode, departureSiteCode: this.form.arrivalSiteCode,
departureProvinceName: this.form.arrivalProvinceName,
departureCityName: this.form.arrivalCityName,
departureDistrictName: this.form.arrivalDistrictName,
transportType: this.form.finalTransportType, transportType: this.form.finalTransportType,
}, },
], ],
@@ -1887,38 +2017,32 @@ export default {
justify-content: flex-end; justify-content: flex-end;
margin-top: 12px; margin-top: 12px;
} }
.route-steps { .route-flow {
position: relative; position: relative;
.el-steps { padding: 4px 0;
--el-text-color-placeholder: #a8abb2; &::before {
position: absolute;
top: 20px;
bottom: 20px;
left: 16px;
width: 2px;
background: #dcdfe6;
content: '';
} }
:deep(.el-step__main) { }
padding-bottom: 16px; .route-flow-row {
} position: relative;
:deep(.el-step__title) { display: grid;
font-size: 16px; grid-template-columns: 32px minmax(0, 1fr);
color: #303133; column-gap: 16px;
} align-items: start;
:deep(.el-step__description) { }
padding: 12px 0 0; .route-flow-marker {
} position: relative;
:deep(.el-step__line) { z-index: 1;
background-color: #dcdfe6;
}
:deep(.el-step__icon) {
width: 32px;
height: 32px;
flex-shrink: 0;
border: 0;
background: transparent;
}
.route-step-icon {
display: inline-flex; display: inline-flex;
width: 32px; width: 32px;
height: 32px; height: 32px;
min-width: 32px;
min-height: 32px;
flex-shrink: 0;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 6px; border-radius: 6px;
@@ -1933,11 +2057,19 @@ export default {
&--end { &--end {
background: #e6a23c; background: #e6a23c;
} }
} }
.route-flow-body {
min-width: 0;
}
.route-flow-segment {
min-height: 48px;
padding-left: 48px;
display: flex;
align-items: center;
} }
.route-node-form { .route-node-form {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(420px, 1fr)) repeat(2, minmax(260px, 1fr)); grid-template-columns: 300px 360px repeat(3, 300px);
gap: 0 16px; gap: 0 16px;
align-items: start; align-items: start;
:deep(.el-form-item) { :deep(.el-form-item) {
@@ -1963,9 +2095,24 @@ export default {
.route-address-name { .route-address-name {
flex: 0 0 220px; flex: 0 0 220px;
} }
.route-address-detail, .route-address-detail {
flex: 0 0 300px;
width: 300px;
}
.route-contact-actions :deep(.el-input) { .route-contact-actions :deep(.el-input) {
flex: 1; flex: 0 0 200px;
width: 200px;
}
:deep(.route-contact-form-item .el-input),
:deep(.route-phone-form-item .el-input),
:deep(.route-date-form-item .el-date-editor) {
width: 200px;
}
:deep(.route-contact-actions) {
width: auto;
}
:deep(.el-form-item__label) {
white-space: nowrap;
} }
.address-picker-button { .address-picker-button {
flex: 0 0 auto; flex: 0 0 auto;
@@ -1985,6 +2132,7 @@ export default {
font-size: 16px; font-size: 16px;
.route-segment-label { .route-segment-label {
flex: 0 0 auto; flex: 0 0 auto;
white-space: nowrap;
} }
.route-title-path { .route-title-path {
display: inline-block; display: inline-block;
@@ -2010,6 +2158,10 @@ export default {
} }
} }
} }
.required-asterisk {
margin-left: 4px;
color: #f56c6c;
}
.route-action-button { .route-action-button {
flex: 0 0 auto; flex: 0 0 auto;
} }
@@ -152,10 +152,7 @@
@click="handleTransportSecondaryAddressInputClick('departure')" @click="handleTransportSecondaryAddressInputClick('departure')"
> >
<template #suffix> <template #suffix>
<el-icon <el-icon @click.stop="openTransportMapDialog('departure')">
v-if="!transportStationMode"
@click.stop="openTransportMapDialog('departure')"
>
<Location /> <Location />
</el-icon> </el-icon>
</template> </template>
@@ -198,7 +195,7 @@
@click="handleTransportSecondaryAddressInputClick('arrival')" @click="handleTransportSecondaryAddressInputClick('arrival')"
> >
<template #suffix> <template #suffix>
<el-icon v-if="!transportStationMode" @click.stop="openTransportMapDialog('arrival')"> <el-icon @click.stop="openTransportMapDialog('arrival')">
<Location /> <Location />
</el-icon> </el-icon>
</template> </template>
@@ -559,9 +556,15 @@
label-width="auto" label-width="auto"
class="shipping-template-page__detail-form shipping-template-page__detail-grid" class="shipping-template-page__detail-form shipping-template-page__detail-grid"
> >
<el-form-item label="模板编号"><span>{{ detailRow.templateCode || '-' }}</span></el-form-item> <el-form-item label="模板编号"
<el-form-item label="模板名称"><span>{{ detailRow.templateName || '-' }}</span></el-form-item> ><span>{{ detailRow.templateCode || '-' }}</span></el-form-item
<el-form-item label="模板类型"><span>{{ detailRow.templateType || '-' }}</span></el-form-item> >
<el-form-item label="模板名称"
><span>{{ detailRow.templateName || '-' }}</span></el-form-item
>
<el-form-item label="模板类型"
><span>{{ detailRow.templateType || '-' }}</span></el-form-item
>
<el-form-item label="备注" class="shipping-template-page__detail-form-wide"> <el-form-item label="备注" class="shipping-template-page__detail-form-wide">
<span>{{ detailRow.remark || '-' }}</span> <span>{{ detailRow.remark || '-' }}</span>
</el-form-item> </el-form-item>
@@ -575,10 +578,16 @@
label-width="auto" label-width="auto"
class="shipping-template-page__detail-form shipping-template-page__detail-grid" class="shipping-template-page__detail-form shipping-template-page__detail-grid"
> >
<el-form-item label="项目"><span>{{ detailRow.projectName || '-' }}</span></el-form-item> <el-form-item label="项目"
<el-form-item label="客户合同"><span>{{ detailRow.contractName || '-' }}</span></el-form-item> ><span>{{ detailRow.projectName || '-' }}</span></el-form-item
>
<el-form-item label="客户合同"
><span>{{ detailRow.contractName || '-' }}</span></el-form-item
>
<el-form-item label="运输方式"> <el-form-item label="运输方式">
<span>{{ getTransportTypeLabel(detailRow.transportType) || detailRow.transportType || '-' }}</span> <span>{{
getTransportTypeLabel(detailRow.transportType) || detailRow.transportType || '-'
}}</span>
</el-form-item> </el-form-item>
<el-form-item label="备注" class="shipping-template-page__detail-form-wide"> <el-form-item label="备注" class="shipping-template-page__detail-form-wide">
<span>{{ detailRow.basicRemark || '-' }}</span> <span>{{ detailRow.basicRemark || '-' }}</span>
@@ -588,7 +597,11 @@
<section class="shipping-template-page__detail-section"> <section class="shipping-template-page__detail-section">
<div class="dialog-section-title">收发货信息</div> <div class="dialog-section-title">收发货信息</div>
<el-form label-position="right" label-width="auto" class="shipping-template-page__detail-form"> <el-form
label-position="right"
label-width="auto"
class="shipping-template-page__detail-form"
>
<el-form-item label="发货地址"> <el-form-item label="发货地址">
<div class="shipping-template-page__detail-route-values"> <div class="shipping-template-page__detail-route-values">
<span>{{ detailRow.departureName || '-' }}</span> <span>{{ detailRow.departureName || '-' }}</span>
@@ -617,11 +630,21 @@
<el-table-column prop="cargoName" label="货物名称" min-width="220" align="center" /> <el-table-column prop="cargoName" label="货物名称" min-width="220" align="center" />
<el-table-column prop="packageType" label="包装" min-width="120" align="center" /> <el-table-column prop="packageType" label="包装" min-width="120" align="center" />
<el-table-column prop="quantity" label="数量" min-width="120" align="center" /> <el-table-column prop="quantity" label="数量" min-width="120" align="center" />
<el-table-column prop="quantityUnit" label="数量单位" min-width="120" align="center" /> <el-table-column
prop="quantityUnit"
label="数量单位"
min-width="120"
align="center"
/>
<el-table-column prop="brand" label="品牌" min-width="120" align="center" /> <el-table-column prop="brand" label="品牌" min-width="120" align="center" />
<el-table-column prop="specification" label="规格" min-width="120" align="center" /> <el-table-column prop="specification" label="规格" min-width="120" align="center" />
<el-table-column prop="model" label="型号" min-width="120" align="center" /> <el-table-column prop="model" label="型号" min-width="120" align="center" />
<el-table-column prop="materialCode" label="物料编码" min-width="140" align="center" /> <el-table-column
prop="materialCode"
label="物料编码"
min-width="140"
align="center"
/>
<el-table-column prop="deviceCode" label="设备编码" min-width="140" align="center" /> <el-table-column prop="deviceCode" label="设备编码" min-width="140" align="center" />
<el-table-column prop="remark" label="备注" min-width="160" align="center" /> <el-table-column prop="remark" label="备注" min-width="160" align="center" />
</el-table> </el-table>
@@ -638,11 +661,16 @@
label-width="auto" label-width="auto"
:class="[ :class="[
'shipping-template-page__freight-form', 'shipping-template-page__freight-form',
`shipping-template-page__freight-form--${detailTransportMode === 'road' ? 'road' : 'non-road'}`, `shipping-template-page__freight-form--${
detailTransportMode === 'road' ? 'road' : 'non-road'
}`,
]" ]"
> >
<template v-if="detailTransportMode === 'road'"> <template v-if="detailTransportMode === 'road'">
<template v-for="(item, index) in detailFreightItems" :key="item.quantityUnit || index"> <template
v-for="(item, index) in detailFreightItems"
:key="item.quantityUnit || index"
>
<el-form-item label="单价"> <el-form-item label="单价">
<el-input :model-value="item.unitPrice || '-'" disabled> <el-input :model-value="item.unitPrice || '-'" disabled>
<template #append>{{ item.priceUnit || '-' }}</template> <template #append>{{ item.priceUnit || '-' }}</template>
@@ -690,7 +718,9 @@
<el-table-column type="index" label="序号" width="70" /> <el-table-column type="index" label="序号" width="70" />
<el-table-column label="文件名" min-width="220"> <el-table-column label="文件名" min-width="220">
<template #default="{ row }"> <template #default="{ row }">
<el-link type="primary" @click="previewAttachment(row)">{{ attachmentName(row) }}</el-link> <el-link type="primary" @click="previewAttachment(row)">{{
attachmentName(row)
}}</el-link>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="附件描述" min-width="220"> <el-table-column label="附件描述" min-width="220">
@@ -901,7 +931,8 @@
><el-button @click="transportMapBox = false">取消</el-button ><el-button @click="transportMapBox = false">取消</el-button
><el-button ><el-button
type="primary" type="primary"
:disabled="!transportMapSelected.longitude" :loading="transportMapLoading"
:disabled="!transportMapSelected.detailAddress || !transportMapSelected.regionName"
@click="confirmTransportMapPick" @click="confirmTransportMapPick"
>确定</el-button >确定</el-button
></template ></template
@@ -1410,10 +1441,7 @@ export default {
return this.parseJsonArray(this.detailRow.goodsJson); return this.parseJsonArray(this.detailRow.goodsJson);
}, },
detailCargoQuantityTotal() { detailCargoQuantityTotal() {
const total = this.detailGoodsRows.reduce( const total = this.detailGoodsRows.reduce((sum, row) => sum + (Number(row.quantity) || 0), 0);
(sum, row) => sum + (Number(row.quantity) || 0),
0
);
return total ? String(Number(total.toFixed(3))) : '系统自动计算'; return total ? String(Number(total.toFixed(3))) : '系统自动计算';
}, },
detailAttachmentRows() { detailAttachmentRows() {
@@ -1433,9 +1461,7 @@ export default {
return this.resolveTransportMode(this.detailRow.transportType); return this.resolveTransportMode(this.detailRow.transportType);
}, },
detailFreightItems() { detailFreightItems() {
return Array.isArray(this.detailFreight.freightItems) return Array.isArray(this.detailFreight.freightItems) ? this.detailFreight.freightItems : [];
? this.detailFreight.freightItems
: [];
}, },
detailFreightTotal() { detailFreightTotal() {
const storedTotal = Number(this.detailFreight.totalFreightAmount); const storedTotal = Number(this.detailFreight.totalFreightAmount);
@@ -1970,6 +1996,7 @@ export default {
this.contractLoading = true; this.contractLoading = true;
getContractList(1, 9999, { getContractList(1, 9999, {
projectId: this.form.projectId, projectId: this.form.projectId,
projectName: this.form.projectName,
...(this.config.contractQueryParams || {}), ...(this.config.contractQueryParams || {}),
}) })
.then(res => { .then(res => {
@@ -2239,6 +2266,9 @@ export default {
this.form[`${prefix}Latitude`] = address.latitude || ''; this.form[`${prefix}Latitude`] = address.latitude || '';
this.form[`${prefix}Contact`] = address.contactName || this.form[`${prefix}Contact`] || ''; this.form[`${prefix}Contact`] = address.contactName || this.form[`${prefix}Contact`] || '';
this.form[`${prefix}Phone`] = address.contactPhone || this.form[`${prefix}Phone`] || ''; this.form[`${prefix}Phone`] = address.contactPhone || this.form[`${prefix}Phone`] || '';
this.$nextTick(() => {
this.$refs.crud?.clearValidate?.([`${prefix}Address`]);
});
}, },
openTransportStationDialog(target) { openTransportStationDialog(target) {
if (!this.form.transportType) return this.$message.warning('请先选择运输方式'); if (!this.form.transportType) return this.$message.warning('请先选择运输方式');
@@ -2299,8 +2329,19 @@ export default {
}, },
openTransportMapDialog(target) { openTransportMapDialog(target) {
if (this.dialogReadonly || !this.form.transportType) return; if (this.dialogReadonly || !this.form.transportType) return;
this.transportMapTarget = target; const prefix = target === 'departure' ? 'departure' : 'arrival';
this.transportMapKeyword = this.form[`${target}Address`] || ''; this.transportMapTarget = prefix;
this.transportMapKeyword = this.form[`${prefix}Address`] || this.form[`${prefix}Name`] || '';
this.transportMapSelected = this.buildTransportMapSelection({
longitude: this.form[`${prefix}Longitude`],
latitude: this.form[`${prefix}Latitude`],
detailAddress: this.form[`${prefix}Address`],
regionName: this.form[`${prefix}Name`],
addressCode: this.form[`${prefix}AddressCode`],
});
this.transportMapStatus = this.transportMapSelected.longitude
? '已加载当前选点,可重新选点'
: '可搜索地址或点击地图选点';
this.transportMapBox = true; this.transportMapBox = true;
}, },
loadAmap() { loadAmap() {
@@ -2310,8 +2351,9 @@ export default {
window._AMapSecurityConfig = { securityJsCode: AMAP_SECURITY_CODE }; window._AMapSecurityConfig = { securityJsCode: AMAP_SECURITY_CODE };
const script = document.createElement('script'); const script = document.createElement('script');
script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}`; script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}`;
script.async = true;
script.onload = resolve; script.onload = resolve;
script.onerror = reject; script.onerror = () => reject(new Error('高德地图组件加载失败'));
document.body.appendChild(script); document.body.appendChild(script);
}); });
return amapLoader; return amapLoader;
@@ -2321,56 +2363,185 @@ export default {
.then(() => { .then(() => {
this.$nextTick(() => { this.$nextTick(() => {
if (!this.transportMapInstance) { if (!this.transportMapInstance) {
const center = this.transportMapSelected.longitude
? this.toLngLat(
this.transportMapSelected.longitude,
this.transportMapSelected.latitude
)
: [116.40769, 39.89945];
this.transportMapInstance = new window.AMap.Map(this.$refs.transportMap, { this.transportMapInstance = new window.AMap.Map(this.$refs.transportMap, {
center: [116.40769, 39.89945], center,
zoom: 11, zoom: this.transportMapSelected.longitude ? 14 : 11,
}); });
this.transportMapInstance.on('click', event => this.pickMapPoint(event.lnglat)); this.transportMapInstance.on('click', event => this.pickMapPoint(event.lnglat));
} else this.transportMapInstance.resize(); } else {
this.transportMapInstance.resize();
}
if (this.transportMapSelected.longitude) {
this.renderTransportMapMarker(
this.toLngLat(
this.transportMapSelected.longitude,
this.transportMapSelected.latitude
)
);
} else {
this.transportMapMarker?.setMap(null);
this.transportMapMarker = null;
}
}); });
}) })
.catch(() => { .catch(() => {
this.$message.error('高德地图组件加载失败'); this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
this.transportMapBox = false; this.transportMapBox = false;
}); });
}, },
searchTransportMapKeyword() { searchTransportMapKeyword() {
if (!this.transportMapKeyword) return this.$message.warning('请输入地址关键词'); const keyword = String(this.transportMapKeyword || '').trim();
this.loadAmap().then(() => { if (!keyword) return this.$message.warning('请输入地址关键词');
this.transportMapLoading = true; this.transportMapLoading = true;
window.AMap.plugin('AMap.Geocoder', () => { this.loadAmap()
this.transportMapGeocoder = this.transportMapGeocoder || new window.AMap.Geocoder(); .then(() => this.ensureTransportMapGeocoder())
this.transportMapGeocoder.getLocation(this.transportMapKeyword, (status, result) => { .then(() => this.runTransportMapGeocode('location', keyword))
.then(result => {
const point = this.resolveTransportMapPoint(result);
if (!point) throw new Error('未找到匹配地址');
return this.pickMapPoint(point, keyword);
})
.catch(() => {
this.transportMapStatus = '地图搜索失败';
this.$message.warning('地图搜索无匹配地址');
})
.finally(() => {
this.transportMapLoading = false; this.transportMapLoading = false;
const location = status === 'complete' && result.geocodes?.[0]?.location;
if (location) this.pickMapPoint(location, this.transportMapKeyword);
else this.$message.warning('地图搜索无匹配地址');
});
});
}); });
}, },
pickMapPoint(point, keyword = '') { pickMapPoint(point, keyword = '') {
const longitude = point.getLng ? point.getLng() : point.lng; const longitude = this.getTransportMapPointLng(point);
const latitude = point.getLat ? point.getLat() : point.lat; const latitude = this.getTransportMapPointLat(point);
if (longitude === undefined || latitude === undefined) return; if (!Number.isFinite(Number(longitude)) || !Number.isFinite(Number(latitude))) {
this.transportMapSelected = { this.$message.warning('选点坐标无效');
longitude: Number(longitude).toFixed(6), return Promise.resolve();
latitude: Number(latitude).toFixed(6),
detailAddress: keyword || this.transportMapKeyword,
};
this.transportMapStatus = '已选点,可确认回填';
if (this.transportMapInstance) {
this.transportMapMarker?.setMap(null);
this.transportMapMarker = new window.AMap.Marker({ position: [longitude, latitude] });
this.transportMapMarker.setMap(this.transportMapInstance);
this.transportMapInstance.setCenter([longitude, latitude]);
} }
const lnglat = this.toLngLat(longitude, latitude);
this.renderTransportMapMarker(lnglat);
this.transportMapSelected = this.buildTransportMapSelection({ longitude, latitude });
this.transportMapStatus = '正在反查地址...';
this.transportMapLoading = true;
return this.ensureTransportMapGeocoder()
.then(() => this.runTransportMapGeocode('address', lnglat))
.then(result => {
const address = this.resolveTransportMapAddress(result);
this.transportMapSelected = {
...this.transportMapSelected,
detailAddress: address.detailAddress || keyword,
regionName: address.regionName,
addressCode: address.addressCode,
};
this.transportMapKeyword =
this.transportMapSelected.detailAddress || this.transportMapKeyword;
this.transportMapStatus = this.transportMapSelected.detailAddress || '已选点,可确认回填';
})
.catch(() => {
this.transportMapSelected = {};
this.transportMapStatus = '反查地址失败';
this.$message.error('反查地址失败,请重新选点');
})
.finally(() => {
this.transportMapLoading = false;
});
},
renderTransportMapMarker(point) {
if (!this.transportMapInstance || !window.AMap) return;
this.transportMapMarker?.setMap(null);
this.transportMapMarker = new window.AMap.Marker({ position: point });
this.transportMapMarker.setMap(this.transportMapInstance);
this.transportMapInstance.setCenter(point);
}, },
confirmTransportMapPick() { confirmTransportMapPick() {
if (!this.transportMapSelected.detailAddress || !this.transportMapSelected.regionName) {
this.$message.warning('请等待地址解析完成后再确认');
return;
}
const prefix = this.transportMapTarget; const prefix = this.transportMapTarget;
this.applyAddress(prefix, this.transportMapSelected); this.applyAddress(prefix, this.transportMapSelected);
this.transportMapBox = false; this.transportMapBox = false;
}, },
buildTransportMapSelection({ longitude, latitude, detailAddress, regionName, addressCode }) {
return {
longitude: this.formatTransportMapCoordinate(longitude),
latitude: this.formatTransportMapCoordinate(latitude),
detailAddress: detailAddress || '',
regionName: regionName || '',
addressCode: addressCode || '',
};
},
formatTransportMapCoordinate(value) {
if (value === undefined || value === null || value === '') return '';
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue.toFixed(6) : '';
},
toLngLat(longitude, latitude) {
return new window.AMap.LngLat(Number(longitude), Number(latitude));
},
getTransportMapPointLng(point) {
if (!point) return undefined;
return typeof point.getLng === 'function' ? point.getLng() : point.lng ?? point.lon;
},
getTransportMapPointLat(point) {
if (!point) return undefined;
return typeof point.getLat === 'function' ? point.getLat() : point.lat;
},
ensureTransportMapGeocoder() {
if (this.transportMapGeocoder) return Promise.resolve(this.transportMapGeocoder);
return new Promise((resolve, reject) => {
if (!window.AMap?.plugin) {
reject(new Error('高德地图组件未就绪'));
return;
}
window.AMap.plugin(['AMap.Geocoder'], () => {
try {
this.transportMapGeocoder = new window.AMap.Geocoder();
resolve(this.transportMapGeocoder);
} catch (error) {
reject(error);
}
});
});
},
runTransportMapGeocode(action, input) {
return new Promise((resolve, reject) => {
const timer = window.setTimeout(() => reject(new Error('高德地图请求超时')), 10000);
const done = (status, result) => {
window.clearTimeout(timer);
if (status === 'complete' && result) {
resolve(result);
return;
}
reject(new Error('高德地图请求失败'));
};
try {
if (action === 'location') this.transportMapGeocoder.getLocation(input, done);
else this.transportMapGeocoder.getAddress(input, done);
} catch (error) {
window.clearTimeout(timer);
reject(error);
}
});
},
resolveTransportMapPoint(result) {
const geocode = Array.isArray(result?.geocodes) ? result.geocodes[0] : null;
return geocode?.location || result?.location || result?.lnglat || null;
},
resolveTransportMapAddress(result = {}) {
const regeocode = result.regeocode || {};
const component = regeocode.addressComponent || {};
const city = Array.isArray(component.city) ? '' : component.city;
return {
detailAddress: regeocode.formattedAddress || '',
regionName: [component.province, city, component.district].filter(Boolean).join(''),
addressCode: component.adcode || '',
};
},
ensureCargoTypeOptions() { ensureCargoTypeOptions() {
if (this.cargoTypeRequest) return this.cargoTypeRequest; if (this.cargoTypeRequest) return this.cargoTypeRequest;
if (this.cargoTypeOptions.length) return Promise.resolve(this.cargoTypeOptions); if (this.cargoTypeOptions.length) return Promise.resolve(this.cargoTypeOptions);
@@ -2538,9 +2709,7 @@ export default {
quantityUnits() { quantityUnits() {
return [ return [
...new Set( ...new Set(
this.transportCargoRows this.transportCargoRows.map(row => String(row.quantityUnit || '').trim()).filter(Boolean)
.map(row => String(row.quantityUnit || '').trim())
.filter(Boolean)
), ),
]; ];
}, },
@@ -2990,10 +3159,7 @@ export default {
} }
.shipping-template-page__detail-route-values { .shipping-template-page__detail-route-values {
display: grid; display: grid;
grid-template-columns: minmax(160px, 1fr) minmax(220px, 2fr) minmax(140px, 1fr) minmax( grid-template-columns: minmax(160px, 1fr) minmax(220px, 2fr) minmax(140px, 1fr) minmax(160px, 1fr);
160px,
1fr
);
gap: 8px; gap: 8px;
width: 100%; width: 100%;
} }
@@ -1703,7 +1703,7 @@
<div <div
class="transport-plan-page__dispatch-item-grid transport-plan-page__dispatch-item-grid--full" class="transport-plan-page__dispatch-item-grid transport-plan-page__dispatch-item-grid--full"
> >
<template v-for="(group, index) in dispatchItemFreightGroups" :key="group.key"> <template v-for="group in dispatchItemFreightGroups" :key="group.key">
<el-form-item label="单价"> <el-form-item label="单价">
<el-input <el-input
:model-value="dispatchFreightGroupUnitPrice(group)" :model-value="dispatchFreightGroupUnitPrice(group)"
@@ -1728,11 +1728,15 @@
</el-form-item> </el-form-item>
<el-form-item label="数量合计" <el-form-item label="数量合计"
><el-input :model-value="dispatchFreightGroupQuantity(group)" disabled ><el-input :model-value="dispatchFreightGroupQuantity(group)" disabled
><template #append>{{ group.quantityUnit || '吨' }}</template></el-input ><template #append>{{ fixedQuantityUnit }}</template></el-input
></el-form-item ></el-form-item
> >
<el-form-item label="运费" <el-form-item label="运费"
><el-input :model-value="dispatchFreightGroupAmount(group)" disabled ><el-input
:model-value="dispatchFreightGroupAmount(group)"
placeholder="请输入运费"
@input="value => handleDispatchFreightGroupAmountInput(group, value)"
@clear="() => handleDispatchFreightGroupAmountInput(group, '')"
><template #suffix>{{ dispatchItemFreightCurrencyLabel }}</template></el-input ><template #suffix>{{ dispatchItemFreightCurrencyLabel }}</template></el-input
></el-form-item ></el-form-item
> >
@@ -1886,6 +1890,7 @@
<el-select <el-select
v-model="dispatchItemForm.quantityUnit" v-model="dispatchItemForm.quantityUnit"
class="transport-plan-page__dispatch-unit-select" class="transport-plan-page__dispatch-unit-select"
clearable
> >
<el-option <el-option
v-for="item in quantityUnitOptions" v-for="item in quantityUnitOptions"
@@ -1954,6 +1959,15 @@
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item label="运费">
<el-input
v-model="dispatchItemForm.freightAmount"
placeholder="请输入运费"
@input="value => handleDispatchNumberInput('freightAmount', value)"
>
<template #suffix>{{ dispatchItemFreightCurrencyLabel }}</template>
</el-input>
</el-form-item>
<el-form-item label="其他费用合计"> <el-form-item label="其他费用合计">
<el-input v-model="dispatchItemForm.otherFeeTotal" placeholder="请输入"> <el-input v-model="dispatchItemForm.otherFeeTotal" placeholder="请输入">
<template #suffix>{{ dispatchItemFreightCurrencyLabel }}</template> <template #suffix>{{ dispatchItemFreightCurrencyLabel }}</template>
@@ -2857,6 +2871,8 @@ const transportPlanImportColumns = [
['备注', 'remark'], ['备注', 'remark'],
]; ];
const TRANSPORT_PLAN_QUANTITY_UNIT = '吨';
const defaultTransportCargo = () => ({ const defaultTransportCargo = () => ({
cargoName: '', cargoName: '',
cargoType: '', cargoType: '',
@@ -2908,6 +2924,7 @@ const defaultDispatchRow = () => ({
materialCode: '', materialCode: '',
unitPrice: '', unitPrice: '',
priceUnit: '元/吨', priceUnit: '元/吨',
freightAmount: '',
otherFeeTotal: '', otherFeeTotal: '',
freightCurrency: 'RMB', freightCurrency: 'RMB',
freightJson: '', freightJson: '',
@@ -3327,7 +3344,7 @@ export default {
dispatchSummaryItems() { dispatchSummaryItems() {
const summaryMap = new Map(); const summaryMap = new Map();
const addQuantity = (unit, field, quantity) => { const addQuantity = (unit, field, quantity) => {
const normalizedUnit = unit || '吨'; const normalizedUnit = unit || TRANSPORT_PLAN_QUANTITY_UNIT;
if (!summaryMap.has(normalizedUnit)) { if (!summaryMap.has(normalizedUnit)) {
summaryMap.set(normalizedUnit, { unit: normalizedUnit, total: 0, assigned: 0 }); summaryMap.set(normalizedUnit, { unit: normalizedUnit, total: 0, assigned: 0 });
} }
@@ -3413,6 +3430,9 @@ export default {
dispatchTransportMode() { dispatchTransportMode() {
return this.resolveTransportMode(this.dispatchItemForm.transportType); return this.resolveTransportMode(this.dispatchItemForm.transportType);
}, },
fixedQuantityUnit() {
return TRANSPORT_PLAN_QUANTITY_UNIT;
},
dispatchIsRoadTransport() { dispatchIsRoadTransport() {
return this.dispatchTransportMode === 'road'; return this.dispatchTransportMode === 'road';
}, },
@@ -3461,27 +3481,30 @@ export default {
}, },
dispatchItemFreightTotal() { dispatchItemFreightTotal() {
if (this.dispatchItemForm.taskEntryMode !== 'full') { if (this.dispatchItemForm.taskEntryMode !== 'full') {
const quantity = Number(this.dispatchItemForm.quantity || 0);
const unitPrice = Number(this.dispatchItemForm.unitPrice || 0);
const otherFeeTotal = Number(this.dispatchItemForm.otherFeeTotal || 0); const otherFeeTotal = Number(this.dispatchItemForm.otherFeeTotal || 0);
const hasFreight = this.dispatchItemForm.unitPrice !== '' && quantity > 0; const freight = this.dispatchItemFreightSubtotal;
const hasFreight = freight !== '';
const hasOtherFee = this.dispatchItemForm.otherFeeTotal !== ''; const hasOtherFee = this.dispatchItemForm.otherFeeTotal !== '';
if (!hasFreight && !hasOtherFee) return ''; if (!hasFreight && !hasOtherFee) return '';
const total = (hasFreight ? quantity * unitPrice : 0) + (hasOtherFee ? otherFeeTotal : 0); const total = (hasFreight ? Number(freight) : 0) + (hasOtherFee ? otherFeeTotal : 0);
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2))); return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)));
} }
const otherFeeTotal = Number(this.dispatchItemForm.otherFeeTotal || 0); const otherFeeTotal = Number(this.dispatchItemForm.otherFeeTotal || 0);
const freightTotal = this.dispatchItemFreightGroups.reduce( const freightSubtotal = this.dispatchItemFreightSubtotal;
(total, group) => total + Number(this.dispatchFreightGroupAmount(group) || 0),
0
);
const hasOtherFee = this.dispatchItemForm.otherFeeTotal !== ''; const hasOtherFee = this.dispatchItemForm.otherFeeTotal !== '';
if (!freightTotal && !hasOtherFee) return ''; if (freightSubtotal === '' && !hasOtherFee) return '';
const total = freightTotal + (hasOtherFee ? otherFeeTotal : 0); const total = Number(freightSubtotal || 0) + (hasOtherFee ? otherFeeTotal : 0);
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2))); return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)));
}, },
dispatchItemFreightSubtotal() { dispatchItemFreightSubtotal() {
if (this.dispatchItemForm.taskEntryMode !== 'full') { if (this.dispatchItemForm.taskEntryMode !== 'full') {
if (
this.dispatchItemForm.freightAmount !== undefined &&
this.dispatchItemForm.freightAmount !== null &&
String(this.dispatchItemForm.freightAmount).trim() !== ''
) {
return this.formatDispatchAmount(this.dispatchItemForm.freightAmount);
}
const quantity = Number(this.dispatchItemForm.quantity || 0); const quantity = Number(this.dispatchItemForm.quantity || 0);
const unitPrice = Number(this.dispatchItemForm.unitPrice || 0); const unitPrice = Number(this.dispatchItemForm.unitPrice || 0);
if (!quantity || !unitPrice) return ''; if (!quantity || !unitPrice) return '';
@@ -3492,27 +3515,25 @@ export default {
(sum, group) => sum + Number(this.dispatchFreightGroupAmount(group) || 0), (sum, group) => sum + Number(this.dispatchFreightGroupAmount(group) || 0),
0 0
); );
return total const hasEnteredAmount = this.dispatchItemFreightGroups.some(group =>
? Number.isInteger(total) (group.rows || []).some(
? String(total) cargo =>
: String(Number(total.toFixed(2))) cargo.freightAmount !== undefined &&
: ''; cargo.freightAmount !== null &&
String(cargo.freightAmount).trim() !== ''
)
);
if (!total && !hasEnteredAmount) return '';
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)));
}, },
dispatchItemFreightGroups() { dispatchItemFreightGroups() {
const groups = []; return [
const groupMap = new Map(); {
(this.dispatchItemCargoRows || []).forEach((cargo, index) => { key: '__all__',
const quantityUnit = String(cargo.quantityUnit || '').trim(); quantityUnit: TRANSPORT_PLAN_QUANTITY_UNIT,
const key = quantityUnit || `__empty_${index}`; rows: this.dispatchItemCargoRows || [],
let group = groupMap.get(key); },
if (!group) { ];
group = { key, quantityUnit, rows: [] };
groupMap.set(key, group);
groups.push(group);
}
group.rows.push(cargo);
});
return groups;
}, },
dispatchItemFreightCurrencyLabel() { dispatchItemFreightCurrencyLabel() {
const value = this.dispatchItemForm.freightCurrency || 'RMB'; const value = this.dispatchItemForm.freightCurrency || 'RMB';
@@ -4447,7 +4468,7 @@ export default {
return Number.isFinite(number) ? number : 0; return Number.isFinite(number) ? number : 0;
}, },
getDispatchQuantityUnit(row = {}) { getDispatchQuantityUnit(row = {}) {
return row.quantityUnit || row.goodsQuantityUnit || row.unit || '吨'; return row.quantityUnit || row.goodsQuantityUnit || row.unit || TRANSPORT_PLAN_QUANTITY_UNIT;
}, },
getDispatchVehicleIdentifier(row = {}) { getDispatchVehicleIdentifier(row = {}) {
return ( return (
@@ -4907,7 +4928,7 @@ export default {
specification: row.specification || row.spec || '', specification: row.specification || row.spec || '',
freightAmount: row.freightAmount ?? row.amount ?? row.totalAmount ?? '', freightAmount: row.freightAmount ?? row.amount ?? row.totalAmount ?? '',
}; };
['quantity', 'mileage', 'unitPrice'].forEach(prop => { ['quantity', 'mileage', 'unitPrice', 'freightAmount'].forEach(prop => {
if (Number(cargo[prop]) === -1) cargo[prop] = ''; if (Number(cargo[prop]) === -1) cargo[prop] = '';
}); });
return cargo; return cargo;
@@ -6908,7 +6929,7 @@ export default {
item.quantityUnit || item.quantityUnit ||
item.goodsQuantityUnit || item.goodsQuantityUnit ||
item.unit || item.unit ||
'吨', TRANSPORT_PLAN_QUANTITY_UNIT,
specification: specification:
item.specification || item.spec || itemGoods.specification || itemGoods.spec || '', item.specification || item.spec || itemGoods.specification || itemGoods.spec || '',
model: item.model || itemGoods.model || '', model: item.model || itemGoods.model || '',
@@ -6968,17 +6989,16 @@ export default {
0 0
); );
const calculatedFreight = unitPrice !== '' && quantity ? Number(unitPrice) * quantity : ''; const calculatedFreight = unitPrice !== '' && quantity ? Number(unitPrice) * quantity : '';
const freight = const explicitFreight = [
calculatedFreight !== '' item.freight,
? calculatedFreight item.freightAmount,
: item.freight ?? item.transportFee,
item.freightAmount ?? freightInfo.freightAmount,
item.transportFee ?? freightInfo.transportFee,
plan.freight ?? plan.freight,
plan.freightAmount ?? plan.freightAmount,
freightInfo.freightAmount ?? ].find(value => value !== undefined && value !== null && String(value).trim() !== '');
freightInfo.transportFee ?? const freight = explicitFreight ?? calculatedFreight;
'';
const freightTotal = const freightTotal =
freight !== '' || otherFeeTotal !== '' freight !== '' || otherFeeTotal !== ''
? Number(freight || 0) + Number(otherFeeTotal || 0) ? Number(freight || 0) + Number(otherFeeTotal || 0)
@@ -6989,7 +7009,7 @@ export default {
freightInfo.totalFreightAmount ?? freightInfo.totalFreightAmount ??
freightInfo.freightTotal ?? freightInfo.freightTotal ??
''; '';
return { unitPrice, freight, otherFeeTotal, freightTotal }; return { unitPrice, freight, freightAmount: freight, otherFeeTotal, freightTotal };
}, },
getDispatchWaybillFallback(plan = {}, item = {}, index = 0) { getDispatchWaybillFallback(plan = {}, item = {}, index = 0) {
const sources = [ const sources = [
@@ -7264,6 +7284,8 @@ export default {
...cargo, ...cargo,
unitPrice: cargo.unitPrice || freightInfo.freightItems[itemIndex]?.unitPrice || '', unitPrice: cargo.unitPrice || freightInfo.freightItems[itemIndex]?.unitPrice || '',
priceUnit: cargo.priceUnit || freightInfo.freightItems[itemIndex]?.priceUnit || '元/吨', priceUnit: cargo.priceUnit || freightInfo.freightItems[itemIndex]?.priceUnit || '元/吨',
freightAmount:
cargo.freightAmount || freightInfo.freightItems[itemIndex]?.freightAmount || '',
})); }));
} }
this.dispatchItemAttachmentRows = this.parseJsonArray( this.dispatchItemAttachmentRows = this.parseJsonArray(
@@ -7480,12 +7502,24 @@ export default {
} }
}, },
dispatchCargoFreightAmount(cargo = {}) { dispatchCargoFreightAmount(cargo = {}) {
if (cargo.freightAmount !== undefined && cargo.freightAmount !== null) {
const enteredAmount = String(cargo.freightAmount).trim();
if (enteredAmount !== '' && Number.isFinite(Number(enteredAmount))) {
const amount = Number(enteredAmount);
return Number.isInteger(amount) ? String(amount) : String(Number(amount.toFixed(2)));
}
}
const quantity = Number(cargo.quantity || 0); const quantity = Number(cargo.quantity || 0);
const unitPrice = Number(cargo.unitPrice || 0); const unitPrice = Number(cargo.unitPrice || 0);
if (!quantity || !unitPrice) return ''; if (!quantity || !unitPrice) return '';
const amount = quantity * unitPrice; const amount = quantity * unitPrice;
return Number.isInteger(amount) ? String(amount) : String(Number(amount.toFixed(2))); return Number.isInteger(amount) ? String(amount) : String(Number(amount.toFixed(2)));
}, },
formatDispatchAmount(value) {
const amount = Number(value || 0);
if (!Number.isFinite(amount)) return '';
return Number(amount.toFixed(2)).toString();
},
dispatchFreightGroupQuantity(group = {}) { dispatchFreightGroupQuantity(group = {}) {
const quantity = (group.rows || []).reduce( const quantity = (group.rows || []).reduce(
(sum, cargo) => sum + Number(cargo.quantity || 0), (sum, cargo) => sum + Number(cargo.quantity || 0),
@@ -7515,6 +7549,7 @@ export default {
const unitPrice = group.rows?.[0]?.unitPrice || ''; const unitPrice = group.rows?.[0]?.unitPrice || '';
(group.rows || []).forEach(cargo => { (group.rows || []).forEach(cargo => {
cargo.unitPrice = unitPrice; cargo.unitPrice = unitPrice;
cargo.freightAmount = '';
}); });
}, },
handleDispatchFreightGroupPriceUnitChange(group, value) { handleDispatchFreightGroupPriceUnitChange(group, value) {
@@ -7522,6 +7557,31 @@ export default {
cargo.priceUnit = value || ''; cargo.priceUnit = value || '';
}); });
}, },
handleDispatchFreightGroupAmountInput(group, value) {
const text = String(value || '').replace(/[^\d.]/g, '');
const parts = text.split('.');
const normalized =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
const rows = group.rows || [];
const total = Number(normalized || 0);
const quantities = rows.map(cargo => Math.max(0, Number(cargo.quantity || 0)));
const quantityTotal = quantities.reduce((sum, quantity) => sum + quantity, 0);
let allocated = 0;
rows.forEach((cargo, index) => {
if (index === rows.length - 1) {
cargo.freightAmount = this.formatDispatchAmount(total - allocated);
return;
}
let amount = 0;
if (quantityTotal) {
amount = Number((total * (quantities[index] / quantityTotal)).toFixed(2));
} else if (index === 0) {
amount = total;
}
allocated += amount;
cargo.freightAmount = this.formatDispatchAmount(amount);
});
},
dispatchItemRemainingQuantity(row = {}) { dispatchItemRemainingQuantity(row = {}) {
const unit = this.getDispatchQuantityUnit(row); const unit = this.getDispatchQuantityUnit(row);
const hasCargoIdentity = Boolean( const hasCargoIdentity = Boolean(
@@ -7718,6 +7778,11 @@ export default {
detailPrefix ? `${goodsPrefix}单价不能小于0` : `${goodsPrefix}单价不能小于0` detailPrefix ? `${goodsPrefix}单价不能小于0` : `${goodsPrefix}单价不能小于0`
); );
} }
if (!this.isBillingFieldEmpty(goods.freightAmount) && Number(goods.freightAmount) < 0) {
return warn(
detailPrefix ? `${goodsPrefix}运费不能小于0` : `${goodsPrefix}运费不能小于0`
);
}
} }
const invalidPhoneField = [ const invalidPhoneField = [
@@ -7854,7 +7919,7 @@ export default {
attachmentsJson: JSON.stringify(this.dispatchItemAttachmentRows), attachmentsJson: JSON.stringify(this.dispatchItemAttachmentRows),
}; };
nextRow.cargoInfo = this.formatDispatchCargoInfo({}, nextRow); nextRow.cargoInfo = this.formatDispatchCargoInfo({}, nextRow);
nextRow.freight = this.getDispatchFeeFields({}, nextRow).freight; nextRow.freight = this.dispatchItemFreightSubtotal;
nextRow.otherFeeTotal = this.dispatchItemForm.otherFeeTotal || ''; nextRow.otherFeeTotal = this.dispatchItemForm.otherFeeTotal || '';
const quantityUnit = this.getDispatchQuantityUnit(nextRow); const quantityUnit = this.getDispatchQuantityUnit(nextRow);
const otherDispatchedQuantity = this.dispatchRows.reduce((total, row, index) => { const otherDispatchedQuantity = this.dispatchRows.reduce((total, row, index) => {
@@ -8093,6 +8158,7 @@ export default {
:deep(.el-form-item__content > .el-input), :deep(.el-form-item__content > .el-input),
:deep(.el-form-item__content > .el-select), :deep(.el-form-item__content > .el-select),
:deep(.el-form-item__content > .el-cascader), :deep(.el-form-item__content > .el-cascader),
:deep(.el-form-item__content > .el-autocomplete),
:deep(.el-form-item__content > .el-input-number), :deep(.el-form-item__content > .el-input-number),
:deep(.el-form-item__content > .el-date-editor) { :deep(.el-form-item__content > .el-date-editor) {
width: 250px; width: 250px;
@@ -8134,7 +8200,9 @@ export default {
&__shipping-title, &__shipping-title,
&__goods-title { &__goods-title {
box-sizing: border-box;
min-width: 100%; min-width: 100%;
max-width: 100%;
} }
&__shipping-title, &__shipping-title,
@@ -8215,7 +8283,7 @@ export default {
} }
&__dispatch-shipping-form &__route-address-row { &__dispatch-shipping-form &__route-address-row {
grid-template-columns: 260px minmax(280px, 1fr) 58px minmax(160px, 220px) 72px minmax( grid-template-columns: 260px minmax(280px, 360px) 58px minmax(160px, 220px) 72px minmax(
160px, 160px,
220px 220px
); );
@@ -8248,8 +8316,10 @@ export default {
&__cargo-wrap { &__cargo-wrap {
display: block; display: block;
box-sizing: border-box;
width: 100%; width: 100%;
min-width: 0; min-width: 0;
max-width: 100%;
overflow: hidden; overflow: hidden;
} }
@@ -8707,13 +8777,21 @@ export default {
} }
&__dispatch-item-dialog { &__dispatch-item-dialog {
width: calc(100% - 32px) !important;
max-width: 1400px;
:deep(.el-dialog__body) { :deep(.el-dialog__body) {
min-width: 0;
padding-top: 20px; padding-top: 20px;
overflow-x: hidden;
} }
} }
&__dispatch-item-form { &__dispatch-item-form {
box-sizing: border-box;
width: 100%; width: 100%;
min-width: 0;
max-width: 100%;
padding: 16px; padding: 16px;
border: 1px solid #ebeef5; border: 1px solid #ebeef5;
background: #fff; background: #fff;
@@ -8810,7 +8888,10 @@ export default {
} }
&__attachment { &__attachment {
box-sizing: border-box;
width: 100%; width: 100%;
min-width: 0;
max-width: 100%;
} }
&__attachment-description { &__attachment-description {
@@ -8832,8 +8913,12 @@ export default {
&__attachment-head { &__attachment-head {
display: flex; display: flex;
box-sizing: border-box;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
width: 100%;
min-width: 0;
max-width: 100%;
margin-bottom: 16px; margin-bottom: 16px;
} }
@@ -9204,6 +9289,13 @@ export default {
grid-template-columns: 240px minmax(240px, 1fr) 40px 58px minmax(140px, 1fr); grid-template-columns: 240px minmax(240px, 1fr) 40px 58px minmax(140px, 1fr);
} }
&__dispatch-shipping-form &__route-address-row {
grid-template-columns: 240px minmax(240px, 300px) 58px minmax(140px, 180px) 72px minmax(
140px,
180px
);
}
&__route-address-row &__route-label:nth-of-type(2), &__route-address-row &__route-label:nth-of-type(2),
&__route-address-row .el-input:last-child { &__route-address-row .el-input:last-child {
grid-column: auto; grid-column: auto;
@@ -9215,6 +9307,10 @@ export default {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
&__dispatch-shipping-form &__route-address-row {
grid-template-columns: 1fr;
}
&__route-label { &__route-label {
text-align: left; text-align: left;
} }
File diff suppressed because it is too large Load Diff
@@ -1053,7 +1053,7 @@
label-width="auto" label-width="auto"
class="waybill-manage-page__freight-form waybill-manage-page__freight-form--road" class="waybill-manage-page__freight-form waybill-manage-page__freight-form--road"
> >
<template v-for="(group, index) in taskFreightGroups" :key="group.key"> <template v-for="group in taskFreightGroups" :key="group.key">
<el-form-item label="单价"> <el-form-item label="单价">
<el-input <el-input
:model-value="taskFullFreightGroupUnitPrice(group)" :model-value="taskFullFreightGroupUnitPrice(group)"
@@ -1093,10 +1093,7 @@
placeholder="单位" placeholder="单位"
disabled disabled
> >
<el-option <el-option :label="fixedQuantityUnit" :value="fixedQuantityUnit" />
:label="group.quantityUnit || '单位'"
:value="group.quantityUnit || ''"
/>
</el-select> </el-select>
</template> </template>
</el-input> </el-input>
@@ -1718,32 +1715,53 @@
</span> </span>
</div> </div>
<div class="waybill-manage-page__waybill-route"> <div class="waybill-manage-page__waybill-route">
<div class="waybill-manage-page__waybill-route-item"> <div class="waybill-manage-page__waybill-route-item is-start">
<span class="waybill-manage-page__waybill-route-dot"></span> <div class="waybill-manage-page__waybill-route-marker"></div>
<div class="waybill-manage-page__waybill-route-detail"> <div class="waybill-manage-page__waybill-route-detail">
<span class="waybill-manage-page__waybill-route-label">发货地</span> <strong class="waybill-manage-page__waybill-route-name">
<strong class="waybill-manage-page__waybill-route-name">{{ {{
waybillDetailRouteName(detailRow, 'departure') formatWaybillRouteCityDistrict(
}}</strong> detailRow.departureAddress || detailRow.departureName
<el-tooltip :content="detailRow.departureAddress || '-'" placement="top"> )
<span class="waybill-manage-page__waybill-route-addr">{{ }}
formatWaybillProvinceCityDistrict(detailRow.departureAddress) </strong>
}}</span> <el-tooltip
:content="detailRow.departureAddress || detailRow.departureName || '-'"
placement="top"
>
<span class="waybill-manage-page__waybill-route-addr">
{{ detailRow.departureAddress || detailRow.departureName || '-' }}
</span>
</el-tooltip> </el-tooltip>
<div class="waybill-manage-page__waybill-route-contact">
<span>联系人{{ detailRow.departureContact || '-' }}</span>
<span>联系方式{{ detailRow.departurePhone || '-' }}</span>
</div> </div>
</div> </div>
<div class="waybill-manage-page__waybill-route-item"> </div>
<span class="waybill-manage-page__waybill-route-dot is-end"></span> <span class="waybill-manage-page__waybill-route-line" aria-hidden="true" />
<div class="waybill-manage-page__waybill-route-item is-end">
<div class="waybill-manage-page__waybill-route-marker"></div>
<div class="waybill-manage-page__waybill-route-detail"> <div class="waybill-manage-page__waybill-route-detail">
<span class="waybill-manage-page__waybill-route-label">收货地</span> <strong class="waybill-manage-page__waybill-route-name">
<strong class="waybill-manage-page__waybill-route-name">{{ {{
waybillDetailRouteName(detailRow, 'arrival') formatWaybillRouteCityDistrict(
}}</strong> detailRow.arrivalAddress || detailRow.arrivalName
<el-tooltip :content="detailRow.arrivalAddress || '-'" placement="top"> )
<span class="waybill-manage-page__waybill-route-addr">{{ }}
formatWaybillProvinceCityDistrict(detailRow.arrivalAddress) </strong>
}}</span> <el-tooltip
:content="detailRow.arrivalAddress || detailRow.arrivalName || '-'"
placement="top"
>
<span class="waybill-manage-page__waybill-route-addr">
{{ detailRow.arrivalAddress || detailRow.arrivalName || '-' }}
</span>
</el-tooltip> </el-tooltip>
<div class="waybill-manage-page__waybill-route-contact">
<span>联系人{{ detailRow.arrivalContact || '-' }}</span>
<span>联系方式{{ detailRow.arrivalPhone || '-' }}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1793,75 +1811,65 @@
<div class="waybill-manage-page__waybill-detail-field"> <div class="waybill-manage-page__waybill-detail-field">
<span>承运类型</span><strong>{{ detailRow.carrierType || '-' }}</strong> <span>承运类型</span><strong>{{ detailRow.carrierType || '-' }}</strong>
</div> </div>
<div <div class="waybill-manage-page__waybill-detail-field">
v-if="waybillIsCarrier(detailRow)"
class="waybill-manage-page__waybill-detail-field"
>
<span>承运商</span><strong>{{ detailRow.carrierName || '-' }}</strong> <span>承运商</span><strong>{{ detailRow.carrierName || '-' }}</strong>
</div> </div>
<div <template v-if="waybillIsRoadTransport(detailRow)">
v-if="waybillIsRoadTransport(detailRow)" <div class="waybill-manage-page__waybill-detail-field">
class="waybill-manage-page__waybill-detail-field"
>
<span>司机</span <span>司机</span
><strong ><strong
>{{ detailRow.driverName || '-' }} {{ detailRow.driverPhone || '' }}</strong >{{
[detailRow.driverName, detailRow.driverPhone].filter(Boolean).join(' ') || '-'
}}</strong
> >
</div> </div>
<div class="waybill-manage-page__waybill-detail-field">
<span>车牌号</span><strong>{{ detailRow.vehicleNo || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>挂车车牌</span><strong>{{ detailRow.trailerVehicleNo || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>押运人</span
><strong
>{{
[detailRow.escortName, detailRow.escortPhone].filter(Boolean).join(' ') || '-'
}}</strong
>
</div>
</template>
<template v-else-if="waybillIsNonRoadTransport(detailRow)">
<div class="waybill-manage-page__waybill-detail-field">
<span>船长</span
><strong
>{{
[detailRow.captainName, detailRow.driverPhone].filter(Boolean).join(' ') || '-'
}}</strong
>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>航班号/船次号/班列号</span>
<strong>{{ detailRow.vehicleNo || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>箱号</span><strong>{{ detailRow.containerNo || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>仓位</span><strong>{{ detailRow.cabinNo || '-' }}</strong>
</div>
</template>
<template v-else>
<div class="waybill-manage-page__waybill-detail-field"> <div class="waybill-manage-page__waybill-detail-field">
<span>{{ waybillVehicleNoLabel(detailRow) }}</span> <span>{{ waybillVehicleNoLabel(detailRow) }}</span>
<strong>{{ detailRow.vehicleNo || '-' }}</strong> <strong>{{ detailRow.vehicleNo || '-' }}</strong>
</div> </div>
</template>
<div <div
v-if="waybillIsNonRoadTransport(detailRow)"
class="waybill-manage-page__waybill-detail-field" class="waybill-manage-page__waybill-detail-field"
> :class="{
<span>船长</span><strong>{{ detailRow.captainName || '-' }}</strong> 'waybill-manage-page__waybill-detail-field--remark': waybillIsRoadTransport(detailRow),
</div> 'waybill-manage-page__waybill-detail-field--remark-full': waybillIsNonRoadTransport(detailRow),
<div }"
v-if="waybillIsNonRoadTransport(detailRow) && waybillIsCarrier(detailRow)"
class="waybill-manage-page__waybill-detail-field"
>
<span>联系电话</span><strong>{{ detailRow.driverPhone || '-' }}</strong>
</div>
<div
v-if="waybillIsNonRoadTransport(detailRow)"
class="waybill-manage-page__waybill-detail-field"
>
<span>箱号</span><strong>{{ detailRow.containerNo || '-' }}</strong>
</div>
<div
v-if="waybillIsNonRoadTransport(detailRow)"
class="waybill-manage-page__waybill-detail-field"
>
<span>舱位</span><strong>{{ detailRow.cabinNo || '-' }}</strong>
</div>
<div
v-if="waybillIsRoadTransport(detailRow) && !waybillIsCarrier(detailRow)"
class="waybill-manage-page__waybill-detail-field"
>
<span>挂车车牌号</span><strong>{{ detailRow.trailerVehicleNo || '-' }}</strong>
</div>
<div
v-if="waybillIsRoadTransport(detailRow) && !waybillIsCarrier(detailRow)"
class="waybill-manage-page__waybill-detail-field"
>
<span>押运人</span><strong>{{ detailRow.escortName || '-' }}</strong>
</div>
<div
v-if="waybillIsRoadTransport(detailRow) && !waybillIsCarrier(detailRow)"
class="waybill-manage-page__waybill-detail-field"
>
<span>押运人手机号</span><strong>{{ detailRow.escortPhone || '-' }}</strong>
</div>
<div
v-if="waybillIsRoadTransport(detailRow) && !waybillIsCarrier(detailRow)"
class="waybill-manage-page__waybill-detail-field"
>
<span>里程(km)</span><strong>{{ detailRow.mileage || '-' }}</strong>
</div>
<div
class="waybill-manage-page__waybill-detail-field waybill-manage-page__waybill-detail-field--remark"
> >
<span>备注</span <span>备注</span
><strong>{{ detailRow.taskRemark || detailRow.remark || '-' }}</strong> ><strong>{{ detailRow.taskRemark || detailRow.remark || '-' }}</strong>
@@ -2204,7 +2212,11 @@
</template> </template>
</el-dialog> </el-dialog>
<waybill-import-dialog v-if="config.enableWaybillImport" v-model="waybillImportBox" /> <waybill-import-dialog
v-if="config.enableWaybillImport"
v-model="waybillImportBox"
@closed="refreshChange"
/>
<flow-design <flow-design
v-if="website.design.designMode" v-if="website.design.designMode"
@@ -2891,6 +2903,8 @@ const extractRecords = res => {
return []; return [];
}; };
const WAYBILL_QUANTITY_UNIT = '吨';
const defaultTransportCargo = () => ({ const defaultTransportCargo = () => ({
cargoName: '', cargoName: '',
cargoType: '', cargoType: '',
@@ -3375,6 +3389,9 @@ export default {
if (!total) return ''; if (!total) return '';
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(3))); return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(3)));
}, },
fixedQuantityUnit() {
return WAYBILL_QUANTITY_UNIT;
},
taskFreightAmount() { taskFreightAmount() {
const quantity = Number(this.taskCargoQuantityTotal || this.form.quantity || 0); const quantity = Number(this.taskCargoQuantityTotal || this.form.quantity || 0);
const unitPrice = Number(this.form.unitPrice || 0); const unitPrice = Number(this.form.unitPrice || 0);
@@ -3383,20 +3400,14 @@ export default {
return Number.isInteger(amount) ? String(amount) : String(Number(amount.toFixed(2))); return Number.isInteger(amount) ? String(amount) : String(Number(amount.toFixed(2)));
}, },
taskFreightGroups() { taskFreightGroups() {
const groups = []; //
const groupMap = new Map(); return [
(this.transportCargoRows || []).forEach((cargo, index) => { {
const quantityUnit = String(cargo.quantityUnit || '').trim(); key: '__all__',
const key = quantityUnit || `__empty_${index}`; quantityUnit: WAYBILL_QUANTITY_UNIT,
let group = groupMap.get(key); rows: this.transportCargoRows || [],
if (!group) { },
group = { key, quantityUnit, rows: [] }; ];
groupMap.set(key, group);
groups.push(group);
}
group.rows.push(cargo);
});
return groups;
}, },
taskFullFreightTotal() { taskFullFreightTotal() {
const total = this.taskFreightGroups.reduce( const total = this.taskFreightGroups.reduce(
@@ -3686,6 +3697,21 @@ export default {
const cityMatch = text.match(/市/); const cityMatch = text.match(/市/);
return cityMatch ? text.slice(0, cityMatch.index + 1) : text; return cityMatch ? text.slice(0, cityMatch.index + 1) : text;
}, },
formatWaybillRouteCityDistrict(value) {
const text = String(value || '').trim();
if (!text) return '-';
const provinceMatch = text.match(/^.+?(?:省|自治区|特别行政区)/);
const cityRegion = provinceMatch ? text.slice(provinceMatch[0].length) : text;
const cityMatch = cityRegion.match(/^(.+?市)/);
if (!cityMatch) return this.formatWaybillProvinceCityDistrict(text);
const city = cityMatch[1];
const remainder = cityRegion.slice(city.length);
const districtMatch = remainder.match(
/^(.+?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗){1,2})/
);
const district = districtMatch ? districtMatch[1] : '';
return [city, district].filter(Boolean).join(' ');
},
waybillDetailRouteName(row = {}, field) { waybillDetailRouteName(row = {}, field) {
const address = String(row[`${field}Address`] || '').trim(); const address = String(row[`${field}Address`] || '').trim();
const name = String(row[`${field}Name`] || '').trim(); const name = String(row[`${field}Name`] || '').trim();
@@ -8625,7 +8651,7 @@ export default {
&__waybill-summary-grid { &__waybill-summary-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(130px, 1fr)) minmax(420px, 2.5fr); grid-template-columns: repeat(4, minmax(120px, 1fr)) minmax(360px, 1.5fr);
gap: 16px 24px; gap: 16px 24px;
} }
@@ -8658,93 +8684,91 @@ export default {
} }
&__waybill-route { &__waybill-route {
display: grid;
grid-template-columns: minmax(100px, 1fr) minmax(36px, 0.7fr) minmax(100px, 1fr);
grid-row: 1 / span 3; grid-row: 1 / span 3;
grid-column: 5; grid-column: 5;
align-items: start;
min-width: 0; min-width: 0;
padding: 4px 0; padding: 4px 0;
} }
&__waybill-route-item { &__waybill-route-item {
position: relative;
display: flex; display: flex;
align-items: stretch; min-width: 0;
gap: 12px; flex-direction: column;
align-items: center;
& + & { text-align: center;
margin-top: 18px;
}
} }
&__waybill-route-dot { &__waybill-route-marker {
position: relative; display: inline-flex;
flex: 0 0 auto; width: 36px;
width: 12px; height: 36px;
margin-top: 2px; align-items: center;
justify-content: center;
&::before { color: #fff;
display: block; border-radius: 8px;
width: 12px;
height: 12px;
border-radius: 50%;
content: '';
background: #409eff; background: #409eff;
box-shadow: 0 0 0 3px #fff, 0 0 0 4px #c6e0ff; font-size: 18px;
} font-weight: 600;
&.is-end::before { .is-end & {
background: #e6a23c; background: #67c23a;
box-shadow: 0 0 0 3px #fff, 0 0 0 4px #f5d9b0;
} }
} }
&__waybill-route-item:not(:last-child) &__waybill-route-dot::after { &__waybill-route-line {
content: ''; width: 100%;
position: absolute; height: 6px;
top: 14px; margin-top: 15px;
left: 5px; border-radius: 1px;
width: 2px; background: #e4e7ed;
height: calc(100% + 10px);
background: repeating-linear-gradient(
to bottom,
#c0c4cc 0,
#c0c4cc 4px,
transparent 4px,
transparent 8px
);
} }
&__waybill-route-detail { &__waybill-route-detail {
flex: 1;
min-width: 0;
display: flex; display: flex;
min-width: 0;
flex-direction: column; flex-direction: column;
gap: 4px; align-items: center;
} gap: 5px;
margin-top: 8px;
&__waybill-route-label { width: 100%;
color: #909399;
font-size: 12px;
} }
&__waybill-route-name { &__waybill-route-name {
color: #303133; max-width: 100%;
font-size: 15px;
font-weight: 500;
line-height: 1.4;
overflow: hidden; overflow: hidden;
display: -webkit-box; color: #303133;
-webkit-line-clamp: 2; font-size: 18px;
-webkit-box-orient: vertical; font-weight: 600;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
} }
&__waybill-route-addr { &__waybill-route-addr {
display: block;
max-width: 100%;
overflow: hidden; overflow: hidden;
color: #606266; color: #606266;
font-size: 12px; font-size: 12px;
line-height: 1.5;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
&__waybill-route-contact {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2px 8px;
max-width: 100%;
color: #909399;
font-size: 12px;
line-height: 1.5;
}
&__waybill-attachments { &__waybill-attachments {
grid-column: 1 / 5; grid-column: 1 / 5;
display: flex; display: flex;
@@ -8763,6 +8787,7 @@ export default {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 18px 32px; gap: 18px 32px;
padding: 4px 20px 8px; padding: 4px 20px 8px;
text-align: left;
} }
&__waybill-detail-field { &__waybill-detail-field {
@@ -8770,6 +8795,7 @@ export default {
align-items: baseline; align-items: baseline;
gap: 12px; gap: 12px;
min-width: 0; min-width: 0;
text-align: left;
span { span {
flex: 0 0 auto; flex: 0 0 auto;
@@ -8790,6 +8816,10 @@ export default {
grid-column: 3 / -1; grid-column: 3 / -1;
} }
&__waybill-detail-field--remark-full {
grid-column: 1 / -1;
}
&__waybill-fee-grid { &__waybill-fee-grid {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -8929,11 +8959,6 @@ export default {
grid-template-columns: repeat(4, minmax(130px, 1fr)); grid-template-columns: repeat(4, minmax(130px, 1fr));
} }
&__waybill-route {
grid-row: auto;
grid-column: 1 / -1;
}
&__waybill-attachments { &__waybill-attachments {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
@@ -8945,6 +8970,21 @@ export default {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
&__waybill-route {
grid-template-columns: minmax(0, 1fr);
gap: 16px;
}
&__waybill-route-line {
width: 10px;
height: 32px;
margin: 0 auto;
}
&__waybill-attachments {
grid-column: auto;
}
&__waybill-carrier-grid { &__waybill-carrier-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -8952,6 +8992,10 @@ export default {
&__waybill-detail-field--remark { &__waybill-detail-field--remark {
grid-column: auto; grid-column: auto;
} }
&__waybill-detail-field--remark-full {
grid-column: auto;
}
} }
@media (max-width: 1280px) { @media (max-width: 1280px) {
+136 -33
View File
@@ -638,17 +638,18 @@
label="运单号" label="运单号"
min-width="150" min-width="150"
show-overflow-tooltip show-overflow-tooltip
/> >
<template #default="{ row }">
<el-link v-if="row.waybillNo" type="primary" @click="openWaybillDetail(row)">
{{ row.waybillNo }}
</el-link>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column <el-table-column
prop="projectName" prop="projectName"
label="项目名称" label="项目名称"
min-width="160" min-width="150"
show-overflow-tooltip
/>
<el-table-column
prop="customerName"
label="客户名称"
min-width="160"
show-overflow-tooltip show-overflow-tooltip
/> />
<el-table-column <el-table-column
@@ -662,48 +663,118 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
prop="cargoName" prop="goodsInfo"
label="货物名称" label="货物信息"
min-width="140" min-width="260"
show-overflow-tooltip show-overflow-tooltip
/> >
<el-table-column <template #default="{ row }">
prop="cargoType" {{ formatCandidateWaybillValue(row, 'goodsInfo') }}
label="货物类型" </template>
min-width="140" </el-table-column>
show-overflow-tooltip
/>
<el-table-column <el-table-column
prop="departureAddress" prop="departureAddress"
label="发货地址" label="发货地址"
min-width="220" min-width="220"
show-overflow-tooltip show-overflow-tooltip
/> >
<template #default="{ row }">
{{ formatCandidateAddress(row, 'departure') }}
</template>
</el-table-column>
<el-table-column <el-table-column
prop="arrivalAddress" prop="arrivalAddress"
label="收货地址" label="收货地址"
min-width="220" min-width="220"
show-overflow-tooltip show-overflow-tooltip
/> >
<template #default="{ row }">
{{ formatCandidateAddress(row, 'arrival') }}
</template>
</el-table-column>
<el-table-column <el-table-column
prop="vehicleNo" prop="contractName"
label="//航班/班列" label="客户合同"
min-width="160" min-width="160"
show-overflow-tooltip show-overflow-tooltip
/> />
<el-table-column <el-table-column
prop="carrierName" prop="planName"
label="承运商名称" label="计划名称"
min-width="160" min-width="150"
show-overflow-tooltip show-overflow-tooltip
/> />
<el-table-column prop="driverName" label="司机" min-width="120" show-overflow-tooltip />
<el-table-column <el-table-column
prop="originalNo" prop="originalNo"
label="原始单号" label="原始单号"
min-width="130" min-width="140"
show-overflow-tooltip show-overflow-tooltip
/> />
<el-table-column
prop="relationNo"
label="关联单号"
min-width="140"
show-overflow-tooltip
/>
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
<el-table-column
prop="startDate"
label="实际发货时间"
min-width="170"
show-overflow-tooltip
>
<template #default="{ row }">
{{ formatCandidateDate(row.startDate) }}
</template>
</el-table-column>
<el-table-column
prop="endDate"
label="实际完成时间"
min-width="170"
show-overflow-tooltip
>
<template #default="{ row }">
{{ formatCandidateDate(row.endDate) }}
</template>
</el-table-column>
<el-table-column
prop="createTime"
label="创建时间"
min-width="170"
show-overflow-tooltip
>
<template #default="{ row }">
{{ formatCandidateDate(row.createTime) }}
</template>
</el-table-column>
<el-table-column
prop="updateTime"
label="更新时间"
min-width="170"
show-overflow-tooltip
>
<template #default="{ row }">
{{ formatCandidateDate(row.updateTime) }}
</template>
</el-table-column>
<el-table-column
prop="businessStatus"
label="状态"
width="110"
fixed="right"
align="center"
>
<template #default="{ row }">
<span
:class="[
'loading-manage-page__status',
`is-${row.businessStatus || 'unknown'}`,
]"
>
{{ formatCandidateWaybillValue(row, 'businessStatus') }}
</span>
</template>
</el-table-column>
</el-table> </el-table>
<div class="loading-manage-dialog__table-toolbar"> <div class="loading-manage-dialog__table-toolbar">
<el-button <el-button
@@ -734,7 +805,10 @@
<el-table-column type="index" label="序号" width="70" align="center" /> <el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="waybillNo" label="运单号" min-width="150"> <el-table-column prop="waybillNo" label="运单号" min-width="150">
<template #default="{ row }"> <template #default="{ row }">
<el-link type="primary">{{ row.waybillNo }}</el-link> <el-link v-if="row.waybillNo" type="primary" @click.stop="openWaybillDetail(row)">
{{ row.waybillNo }}
</el-link>
<span v-else>-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="projectName" label="项目名称" min-width="160" /> <el-table-column prop="projectName" label="项目名称" min-width="160" />
@@ -1009,6 +1083,7 @@ import {
loadingStatusMap, loadingStatusMap,
loadingStatusOptions, loadingStatusOptions,
} from '@/option/business/loading-manage'; } from '@/option/business/loading-manage';
import { option as waybillOption } from '@/option/business/waybill-manage';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { Location, Rank, Refresh, Setting } from '@element-plus/icons-vue'; import { Location, Rank, Refresh, Setting } from '@element-plus/icons-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -1377,6 +1452,27 @@ export default {
statusText(row) { statusText(row) {
return row.businessStatusName || loadingStatusMap[row.businessStatus] || '未知'; return row.businessStatusName || loadingStatusMap[row.businessStatus] || '未知';
}, },
formatCandidateWaybillValue(row, prop) {
if (!row) return '-';
if (prop === 'businessStatus') return this.statusText(row);
if (prop === 'transportType') return this.transportTypeLabel(row.transportType);
const column = waybillOption.column.find(item => item.prop === prop);
if (column?.formatter) {
const value = column.formatter(row, {}, row[prop]);
return value === undefined || value === null || value === '' ? '-' : value;
}
const value = row[prop];
return value === undefined || value === null || value === '' ? '-' : value;
},
formatCandidateDate(value) {
if (!value) return '-';
const date = dayjs(value);
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : value;
},
formatCandidateAddress(row, type) {
const value = row?.[`${type}Address`] || row?.[`${type}Name`] || '';
return this.formatProvinceCityDistrict(value) || '-';
},
splitText(value) { splitText(value) {
if (!value) return []; if (!value) return [];
return String(value) return String(value)
@@ -1924,15 +2020,17 @@ export default {
async loadCandidateWaybills() { async loadCandidateWaybills() {
this.candidateLoading = true; this.candidateLoading = true;
try { try {
const res = await getWaybillList( const query = {
this.candidatePage.currentPage,
this.candidatePage.pageSize,
{
...this.buildQueryParams(this.candidateQuery), ...this.buildQueryParams(this.candidateQuery),
businessStatus: 'pending', businessStatus: 'pending',
onlyUnassignedLoading: 1, onlyUnassignedLoading: 1,
...(this.dialogMode === 'add' ? { transportType: 'road' } : {}), ...(this.dialogMode === 'add' ? { transportType: 'road' } : {}),
} };
const transportType = this.getCandidateTransportTypeQuery(query.transportType);
const res = await getWaybillList(
this.candidatePage.currentPage,
this.candidatePage.pageSize,
{ ...query, transportType }
); );
const page = unwrapPage(res); const page = unwrapPage(res);
this.candidateRows = page.records; this.candidateRows = page.records;
@@ -1941,6 +2039,11 @@ export default {
this.candidateLoading = false; this.candidateLoading = false;
} }
}, },
getCandidateTransportTypeQuery(value) {
const normalized = String(value || '').trim().toLowerCase();
if (['road', '公路运输'].includes(normalized)) return 'road,公路运输';
return value || '';
},
openCandidateSearch() { openCandidateSearch() {
this.candidateSearchVisible = true; this.candidateSearchVisible = true;
this.loadCandidateWaybills(); this.loadCandidateWaybills();
+103 -5
View File
@@ -1,7 +1,7 @@
<template> <template>
<basic-container class="master-order-page"> <basic-container class="master-order-page">
<template v-if="mode === 'list'"> <template v-if="mode === 'list'">
<el-form :model="query" class="master-search" label-width="88px" @submit.prevent> <el-form :model="query" class="master-search" label-width="160px" @submit.prevent>
<el-row :gutter="16"> <el-row :gutter="16">
<el-col v-for="field in primaryFields" :key="field.prop" :span="6" <el-col v-for="field in primaryFields" :key="field.prop" :span="6"
><el-form-item :label="field.label" ><el-form-item :label="field.label"
@@ -330,23 +330,100 @@ export default {
const key = keys.find(item => route[item] !== undefined && route[item] !== null); const key = keys.find(item => route[item] !== undefined && route[item] !== null);
return key ? route[key] : 0; return key ? route[key] : 0;
}, },
isRoadTransportType(type) {
const value = String(type || '').trim().toLowerCase();
return value === 'road' || value.includes('公路');
},
formatRoadAddress(value) {
const text = String(value || '').replace(/\s+/g, '');
if (!text) return '-';
// 公路地址按“市 区县”展示,兼容省市区拼接及历史上只保存市/区县的值。
const withoutProvince = text.replace(/^.*?(?:省|自治区|特别行政区)/, '');
// “梅州市”同时包含“州”和“市”,避免误把城市截断为“梅州”。
const cityMatch = withoutProvince.match(/.+?(?:市|州(?!市)|盟(?!市))/);
if (!cityMatch) return text;
const city = cityMatch[0];
const districtSource = withoutProvince.slice(city.length);
const districtMatch = districtSource.match(/^[^市州盟]*?(?:区|县|旗)/);
if (!districtMatch) return city;
// 兼容“梅县区”这类名称,正则首次命中“县”后还需保留后面的“区”。
const district = `${districtMatch[0]}${
districtSource.slice(districtMatch[0].length).startsWith('区') ? '区' : ''
}`;
return `${city} ${district}`;
},
routeNodeName(value, address, transportType, region = {}) {
const text = String(value || '').trim();
if (!text) return '-';
const siteCode = String(region.siteCode || '').trim();
// 运输方式切换不应改变地址本身的展示模式:有站点编码时显示站点名称,
// 没有站点编码的区域地址统一按“市 区县”格式展示。
if (siteCode && siteCode !== '/') return text;
// 优先从完整的地址名称/详细地址解析,避免后端只返回“梅州”等不完整的市名称时
// 提前返回城市,导致区县被遗漏。
const parsedText = this.formatRoadAddress(text);
if (parsedText.includes(' ')) return parsedText;
const parsedAddress = this.formatRoadAddress(address);
if (parsedAddress.includes(' ')) return parsedAddress;
const city = String(region.cityName || '').trim();
const district = String(region.districtName || '').trim();
if (city && district) return `${city} ${district}`;
if (city) return city;
if (district && address) {
const formattedAddress = this.formatRoadAddress(address);
const addressCity = formattedAddress.split(' ')[0];
if (addressCity && addressCity !== formattedAddress) return `${addressCity} ${district}`;
}
const isRegional =
/省|自治区|特别行政区/.test(text) ||
/(?:市|州|盟).*?(?:区|县|旗)/.test(text) ||
/(?:区|县|旗)$/.test(text);
if (!isRegional) return text;
// 区县简称(如“西乡塘区”)从详细地址中补齐所属城市。
const source = /省|自治区|特别行政区|市|州|盟/.test(text) ? text : address || text;
const formatted = this.formatRoadAddress(source);
if (/(?:区|县|旗)$/.test(text) && !formatted.includes(text)) {
const city = formatted.split(' ')[0];
return city && city !== formatted ? `${city} ${text}` : formatted;
}
return formatted;
},
routeNodes(row = {}) { routeNodes(row = {}) {
const routes = row.routeProgress || []; const routes = row.routeProgress || [];
const total = this.routeNumber(row.totalQuantity); const total = this.routeNumber(row.totalQuantity);
const firstTransportType = routes[0]?.transportType || row.routes?.[0]?.transportType || row.transportType || '';
if (!routes.length) { if (!routes.length) {
return [ return [
{ {
key: 'start', key: 'start',
type: 'start', type: 'start',
text: '起', text: '起',
name: row.departureName || row.departureAddress || '-', name: this.routeNodeName(
row.departureName || row.departureAddress,
row.departureAddress,
firstTransportType,
{
cityName: row.departureCityName,
districtName: row.departureDistrictName,
siteCode: row.departureSiteCode,
}
),
lines: [`已调度0/${total}`], lines: [`已调度0/${total}`],
}, },
{ {
key: 'end', key: 'end',
type: 'end', type: 'end',
text: '终', text: '终',
name: row.arrivalName || row.arrivalAddress || '-', name: this.routeNodeName(
row.arrivalName || row.arrivalAddress,
row.arrivalAddress,
row.finalTransportType || firstTransportType,
{
cityName: row.arrivalCityName,
districtName: row.arrivalDistrictName,
siteCode: row.arrivalSiteCode,
}
),
lines: [`到达0/${total}`], lines: [`到达0/${total}`],
}, },
]; ];
@@ -356,7 +433,16 @@ export default {
key: 'start', key: 'start',
type: 'start', type: 'start',
text: '起', text: '起',
name: row.departureName || row.departureAddress || '-', name: this.routeNodeName(
row.departureName || row.departureAddress,
row.departureAddress,
firstTransportType,
{
cityName: row.departureCityName,
districtName: row.departureDistrictName,
siteCode: row.departureSiteCode,
}
),
lines: [`已调度${this.routeNumber(routes[0]?.dispatchedQuantity)}/${total}`], lines: [`已调度${this.routeNumber(routes[0]?.dispatchedQuantity)}/${total}`],
}, },
...routes.map((route, index) => { ...routes.map((route, index) => {
@@ -367,7 +453,16 @@ export default {
key: route.segmentNo || `route-${index}`, key: route.segmentNo || `route-${index}`,
type: isEnd ? 'end' : 'middle', type: isEnd ? 'end' : 'middle',
text: isEnd ? '终' : '经', text: isEnd ? '终' : '经',
name: route.arrivalName || route.departureName || row.arrivalName || '-', name: this.routeNodeName(
route.arrivalName || route.departureName || row.arrivalName,
route.arrivalAddress || route.departureAddress,
route.transportType,
{
cityName: route.arrivalCityName || route.departureCityName,
districtName: route.arrivalDistrictName || route.departureDistrictName,
siteCode: route.arrivalSiteCode || route.departureSiteCode,
}
),
lines: isEnd lines: isEnd
? [`到达${arrived}/${total}`] ? [`到达${arrived}/${total}`]
: [`到达 ${arrived}/${total}`, `已调度 ${dispatched}/到达${arrived}/${total}`], : [`到达 ${arrived}/${total}`, `已调度 ${dispatched}/到达${arrived}/${total}`],
@@ -412,6 +507,9 @@ export default {
.el-date-editor { .el-date-editor {
width: 100%; width: 100%;
} }
:deep(.el-form-item__label) {
white-space: nowrap;
}
.search-actions { .search-actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;