Files
tms-erp-web/src/views/business/components/master-order-dispatch.vue
T
b2894lxlx 6554ad80dd 1、新增应付明细
2、新增应收明细
3、新增异常处置
4、新增风险处置
5、新增在途追踪
6、修复业务模块bug
2026-08-13 00:01:23 +08:00

447 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div v-loading="loading" class="master-dispatch">
<section v-if="master" class="master-overview">
<div class="overview-heading">
<div>
<h2>多联总单调度 <span>|</span> {{ master.masterNo }}</h2>
<el-tag :type="statusType(master.businessStatus)">{{ statusName(master.businessStatus) }}</el-tag>
</div>
</div>
<div class="overview-content">
<dl class="overview-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><dt>货物名称</dt><dd>{{ goodsNames }}</dd></div>
<div><dt>货物类型</dt><dd>{{ goodsTypes }}</dd></div>
<div><dt>运输周期</dt><dd>{{ dateRange }}</dd></div>
</dl>
<div class="route-overview">
<template v-for="(route, index) in routes" :key="route.segmentNo">
<div class="route-overview__node">
<span :class="['route-badge', index === 0 ? 'start' : 'middle']">{{ index === 0 ? '起' : '经' }}</span>
<strong>{{ route.departureName || '-' }}</strong>
<small>{{ route.departureAddress || '-' }}</small>
<small>{{ contactText(route.departureContact, route.departurePhone) }}</small>
</div>
<div class="route-overview__line"></div>
<div v-if="index === routes.length - 1" class="route-overview__node">
<span class="route-badge end"></span>
<strong>{{ route.arrivalName || '-' }}</strong>
<small>{{ route.arrivalAddress || '-' }}</small>
<small>{{ contactText(route.arrivalContact, route.arrivalPhone) }}</small>
</div>
</template>
</div>
</div>
</section>
<section v-for="(route, index) in routes" :key="route.segmentNo" class="dispatch-section" :data-segment="route.segmentNo">
<header class="segment-header">
<div>
<el-checkbox v-model="route.selected" :label="`${route.segmentNo}${route.departureName} → ${route.arrivalName}`" />
</div>
<div>总量 {{ formatQuantity(totalQuantity) }}已调度 <em>{{ formatQuantity(route.dispatchedQuantity) }}</em>剩余 <em>{{ formatQuantity(remaining(route)) }}</em></div>
</header>
<div v-show="route.selected" class="segment-content">
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form">
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="运输类型" required class="transport-type-field"><el-input :model-value="route.transportType" readonly /></el-form-item>
</el-col>
<el-col v-if="isRoad(route)" :span="8">
<el-form-item label="单据类型" required>
<el-radio-group v-model="route.documentType">
<el-radio value="计划单">计划单</el-radio><el-radio value="运单">运单</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8">
<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-col :span="8">
<el-form-item label="收货地址" required><el-input :model-value="addressText(route.arrivalName, route.arrivalAddress)" readonly /></el-form-item>
</el-col>
<el-col :span="4"><el-form-item label="联系人"><el-input v-model="route.arrivalContact" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="4"><el-form-item label="联系方式"><el-input v-model="route.arrivalPhone" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="4"><el-form-item :label="dateEndLabel(route)" required><el-date-picker v-model="route.estimatedEndTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择" /></el-form-item></el-col>
</el-row>
</el-form>
<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-column type="index" label="序号" width="64" />
<el-table-column label="货物名称" min-width="180"><template #default="{ row }"><el-select v-model="row.sourceIndex" placeholder="请选择总单货物" @change="selectGoods(route, row)"><el-option v-for="item in masterGoods" :key="item.sourceIndex" :label="item.label" :value="item.sourceIndex" /></el-select></template></el-table-column>
<el-table-column label="货物类型" min-width="130"><template #default="{ row }"><el-input :model-value="row.cargoType" readonly /></template></el-table-column>
<el-table-column label="剩余数量" width="120"><template #default="{ row }">{{ formatQuantity(goodsRemainingQuantity(route, row)) }}</template></el-table-column>
<el-table-column label="本次数量" width="160"><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="packageType" 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="model" label="型号" min-width="110" />
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
<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>
<div class="freight-heading"><h3>运费信息</h3></div>
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form freight-form">
<el-row :gutter="16">
<el-col :span="6"><el-form-item label="单价"><el-input :model-value="route.unitPrice" inputmode="decimal" placeholder="请输入" @input="value => handleUnitPriceInput(route, value)" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="计价单位"><el-select v-model="route.priceUnit"><el-option label="元/吨" value="元/吨" /><el-option label="元/件" value="元/件" /></el-select></el-form-item></el-col>
<el-col :span="6"><el-form-item label="重量合计"><el-input :model-value="formatQuantity(dispatchWeight(route))" readonly><template #append></template></el-input></el-form-item></el-col>
<el-col :span="6"><el-form-item label="运费"><el-input :model-value="formatAmount(freightAmount(route))" readonly><template #append></template></el-input></el-form-item></el-col>
</el-row>
</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">
<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>
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="里程(km)" required><el-input :model-value="route.mileage" inputmode="decimal" 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>
</section>
<div class="dispatch-bottom">
<section class="pending-panel">
<div class="pending-bar">
<div class="pending-summary">
<strong>待提交 <em>{{ pending.length }}</em> <span class="pending-summary-text">{{ pendingSegmentSummary }}</span></strong>
<el-link type="primary" @click="pendingExpanded = !pendingExpanded">{{ pendingExpanded ? '收起待调度列表' : '展开待调度列表' }}</el-link>
</div>
<div class="pending-actions"><el-button @click="$emit('back')">返回</el-button><el-button type="primary" :loading="submitting" @click="submit">确认调度</el-button></div>
</div>
<div v-show="pendingExpanded" class="pending-list">
<template v-for="(items, segmentNo) in pendingGroups" :key="segmentNo">
<h3>{{ segmentNo }}{{ items[0].departureName }} {{ items[0].arrivalName }}</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>
</template>
<el-empty v-if="!pending.length" description="暂无待提交调度条目" :image-size="60" />
</div>
</section>
</div>
</div>
</template>
<script>
import * as api from '@/api/business/master-order';
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
const unwrapRecords = res => {
const data = res?.data || res;
if (Array.isArray(data)) return data;
if (Array.isArray(data?.records)) return data.records;
if (Array.isArray(data?.data)) return data.data;
if (Array.isArray(data?.data?.records)) return data.data.records;
return [];
};
export default {
props: { id: [String, Number] },
emits: ['back'],
data() {
return { loading: false, submitting: false, master: null, routes: [], pending: [], pendingExpanded: false, editingId: null, driverInputs: {}, carrierOptions: [], driverOptions: [], carrierLoading: false, driverLoading: false };
},
computed: {
totalQuantity() { return (this.master?.goods || []).reduce((sum, item) => sum + Number(item.quantity || 0), 0); },
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('、') || '-'; },
masterGoods() { return (this.master?.goods || []).map((item, sourceIndex) => ({ ...item, sourceIndex, label: [item.cargoName, 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)}` : '-'; },
pendingGroups() { return this.pending.reduce((groups, item) => { (groups[item.segmentNo] ||= []).push(item); return groups; }, {}); },
pendingSegmentSummary() {
return this.routes.map(route => {
const items = this.pending.filter(item => item.segmentNo === route.segmentNo);
if (!items.length) return `${route.segmentNo}:无`;
const documentSummary = items.reduce((result, item) => {
result[item.documentType] = (result[item.documentType] || 0) + 1;
return result;
}, {});
return `${route.segmentNo}${Object.entries(documentSummary).map(([type, count]) => `${type} x ${count}`).join('、')}`;
}).join(' | ');
},
},
async mounted() {
await Promise.all([this.load(), this.loadCarrierOptions(), this.loadDriverOptions()]);
},
methods: {
async load() {
this.loading = true;
try {
const res = await api.getDetail(this.id);
this.master = res.data?.data || res.data || res;
this.routes = (this.master.routes || []).map((node, index) => this.createRoute(node, index));
} finally { this.loading = false; }
},
createRoute(node, index) {
const previous = index ? this.master.routes[index - 1] : this.master;
return {
...node,
selected: false,
documentType: '运单',
carrierType: '承运商',
unitPrice: undefined,
priceUnit: '元/吨',
departureName: previous.departureName || '', departureAddress: previous.departureAddress || '', departureContact: previous.departureContact || '', departurePhone: previous.departurePhone || '',
arrivalName: node.departureName || this.master.arrivalName || '', arrivalAddress: node.departureAddress || this.master.arrivalAddress || '', arrivalContact: node.departureContact || '', arrivalPhone: node.departurePhone || '',
goods: [],
};
},
isRoad(route) { return String(route.transportType || '').includes('公路'); },
remaining(route) {
return Math.max(
0,
this.totalQuantity -
Number(route.dispatchedQuantity || 0) -
this.pendingSegmentQuantity(route)
);
},
formatQuantity(value) { const number = Number(value || 0); return Number.isInteger(number) ? number : number.toFixed(3).replace(/\.?0+$/, ''); },
formatAmount(value) { return Number(value || 0).toFixed(2); },
dispatchWeight(route) { return (route.goods || []).reduce((sum, item) => sum + Number(item.dispatchQuantity || 0), 0); },
freightAmount(route) { return Number(route.unitPrice || 0) * this.dispatchWeight(route); },
quantityNumber(value) { return Number(value || 0); },
goodsKey(goods = {}, includeUnit = true) {
return [
goods.cargoName || '',
goods.cargoType || '',
includeUnit ? goods.quantityUnit || '' : '',
].join('\u0000');
},
isSameGoods(left = {}, right = {}) {
if (left.sourceIndex !== undefined && right.sourceIndex !== undefined) return Number(left.sourceIndex) === Number(right.sourceIndex);
return left.cargoName === right.cargoName && left.cargoType === right.cargoType && left.quantityUnit === right.quantityUnit;
},
dispatchedGoodsQuantity(route, row) {
const dispatchedGoods = route.dispatchedGoods || {};
return this.quantityNumber(
dispatchedGoods[this.goodsKey(row)] ?? dispatchedGoods[this.goodsKey(row, false)]
);
},
baseGoodsRemainingQuantity(route, row) {
return Math.max(0, this.quantityNumber(row.quantity) - this.dispatchedGoodsQuantity(route, row));
},
pendingSegmentQuantity(route) {
return this.pending.reduce((sum, item) => {
if (item.segmentNo !== route.segmentNo) return sum;
return sum + this.quantityNumber(item.quantity);
}, 0);
},
pendingGoodsQuantity(route, row) {
return this.pending.reduce((sum, item) => {
if (item.segmentNo !== route.segmentNo || !this.isSameGoods(item, row)) return sum;
return sum + this.quantityNumber(item.quantity);
}, 0);
},
editingGoodsQuantity(route, row) {
return (route.goods || []).reduce((sum, item) => {
if (item === row || !this.isSameGoods(item, row)) return sum;
return sum + this.quantityNumber(item.dispatchQuantity);
}, 0);
},
goodsRemainingQuantity(route, row) {
return Math.max(
0,
this.baseGoodsRemainingQuantity(route, row) -
this.pendingGoodsQuantity(route, row) -
this.editingGoodsQuantity(route, row)
);
},
availableDispatchQuantity(route, row) { return this.goodsRemainingQuantity(route, row); },
handleDispatchQuantityInput(route, row, value) {
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
const nextValue = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 3)}` : integer;
const availableQuantity = this.availableDispatchQuantity(route, row);
if (Number(nextValue || 0) > availableQuantity) {
row.dispatchQuantity = this.formatQuantity(availableQuantity);
this.$message.warning('本次数量不能超过剩余数量');
return;
}
row.dispatchQuantity = nextValue;
},
handleUnitPriceInput(route, value) {
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
route.unitPrice = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
},
handleMileageInput(route, value) {
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
route.mileage = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
},
addressText(name, address) { return [name, address].filter(Boolean).join(' / ') || '-'; },
contactText(name, phone) { return [name, phone].filter(Boolean).join(' - ') || '-'; },
dateStartLabel(route) { return route.documentType === '计划单' ? '计划开始日期' : '预计发货日期'; },
dateEndLabel(route) { return route.documentType === '计划单' ? '计划结束日期' : '预计完成日期'; },
createGoodsRow(goods = {}, sourceIndex) {
return {
...goods,
sourceIndex,
dispatchQuantity: undefined,
};
},
addGoods(route) { route.goods.push(this.createGoodsRow()); },
selectGoods(route, row) {
const source = (this.master.goods || [])[Number(row.sourceIndex)];
if (!source) return;
Object.assign(row, this.createGoodsRow(source, Number(row.sourceIndex)));
},
removeGoods(route, index) { route.goods.splice(index, 1); },
handleCarrierTypeChange(route, value) {
route.carrierType = value;
if (value === '承运商') {
route.trailerVehicleNo = '';
route.escortName = '';
route.escortPhone = '';
route.mileage = undefined;
} else {
route.carrierName = '';
}
},
carrierOptionLabel(item) { return item.customerName || item.carrierName || item.fullName || item.name || ''; },
carrierOptionKey(item) { return item.id || item.customerId || item.carrierId || this.carrierOptionLabel(item); },
driverOptionLabel(item) { return item.driverName || item.name || ''; },
driverOptionKey(item) { return item.id || item.driverId || this.driverOptionLabel(item) || item.mobile; },
handleDriverChange(route, value) {
const driver = this.driverOptions.find(item => this.driverOptionLabel(item) === value);
if (driver) route.driverPhone = driver.mobile || driver.driverPhone || driver.phone || '';
},
async loadCarrierOptions(keyword = '') {
this.carrierLoading = true;
try {
this.carrierOptions = unwrapRecords(
await getCustomerList(1, 20, { customerName: keyword, customerType: '承运商' })
);
} finally {
this.carrierLoading = false;
}
},
async loadDriverOptions(keyword = '') {
this.driverLoading = true;
try {
this.driverOptions = unwrapRecords(await getDriverList(1, 20, { driverName: keyword }));
} finally {
this.driverLoading = false;
}
},
setDriverInput(segmentNo, element) {
if (element) this.driverInputs[segmentNo] = element;
else delete this.driverInputs[segmentNo];
},
resetForNextDispatch(route) {
route.driverName = '';
route.driverPhone = '';
route.vehicleNo = '';
route.trailerVehicleNo = '';
route.escortName = '';
route.escortPhone = '';
route.goods.forEach(item => { item.dispatchQuantity = undefined; });
this.$nextTick(() => this.driverInputs[route.segmentNo]?.focus());
},
addToPending(route, continueDispatch) {
const goods = route.goods.filter(item => Number(item.dispatchQuantity) > 0);
if (!goods.length) return this.$message.warning('请填写本次数量');
if (goods.some(item => Number(item.dispatchQuantity) > this.availableDispatchQuantity(route, item))) return this.$message.warning('本次数量不能超过剩余数量');
if (!route.estimatedStartTime) return this.$message.warning(`请选择${this.dateStartLabel(route)}`);
if (!route.estimatedEndTime) return this.$message.warning(`请选择${this.dateEndLabel(route)}`);
if (route.documentType === '运单') {
if (route.carrierType === '承运商' && (!route.carrierName || !route.vehicleNo)) 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('请补全自运或网货平台的车辆与人员信息');
}
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: route.mileage, unitPrice: route.unitPrice, priceUnit: route.priceUnit, 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,
sourceIndex: item.sourceIndex, 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,
}));
this.editingId = null;
this.pendingExpanded = true;
if (continueDispatch) this.resetForNextDispatch(route);
else {
route.goods.forEach(item => { item.dispatchQuantity = undefined; });
route.selected = false;
}
this.$message.success('已加入调度清单');
},
editPending(item) {
const route = this.routes.find(routeItem => routeItem.segmentNo === item.segmentNo);
if (!route) return;
const sourceIndex = (this.master.goods || []).findIndex(goods => goods.cargoName === item.cargoName && goods.cargoType === item.cargoType);
let goods = route.goods.find(goodsItem => goodsItem.cargoName === item.cargoName && goodsItem.cargoType === item.cargoType);
if (!goods && sourceIndex >= 0) {
goods = this.createGoodsRow(this.master.goods[sourceIndex], sourceIndex);
route.goods.push(goods);
}
if (goods) goods.dispatchQuantity = item.quantity;
Object.assign(route, item);
this.editingId = item.id;
this.removePending(item.id);
this.$nextTick(() => document.querySelector(`[data-segment="${item.segmentNo}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
},
removePending(id) { this.pending = this.pending.filter(item => item.id !== id); if (this.editingId === id) this.editingId = null; },
async submit() {
if (!this.pending.length) return this.$message.warning('请加入待提交调度清单');
this.submitting = true;
try { await api.dispatch({ id: this.master.id, dispatches: this.pending.map(({ id, ...item }) => item) }); this.$message.success('调度成功'); this.$emit('back'); } finally { this.submitting = false; }
},
statusName(value) { return ({ waiting_dispatch: '待调度', dispatching: '调度中', completed: '调度完成', closed: '调度关闭' })[value] || value || '-'; },
statusType(value) { return ({ waiting_dispatch: 'warning', dispatching: 'success', completed: 'primary', closed: 'danger' })[value] || 'info'; },
},
};
</script>
<style scoped lang="scss">
.master-dispatch { min-height: 100%; padding-bottom: 124px; background: #fff; color: #303133; }
.master-overview, .dispatch-section { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
.overview-heading, .segment-header, .pending-bar { display: flex; align-items: center; justify-content: space-between; }
.overview-heading { padding: 18px 24px 12px; h2 { display: inline-block; margin: 0 16px 0 0; font-size: 20px; } h2 span { margin: 0 8px; color: #909399; font-weight: 400; } }
.overview-content { display: grid; grid-template-columns: minmax(400px, .9fr) minmax(620px, 1.6fr); gap: 28px; padding: 8px 24px 20px; }
.overview-meta { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 24px; margin: 0; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { margin: 0; color: #409eff; font-size: 14px; word-break: break-all; } }
.route-overview { display: flex; align-items: flex-start; justify-content: flex-end; padding-top: 8px; overflow-x: auto; }
.route-overview__node { display: grid; flex: 0 0 130px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } }
.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-overview__line { flex: 1 0 56px; min-width: 56px; height: 32px; margin-top: 0; border-bottom: 2px solid #606266; }
.segment-header { min-height: 46px; padding: 0 20px; border-bottom: 1px solid #eff1f7; font-weight: 600; em { color: #409eff; font-style: normal; } }
.segment-content { padding: 16px 24px; }
.dispatch-form { :deep(.el-form-item) { margin-bottom: 16px; } :deep(.el-input), :deep(.el-select), :deep(.el-date-editor), :deep(.el-input-number) { width: 100%; } }
.transport-type-field :deep(.el-input) { width: 240px; }
.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; } }
.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; } }
.carrier-type-form { margin-top: 16px; }
.carrier-form { padding-top: 8px; }
.segment-actions { display: flex; justify-content: flex-end; gap: 12px; padding-top: 4px; }
.dispatch-bottom { position: fixed; z-index: 10; bottom: 0; left: $sidebar_width; width: calc(100% - #{$sidebar_width}); border-top: 1px solid #eff1f7; background: #fff; box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04); }
:global(.avue--collapse) .dispatch-bottom { left: $sidebar_collapse; width: calc(100% - #{$sidebar_collapse}); }
:global(.avue-layout--horizontal) .dispatch-bottom { left: 0; width: 100%; }
.pending-panel { background: #fff; }
.pending-bar { min-height: 58px; padding: 0 32px; em { color: #409eff; font-style: normal; } }
.pending-summary, .pending-actions { display: flex; align-items: center; }
.pending-summary { gap: 16px; }
.pending-summary-text { color: #606266; font-weight: 400; }
.pending-actions { gap: 12px; margin-left: auto; }
.pending-list { max-height: min(420px, calc(100vh - 130px)); padding: 0 32px 16px; overflow: auto; h3 { margin: 14px 0 10px; font-size: 14px; } }
.table-actions { display: flex; flex-wrap: wrap; gap: 8px; }
@media screen and (max-width: 1200px) { .overview-content { grid-template-columns: 1fr; } .route-overview { justify-content: flex-start; } }
@media screen and (max-width: 992px) { .dispatch-bottom { left: 0; width: 100%; } }
</style>