修复业务模块bug

This commit is contained in:
2026-08-13 20:55:55 +08:00
parent b25b2fae20
commit 5c89140b21
5 changed files with 99 additions and 33 deletions
+1 -1
View File
@@ -432,7 +432,7 @@ export const option = {
{
label: '运费',
prop: 'freight',
formatter: row => formatFreightTotal(row),
formatter: row => formatFreight(row),
minWidth: 120,
addDisplay: false,
editDisplay: false,
@@ -11082,6 +11082,14 @@ export default {
this.$message.warning('请选择至少一条数据');
return;
}
const nonRoadWaybills = this.selectionList.filter(row => {
const transportType = String(row.transportType || '').trim();
return transportType !== '公路运输' && transportType.toLowerCase() !== 'road';
});
if (nonRoadWaybills.length) {
this.$message.warning('公路配置仅支持运输方式为“公路运输”的运单');
return;
}
if (typeof this.api.roadLoading !== 'function') {
this.$message.warning('公路配载功能暂未配置');
return;
@@ -1,7 +1,7 @@
<template>
<div v-loading="loading" class="master-detail">
<section v-if="master" class="detail-overview">
<div class="detail-heading"><div><h2>多联总单详情 <span>|</span> {{ master.masterNo }}</h2><el-tag :type="statusType(master.businessStatus)">{{ statusName(master.businessStatus) }}</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)">{{ 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>
<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>
</section>
@@ -14,7 +14,7 @@
<section v-if="master" class="execution-detail-card">
<div class="execution-detail-heading"><h3>分段执行明细</h3></div>
<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 class="segment-detail__transport-type">{{ segmentTransportType(route) || '-' }}</span><span class="segment-detail__progress-bar"><span :style="{ width: `${segmentProgress(route)}%` }"></span></span><span>{{ route.departureName || '-' }} {{ route.arrivalName || '-' }}</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>{{ 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>
<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>
<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">{{ waybillStatusName(row.businessStatus) }}</el-tag></template></el-table-column><el-table-column prop="createTime" label="创建时间" min-width="180" /></el-table>
@@ -25,7 +25,22 @@
</section>
<section v-if="master" class="master-goods-card">
<div class="master-goods-card__heading"><h3>货物信息</h3></div>
<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="cargoType" label="货物类型" min-width="130" /><el-table-column label="数量" width="130"><template #default="{ row }">{{ quantity(row.quantity) }} {{ row.quantityUnit || '' }}</template></el-table-column><el-table-column prop="packageType" label="包装" min-width="120" /><el-table-column prop="brand" label="品牌" min-width="120" /><el-table-column prop="specification" label="规格" min-width="120" /><el-table-column prop="model" label="型号" min-width="120" /></el-table>
<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="cargoType" label="货物类型" min-width="130" />
<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 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 label="规格型号" min-width="150"><template #default="{ row }">{{ [row.specification, row.model].filter(Boolean).join('/') || '-' }}</template></el-table-column>
<el-table-column prop="remark" label="备注" min-width="160" />
<el-table-column label="货物单价(元)" min-width="130"><template #default="{ row }">{{ row.unitPrice ?? '-' }}</template></el-table-column>
<el-table-column label="计划日期" min-width="130"><template #default="{ row }">{{ row.planDate || row.planStartDate || '-' }}</template></el-table-column>
</el-table>
<el-empty v-if="!(master.goods || []).length" description="暂无货物信息" :image-size="56" />
</section>
</div>
@@ -56,6 +71,7 @@ export default {
});
},
attachments() { const value = this.master?.attachmentsJson; if (Array.isArray(value)) return value; if (!value) return []; try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed : []; } catch (error) { return []; } },
transportFlowLabel() { return this.segments.map(route => this.segmentTransportType(route).slice(0, 1)).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('、') || '-'; },
dateRange() { return this.master?.planStartTime && this.master?.planEndTime ? `${String(this.master.planStartTime).slice(0, 10)} ~ ${String(this.master.planEndTime).slice(0, 10)}` : '-'; },
@@ -112,6 +128,7 @@ export default {
.master-detail { padding-bottom: 20px; color: #303133; }
.detail-overview, .execution-detail-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
.detail-heading { display: flex; align-items: center; justify-content: space-between; padding: 18px 24px; h2 { display: inline-block; margin: 0 16px 0 0; font-size: 20px; } h2 span { margin: 0 8px; color: #909399; font-weight: 400; } }
.transport-flow-tag { margin-left: 8px; }
.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__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; } }
@@ -122,7 +139,7 @@ export default {
.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__progress { color: #606266; font-size: 14px; font-weight: 400; }
.segment-detail__transport-type { color: #409eff; font-size: 14px; font-weight: 500; }
.segment-detail__progress-bar { display: inline-flex; width: 72px; 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__header-right { display: flex; align-items: center; gap: 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; }
@@ -56,12 +56,16 @@
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="发货地址" required><el-input :model-value="addressText(route.departureName, route.departureAddress)" readonly /></el-form-item>
</el-col>
<el-col :span="4"><el-form-item label="联系人"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="4"><el-form-item label="联系方式"><el-input v-model="route.departurePhone" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="4"><el-form-item :label="dateStartLabel(route)" required><el-date-picker v-model="route.estimatedStartTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择" /></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="收货地址" required><el-input :model-value="addressText(route.arrivalName, route.arrivalAddress)" readonly /></el-form-item>
</el-col>
@@ -121,13 +125,23 @@
<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">
<el-col v-if="route.carrierType === '承运商'" :span="6"><el-form-item label="承运商" required><el-select v-model="route.carrierName" filterable remote clearable placeholder="请选择" :remote-method="loadCarrierOptions" :loading="carrierLoading" @visible-change="visible => visible && loadCarrierOptions()"><el-option v-for="item in carrierOptions" :key="carrierOptionKey(item)" :label="carrierOptionLabel(item)" :value="carrierOptionLabel(item)" /></el-select></el-form-item></el-col>
<el-col :span="6"><el-form-item label="司机" :required="route.carrierType !== '承运商'"><el-select :ref="element => setDriverInput(route.segmentNo, element)" v-model="route.driverName" filterable remote clearable placeholder="请选择" :remote-method="loadDriverOptions" :loading="driverLoading" @visible-change="visible => visible && loadDriverOptions()" @change="value => handleDriverChange(route, value)"><el-option v-for="item in driverOptions" :key="driverOptionKey(item)" :label="driverOptionLabel(item)" :value="driverOptionLabel(item)" /></el-select></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="挂车车牌号" required><el-input v-model="route.trailerVehicleNo" placeholder="请输入" /></el-form-item></el-col>
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人" required><el-input v-model="route.escortName" placeholder="请输入" /></el-form-item></el-col>
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人手机号" required><el-input v-model="route.escortPhone" placeholder="请输入" /></el-form-item></el-col>
<template v-if="isRoad(route)">
<el-col v-if="route.carrierType === '承运商'" :span="6"><el-form-item label="承运商" required><el-select v-model="route.carrierName" filterable remote clearable placeholder="请选择" :remote-method="loadCarrierOptions" :loading="carrierLoading" @visible-change="visible => visible && loadCarrierOptions()"><el-option v-for="item in carrierOptions" :key="carrierOptionKey(item)" :label="carrierOptionLabel(item)" :value="carrierOptionLabel(item)" /></el-select></el-form-item></el-col>
<el-col :span="6"><el-form-item label="司机" :required="route.carrierType !== '承运商'"><el-select :ref="element => setDriverInput(route.segmentNo, element)" v-model="route.driverName" filterable remote clearable placeholder="请选择" :remote-method="loadDriverOptions" :loading="driverLoading" @visible-change="visible => visible && loadDriverOptions()" @change="value => handleDriverChange(route, value)"><el-option v-for="item in driverOptions" :key="driverOptionKey(item)" :label="driverOptionLabel(item)" :value="driverOptionLabel(item)" /></el-select></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="挂车车牌号" required><el-input v-model="route.trailerVehicleNo" placeholder="请输入" /></el-form-item></el-col>
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人" required><el-input v-model="route.escortName" placeholder="请输入" /></el-form-item></el-col>
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人手机号" required><el-input v-model="route.escortPhone" placeholder="请输入" /></el-form-item></el-col>
</template>
<template v-else>
<el-col v-if="route.carrierType === '承运商'" :span="6"><el-form-item label="承运商" required><el-select v-model="route.carrierName" filterable remote clearable placeholder="请选择" :remote-method="loadCarrierOptions" :loading="carrierLoading" @visible-change="visible => visible && loadCarrierOptions()"><el-option v-for="item in carrierOptions" :key="carrierOptionKey(item)" :label="carrierOptionLabel(item)" :value="carrierOptionLabel(item)" /></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="船长" required><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="箱号" required><el-input v-model="route.containerNo" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="舱位" required><el-input v-model="route.cabinNo" placeholder="请输入" /></el-form-item></el-col>
</template>
<el-col :span="6"><el-form-item label="里程(km)" required><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>
@@ -284,7 +298,7 @@ export default {
else Object.assign(row, { sourceIndex: undefined, cargoName: '', quantity: '', quantityUnit: '' });
this.syncFreightItems(route);
},
isRoad(route) { return String(route.transportType || '').includes('公路'); },
isRoad(route) { const type = String(route.transportType || '').toLowerCase(); return type.includes('公路') || type === 'road'; },
dispatchedQuantity(route) {
return this.quantityNumber(route.dispatchedQuantity) + this.pendingSegmentQuantity(route);
},
@@ -344,9 +358,13 @@ export default {
},
dispatchedGoodsQuantity(route, row) {
const dispatchedGoods = route.dispatchedGoods || {};
return this.quantityNumber(
dispatchedGoods[this.goodsKey(row)] ?? dispatchedGoods[this.goodsKey(row, false)]
);
const exactValue = dispatchedGoods[this.goodsKey(row)] ?? dispatchedGoods[this.goodsKey(row, false)];
if (exactValue !== undefined && exactValue !== null) return this.quantityNumber(exactValue);
const matchedValue = Object.entries(dispatchedGoods).find(([key]) => {
const [cargoName, cargoType] = String(key).split('\u0000');
return cargoName === row.cargoName && cargoType === row.cargoType;
})?.[1];
return this.quantityNumber(matchedValue);
},
baseGoodsRemainingQuantity(route, row) {
return Math.max(0, this.quantityNumber(row.quantity) - this.dispatchedGoodsQuantity(route, row));
@@ -493,7 +511,6 @@ export default {
route.trailerVehicleNo = '';
route.escortName = '';
route.escortPhone = '';
route.mileage = undefined;
} else {
route.carrierName = '';
}
@@ -532,6 +549,9 @@ export default {
route.driverName = '';
route.driverPhone = '';
route.vehicleNo = '';
route.captainName = '';
route.containerNo = '';
route.cabinNo = '';
route.trailerVehicleNo = '';
route.escortName = '';
route.escortPhone = '';
@@ -548,15 +568,19 @@ export default {
if (!route.estimatedEndTime) return this.$message.warning(`请选择${this.dateEndLabel(route)}`);
if (!this.validateRoutePhones(route)) return;
if (route.documentType === '运单') {
if (route.carrierType === '承运商' && (!route.carrierName || !route.vehicleNo || !route.mileage)) return this.$message.warning('请填写承运商、车牌号和里程');
if (route.carrierType !== '承运商' && (!route.driverName || !route.driverPhone || !route.vehicleNo || !route.trailerVehicleNo || !route.escortName || !route.escortPhone || route.mileage === undefined || route.mileage === null || route.mileage === '')) return this.$message.warning('请补全自运或网货平台的车辆与人员信息');
if (this.isRoad(route)) {
if (route.carrierType === '承运商' && (!route.carrierName || !route.vehicleNo || !route.mileage)) return this.$message.warning('请填写承运商、车牌号和里程');
if (route.carrierType !== '承运商' && (!route.driverName || !route.driverPhone || !route.vehicleNo || !route.trailerVehicleNo || !route.escortName || !route.escortPhone || route.mileage === undefined || route.mileage === null || route.mileage === '')) return this.$message.warning('请补全自运或网货平台的车辆与人员信息');
} else if (!route.vehicleNo || !route.captainName || !route.driverPhone || !route.containerNo || !route.cabinNo || route.mileage === undefined || route.mileage === null || route.mileage === '' || (route.carrierType === '承运商' && !route.carrierName)) {
return this.$message.warning('请补全非公路运输的承运信息和里程');
}
}
const batchNo = `${route.segmentNo}-${Date.now()}`;
goods.forEach(item => this.pending.push({
id: `${route.segmentNo}-${item.cargoName}-${Date.now()}-${Math.random()}`,
batchNo,
segmentNo: route.segmentNo, relationNo: route.segmentNo, documentType: route.documentType, transportType: route.transportType, carrierType: route.carrierType,
carrierName: route.carrierName, driverName: route.driverName, driverPhone: route.driverPhone, vehicleNo: route.vehicleNo, 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 || 'CNY', freightAmount: this.freightAmount(this.freightItemsForGoods(route, item)), freightTotal: this.freightTotal(route), otherFeeTotal: route.otherFeeTotal || '', freightJson: this.buildFreightJson(route), remark: route.remark,
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 || 'CNY', freightAmount: this.freightAmount(this.freightItemsForGoods(route, item)), freightTotal: this.freightTotal(route), otherFeeTotal: route.otherFeeTotal || '', freightJson: this.buildFreightJson(route), remark: route.remark,
departureName: route.departureName, departureAddress: route.departureAddress, departureContact: route.departureContact, departurePhone: route.departurePhone,
arrivalName: route.arrivalName, arrivalAddress: route.arrivalAddress, arrivalContact: route.arrivalContact, arrivalPhone: route.arrivalPhone,
estimatedStartTime: route.estimatedStartTime, estimatedEndTime: route.estimatedEndTime,
+30 -13
View File
@@ -181,7 +181,7 @@
accept=".zip,.7z"
:auto-upload="false"
:limit="1"
:on-change="uploadVoucher"
:on-change="selectVoucherFile"
:on-remove="clearFile"
><el-button
type="primary"
@@ -425,7 +425,8 @@ const loading = ref(false),
closingProgressDialog = ref(false),
cancelledTaskIds = ref(new Set()),
uploadCancelled = ref(false),
activeUploadTaskId = ref();
activeUploadTaskId = ref(),
selectedVoucherFile = ref();
const query = reactive({
voucherBatchNo: '',
auditStatus: '',
@@ -499,6 +500,7 @@ const changeProject = projectId => {
};
const openUpload = async row => {
await loadProjects();
selectedVoucherFile.value = undefined;
Object.assign(editing, {
id: row?.id || '',
voucherBatchNo: row?.voucherBatchNo || '',
@@ -520,7 +522,10 @@ const openUpload = async row => {
editing.waybillImportBatchIds = selectedBatches.value.map(item => item.id);
}
};
const clearFile = () => Object.assign(editing, { fileName: '', fileUrl: '', fileTaskId: '' });
const clearFile = () => {
selectedVoucherFile.value = undefined;
Object.assign(editing, { fileName: '', fileUrl: '', fileTaskId: '' });
};
const isCurrentUploadTask = taskId =>
taskId && activeUploadTaskId.value && String(taskId) === String(activeUploadTaskId.value);
const stopCurrentUpload = async taskId => {
@@ -543,11 +548,11 @@ const stopCurrentUpload = async taskId => {
ElMessage.error(error.message || '暂停上传失败');
}
};
const closeUploadDialog = async () => {
const closeUploadDialog = async (pauseUpload = true) => {
if (closingUploadDialog.value) return;
closingUploadDialog.value = true;
try {
await stopCurrentUpload(editing.fileTaskId);
if (pauseUpload) await stopCurrentUpload(editing.fileTaskId);
uploadVisible.value = false;
} finally {
closingUploadDialog.value = false;
@@ -633,6 +638,7 @@ const uploadParts = async (file, task, sessionId) => {
editing.fileName = file.name;
editing.fileTaskId = task.id;
editing.fileUrl = fileUrlRes.data?.data || '';
if (!editing.fileUrl) throw new Error('未获取到上传文件地址,请稍后重试');
await api.completeUploadFile({
voucherBatchNo: task.businessId,
fileTaskId: task.id,
@@ -726,14 +732,23 @@ const uploadVoucher = async file => {
cancelledTaskIds.value.delete(String(task.id));
resumeTarget.value = undefined;
await uploadParts(raw, task, sessionId);
return Boolean(editing.fileUrl);
} catch (error) {
if (error.name !== 'AbortError')
ElMessage.error(error.message || '上传中断,可在上传进度中继续上传');
return false;
}
};
const selectVoucherFile = file => {
if (!file?.raw) return;
selectedVoucherFile.value = file.raw;
editing.fileName = file.raw.name;
editing.fileUrl = '';
editing.fileTaskId = '';
};
watch(uploadVisible, visible => {
// 兜底处理:右上角、遮罩、Esc 直接修改 v-model 时仍必须停止上传。
if (!visible) void stopCurrentUpload(editing.fileTaskId);
if (!visible && !closingUploadDialog.value) void stopCurrentUpload(editing.fileTaskId);
});
const openBatchDialog = async () => {
batchVisible.value = true;
@@ -773,21 +788,23 @@ const removeBatch = row => {
const submitUpload = async () => {
saving.value = true;
try {
if (uploading.value && editing.fileTaskId) {
await stopCurrentUpload(editing.fileTaskId);
}
const valid = await uploadFormRef.value.validate().catch(() => false);
if (!valid) return;
if (!editing.fileUrl && !editing.fileTaskId) {
ElMessage.warning('请先选择执行凭证');
return;
if (!editing.fileUrl) {
if (!selectedVoucherFile.value) {
ElMessage.warning('请先选择执行凭证');
return;
}
const uploaded = await uploadVoucher({ raw: selectedVoucherFile.value });
if (!uploaded) return;
selectedVoucherFile.value = undefined;
}
await api.submit({
...editing,
waybillImportBatchIds: selectedBatches.value.map(item => item.id),
});
ElMessage.success('提交成功');
await closeUploadDialog();
await closeUploadDialog(false);
load();
} finally {
saving.value = false;