完善配载确认、承运商组织过滤司机与地址展示

配载草稿确认生成正式单并优化详情/列表地址市区展示;运单调度等按承运商联系人所属组织筛选司机车辆;统一地图搜索结果与登录默认账号清理。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-23 18:35:27 +08:00
parent 694fe1a433
commit a919781d4f
19 changed files with 1701 additions and 309 deletions
+3 -7
View File
@@ -289,8 +289,8 @@
</el-button>
</div>
<div class="map-picker-content">
<map-search-results :results="routeMapSearchResults" @select="selectRouteMapSearchResult" />
<div ref="routeAmap" class="common-route-page__map"></div>
<map-search-results :results="routeMapSearchResults" @select="selectRouteMapSearchResult" />
</div>
<div class="common-route-page__map-info">
<span>{{ routeMapStatus }}</span>
@@ -323,6 +323,7 @@ import { addressTypeOptions } from '@/option/base/common-address';
import { config, excelOption, option } from '@/option/business/common-route';
import { getToken } from '@/utils/auth';
import { openImportDialog } from '@/utils/import-excel';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate';
import { List, Search } from '@element-plus/icons-vue';
@@ -616,12 +617,7 @@ export default {
})
.then(() => this.runAmapGeocode('location', keyword))
.then(result => {
this.routeMapSearchResults = (result.geocodes || []).map((item, index) => ({
id: item.id || index,
name: item.formattedAddress || keyword,
address: item.formattedAddress || keyword,
location: item.location,
}));
this.routeMapSearchResults = normalizeMapSearchResults(result, keyword);
const point = this.resolveMapPoint(result);
if (!point) {
this.routeMapStatus = '未找到匹配地址';
@@ -4007,7 +4007,7 @@
</el-input>
<div class="business-crud-page__dispatch-quantity-hint">
剩余数量{{
formatDispatchQuantity(dispatchItemRemainingQuantity(dispatchItemForm))
formatDispatchQuantity(dispatchItemHintRemainingQuantity(dispatchItemForm))
}}
{{ dispatchItemForm.quantityUnit || '吨' }}
</div>
@@ -4064,7 +4064,11 @@
</el-input>
</el-form-item>
<el-form-item label="其他费用合计">
<el-input v-model="dispatchItemForm.otherFeeTotal" placeholder="请输入">
<el-input
v-model="dispatchItemForm.otherFeeTotal"
placeholder="请输入"
@input="value => handleDispatchNumberInput('otherFeeTotal', value)"
>
<template #suffix>{{ dispatchItemFreightCurrencyLabel }}</template>
</el-input>
</el-form-item>
@@ -4996,8 +5000,8 @@
</el-button>
</div>
<div class="map-picker-content">
<map-search-results :results="transportMapSearchResults" @select="selectTransportMapSearchResult" />
<div ref="transportAmap" class="business-crud-page__map"></div>
<map-search-results :results="transportMapSearchResults" @select="selectTransportMapSearchResult" />
</div>
<div class="business-crud-page__map-info">
<span>{{ transportMapStatus }}</span>
@@ -5271,7 +5275,7 @@ import {
getVoucherImages as getProcessConfigVoucherImages,
} from '@/api/business/process-config';
import { getList as getCarrierCustomerList } from '@/api/vehicle/customer-archive';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
import { getDictionary } from '@/api/system/dictbiz';
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { addressTypeOptions } from '@/option/base/common-address';
@@ -5279,6 +5283,7 @@ import { packageOptions } from '@/option/business/common';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { openImportDialog } from '@/utils/import-excel';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate';
@@ -6340,19 +6345,21 @@ export default {
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 otherFeeRaw = this.normalizeDispatchFeeValue(this.dispatchItemForm.otherFeeTotal);
const otherFeeTotal = Number(otherFeeRaw || 0);
const hasFreight = this.dispatchItemForm.unitPrice !== '' && quantity > 0;
const hasOtherFee = this.dispatchItemForm.otherFeeTotal !== '';
const hasOtherFee = otherFeeRaw !== '';
if (!hasFreight && !hasOtherFee) return '';
const total = (hasFreight ? quantity * unitPrice : 0) + (hasOtherFee ? otherFeeTotal : 0);
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)));
}
const otherFeeTotal = Number(this.dispatchItemForm.otherFeeTotal || 0);
const otherFeeRaw = this.normalizeDispatchFeeValue(this.dispatchItemForm.otherFeeTotal);
const otherFeeTotal = Number(otherFeeRaw || 0);
const freightTotal = this.dispatchItemCargoRows.reduce(
(total, cargo) => total + Number(this.dispatchCargoFreightAmount(cargo) || 0),
0
);
const hasOtherFee = this.dispatchItemForm.otherFeeTotal !== '';
const hasOtherFee = otherFeeRaw !== '';
if (!freightTotal && !hasOtherFee) return '';
const total = freightTotal + (hasOtherFee ? otherFeeTotal : 0);
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)));
@@ -8919,6 +8926,7 @@ export default {
this.form.carrierContractId = '';
}
this.form.carrierType = nextValue;
this.clearTaskDriverVehicleFields();
if (this.isWaybillDetailLayout) {
this.taskCarrierRequestId += 1;
this.taskCarrierOptions = [];
@@ -9036,25 +9044,58 @@ export default {
},
handleTaskCarrierChange(value) {
const carrier = this.taskCarrierOptions.find(item =>
[item.value, item.customerName, item.carrierName, item.fullName, item.name].some(name =>
String(name || '') === String(value || '')
)
[
item.value,
item.customerName,
item.carrierName,
item.fullName,
item.name,
item.carrierContractId,
].some(name => String(name || '') === String(value || ''))
);
if (this.isWaybillDetailLayout && this.form.carrierType === '承运商') {
this.form.carrierContractId = carrier?.carrierContractId || '';
this.form.carrierId = carrier?.carrierId || '';
this.form.carrierName = carrier?.carrierName || value || '';
return;
} else {
this.form.carrierId = carrier?.id || carrier?.carrierId || '';
this.form.carrierName = value || '';
}
this.form.carrierId = carrier?.id || '';
this.form.carrierName = value || '';
this.clearTaskDriverVehicleFields();
},
clearTaskDriverVehicleFields() {
this.form.driverId = '';
this.form.driverName = '';
this.form.driverPhone = '';
this.form.vehicleNo = '';
this.form.trailerVehicleNo = '';
this.form.escortName = '';
this.form.escortPhone = '';
this.taskDriverOptions = [];
},
getActiveCarrierFilter() {
if (this.dispatchItemBox) {
return {
carrierId: this.dispatchItemForm.carrierId || '',
carrierName: this.dispatchItemForm.carrierName || '',
};
}
return {
carrierId: this.form.carrierId || '',
carrierName: this.form.carrierName || '',
};
},
loadTaskDriverOptions() {
if (!this.taskInfoFormEnabled || this.taskDriverLoading) return Promise.resolve([]);
const carrier = this.getActiveCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
this.taskDriverOptions = [];
return Promise.resolve([]);
}
this.taskDriverLoading = true;
return getDriverList(1, 9999, {})
.then(res => {
this.taskDriverOptions = extractRecords(res);
return fetchDriversByCarrierOrganizations({ size: 9999, posts: '司机' }, carrier)
.then(records => {
this.taskDriverOptions = records;
return this.taskDriverOptions;
})
.finally(() => {
@@ -9063,10 +9104,17 @@ export default {
},
fetchTaskDriverSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim();
const carrier = this.getActiveCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.taskDriverLoading = true;
getDriverList(1, 20, keyword ? { driverName: keyword } : {})
.then(res => {
const records = extractRecords(res);
fetchDriversByCarrierOrganizations(
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
carrier
)
.then(records => {
this.taskDriverOptions = records;
callback(
records.map(item => ({
@@ -10239,12 +10287,7 @@ export default {
this.ensureTransportAmapGeocoder()
.then(() => this.runAmapGeocode('location', keyword))
.then(result => {
this.transportMapSearchResults = (result.geocodes || []).map((item, index) => ({
id: item.id || index,
name: item.formattedAddress || keyword,
address: item.formattedAddress || keyword,
location: item.location,
}));
this.transportMapSearchResults = normalizeMapSearchResults(result, keyword);
const point = this.resolveMapPoint(result);
if (!point) {
this.transportMapStatus = '未找到匹配地址';
@@ -12760,6 +12803,9 @@ export default {
...baseRow,
...(index >= 0 ? row : {}),
};
this.dispatchItemForm.otherFeeTotal = this.normalizeDispatchFeeValue(
this.dispatchItemForm.otherFeeTotal
);
this.dispatchItemForm.carrierType = this.dispatchItemForm.carrierType || '承运商';
const goodsRows = this.parseJsonArray(this.dispatchItemForm.goodsJson);
this.dispatchItemCargoRows = (goodsRows.length ? goodsRows : [this.dispatchItemForm]).map(
@@ -12816,6 +12862,7 @@ export default {
}
}
this.dispatchItemForm.carrierType = nextValue;
this.clearDispatchDriverVehicleFields();
this.loadDispatchCarrierOptions(this.dispatchRow);
},
isDispatchCarrierRequired(carrierType) {
@@ -12945,20 +12992,39 @@ export default {
});
if (this.dispatchIsCarrierMode) {
this.dispatchItemForm.carrierContractId = carrier?.carrierContractId || '';
this.dispatchItemForm.carrierId = '';
this.dispatchItemForm.carrierId = carrier?.carrierId || '';
this.dispatchItemForm.carrierName = carrier?.carrierName || '';
return;
} else {
this.dispatchItemForm.carrierContractId = '';
this.dispatchItemForm.carrierId = carrier?.id || carrier?.carrierId || '';
this.dispatchItemForm.carrierName = carrier?.carrierName || value || '';
}
this.dispatchItemForm.carrierContractId = '';
this.dispatchItemForm.carrierId = carrier?.id || '';
this.dispatchItemForm.carrierName = carrier?.carrierName || value || '';
this.clearDispatchDriverVehicleFields();
},
clearDispatchDriverVehicleFields() {
this.dispatchItemForm.driverId = '';
this.dispatchItemForm.driverName = '';
this.dispatchItemForm.driverPhone = '';
this.dispatchItemForm.vehicleNo = '';
this.dispatchItemForm.trailerVehicleNo = '';
this.dispatchItemForm.escortName = '';
this.dispatchItemForm.escortPhone = '';
this.taskDriverOptions = [];
},
loadDispatchDriverOptions() {
if (this.taskDriverLoading) return Promise.resolve([]);
const carrier = {
carrierId: this.dispatchItemForm.carrierId || '',
carrierName: this.dispatchItemForm.carrierName || '',
};
if (!carrier.carrierId && !carrier.carrierName) {
this.taskDriverOptions = [];
return Promise.resolve([]);
}
this.taskDriverLoading = true;
return getDriverList(1, 9999, {})
.then(res => {
this.taskDriverOptions = extractRecords(res);
return fetchDriversByCarrierOrganizations({ size: 9999, posts: '司机' }, carrier)
.then(records => {
this.taskDriverOptions = records;
return this.taskDriverOptions;
})
.finally(() => {
@@ -13035,6 +13101,11 @@ export default {
this.dispatchItemForm[prop] =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
},
normalizeDispatchFeeValue(value) {
if (value === undefined || value === null || value === '') return '';
if (Number(value) === -1) return '';
return value;
},
addDispatchCargoRow(index = -1) {
const nextRow = this.normalizeTransportCargoRow({ priceUnit: '元/吨' });
if (index > -1) this.dispatchItemCargoRows.splice(index + 1, 0, nextRow);
@@ -13118,6 +13189,12 @@ export default {
}, 0)
);
},
/** 精简录入提示:可调度余量扣减当前本次数量后的剩余 */
dispatchItemHintRemainingQuantity(row = {}) {
const available = this.dispatchItemRemainingQuantity(row);
const current = this.parseDispatchQuantity(row.quantity);
return Math.max(available - current, 0);
},
pruneDispatchPendingRows(rows = []) {
const planQuantities = new Map();
this.dispatchPlanGoodsRows.forEach(goods => {
@@ -13364,14 +13441,29 @@ export default {
}),
];
const firstGoods = goodsRows[0] || {};
const quantitySum = goodsRows.reduce(
(sum, goods) => sum + this.parseDispatchQuantity(goods.quantity),
0
);
const quantityText = quantitySum
? this.formatDispatchQuantity(quantitySum)
: firstGoods.quantity || this.dispatchItemForm.quantity || '';
const otherFeeTotal = this.normalizeDispatchFeeValue(this.dispatchItemForm.otherFeeTotal);
const nextRow = {
...this.dispatchItemForm,
goodsJson: JSON.stringify(goodsRows),
cargoType: firstGoods.cargoType || this.dispatchItemForm.cargoType || '',
cargoTypeCode: firstGoods.cargoTypeCode || this.dispatchItemForm.cargoTypeCode || '',
cargoTypePath: firstGoods.cargoTypePath || this.dispatchItemForm.cargoTypePath || [],
cargoName: firstGoods.cargoName || this.dispatchItemForm.cargoName || '',
quantity: firstGoods.quantity || this.dispatchItemForm.quantity || '',
cargoName:
goodsRows
.map(goods => goods.cargoName || goods.goodsName || '')
.filter(Boolean)
.join('.') ||
firstGoods.cargoName ||
this.dispatchItemForm.cargoName ||
'',
quantity: quantityText,
quantityUnit: firstGoods.quantityUnit || this.dispatchItemForm.quantityUnit || '',
unitPrice: firstGoods.unitPrice || '',
priceUnit: firstGoods.priceUnit || '',
@@ -13379,7 +13471,7 @@ export default {
freightJson: JSON.stringify({
currency: this.dispatchItemForm.freightCurrency || 'CNY',
totalFreightAmount: this.dispatchItemFreightSubtotal,
otherFreightAmount: this.dispatchItemForm.otherFeeTotal || '',
otherFreightAmount: otherFeeTotal,
freightItems: goodsRows.map((cargo, index) => ({
cargoIndex: index,
cargoName: cargo.cargoName || '',
@@ -13395,7 +13487,7 @@ export default {
};
nextRow.cargoInfo = this.formatDispatchCargoInfo({}, nextRow);
nextRow.freight = this.getDispatchFeeFields({}, nextRow).freight;
nextRow.otherFeeTotal = this.dispatchItemForm.otherFeeTotal || '';
nextRow.otherFeeTotal = otherFeeTotal;
const quantityUnit = this.getDispatchQuantityUnit(nextRow);
const otherDispatchedQuantity = this.dispatchRows.reduce((total, row, index) => {
if (
@@ -156,11 +156,11 @@
<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="司机" :required="route.carrierType !== '承运商'"><el-autocomplete :ref="element => setDriverInput(route.segmentNo, element)" v-model="route.driverName" :debounce="300" :fetch-suggestions="(query, cb) => fetchDriverSuggestions(route, query, cb)" 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-autocomplete v-model="route.escortName" :debounce="300" :fetch-suggestions="(query, cb) => fetchEscortSuggestions(route, query, cb)" 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>
@@ -233,7 +233,7 @@
<script>
import * as api from '@/api/business/master-order';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
import { isMobile } from '@/utils/validate';
const unwrapRecords = res => {
@@ -290,7 +290,7 @@ export default {
},
},
async mounted() {
await Promise.all([this.load(), this.loadCarrierOptions(), this.loadDriverOptions()]);
await Promise.all([this.load(), this.loadCarrierOptions()]);
this.applySelfOperatedCarrierDefaults();
},
methods: {
@@ -727,6 +727,7 @@ export default {
route.carrierName = '';
route.carrierId = '';
route.carrierContractId = '';
this.clearRouteDriverVehicleFields(route);
if (value === '承运商') {
route.trailerVehicleNo = '';
route.escortName = '';
@@ -746,6 +747,24 @@ export default {
route.carrierContractId = route.carrierType !== '自运' ? contract?.contractId || '' : '';
route.carrierId = route.carrierType !== '自运' ? contract?.carrierId || '' : '';
route.carrierName = contract?.carrierName || '';
this.clearRouteDriverVehicleFields(route);
},
clearRouteDriverVehicleFields(route) {
if (!route) return;
route.driverId = '';
route.driverName = '';
route.driverPhone = '';
route.vehicleNo = '';
route.trailerVehicleNo = '';
route.escortName = '';
route.escortPhone = '';
this.driverOptions = [];
},
getRouteCarrierFilter(route = {}) {
return {
carrierId: route.carrierId || '',
carrierName: route.carrierName || '',
};
},
carrierOptionValue(item = {}) {
return item.contractId || item.id || item.value || item.carrierName || '';
@@ -817,18 +836,30 @@ export default {
route.carrierContractId = '';
});
},
async loadDriverOptions(keyword = '') {
async loadDriverOptions(route = {}, keyword = '') {
const carrier = this.getRouteCarrierFilter(route);
if (!carrier.carrierId && !carrier.carrierName) {
this.driverOptions = [];
return [];
}
this.driverLoading = true;
try {
this.driverOptions = unwrapRecords(
await getDriverList(1, 20, { driverName: keyword, posts: '司机' })
this.driverOptions = await fetchDriversByCarrierOrganizations(
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
carrier
);
return this.driverOptions;
} finally {
this.driverLoading = false;
}
},
fetchDriverSuggestions(queryString, callback) {
this.loadDriverOptions(String(queryString || '').trim())
fetchDriverSuggestions(route, queryString, callback) {
const carrier = this.getRouteCarrierFilter(route);
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.loadDriverOptions(route, String(queryString || '').trim())
.then(() =>
callback(
this.driverOptions.map(item => ({
@@ -846,13 +877,21 @@ export default {
const drivingVehicle = String(item.drivingVehicle || '').trim();
if (drivingVehicle) route.vehicleNo = drivingVehicle;
},
fetchEscortSuggestions(queryString, callback) {
fetchEscortSuggestions(route, queryString, callback) {
const keyword = String(queryString || '').trim();
const carrier = this.getRouteCarrierFilter(route);
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.escortLoading = true;
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '押运员' })
.then(res => {
fetchDriversByCarrierOrganizations(
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
carrier
)
.then(records => {
callback(
unwrapRecords(res).map(item => ({
records.map(item => ({
...item,
value: this.driverOptionLabel(item),
}))
@@ -402,8 +402,8 @@
>
</div>
<div class="map-picker-content">
<map-search-results :results="mapSearchResults" @select="selectAddressMapSearchResult" />
<div ref="addressMap" class="address-map" />
<map-search-results :results="mapSearchResults" @select="selectAddressMapSearchResult" />
</div>
<div class="address-map-status">
{{ mapStatus
@@ -601,6 +601,7 @@ let amapLoader;
import { getList as getCommonRouteList } from '@/api/business/common-route';
import * as api from '@/api/business/master-order';
import { formatUpdateUserName } from '@/utils/audit';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { isMobile } from '@/utils/validate';
export default {
components: {
@@ -1285,14 +1286,8 @@ export default {
)
)
.then(result => {
const geocodes = result?.geocodes || [];
this.mapSearchResults = geocodes.map((item, index) => ({
id: item.id || index,
name: item.formattedAddress || this.mapKeyword,
address: item.formattedAddress || this.mapKeyword,
location: item.location,
}));
const point = geocodes[0]?.location;
this.mapSearchResults = normalizeMapSearchResults(result, this.mapKeyword);
const point = this.mapSearchResults[0]?.location;
if (!point) return this.$message.warning('地图搜索无匹配地址');
this.pickAddressMap(point, this.mapKeyword);
})
@@ -2199,8 +2194,15 @@ export default {
display: flex;
gap: 8px;
margin-bottom: 12px;
.el-input {
flex: 1;
min-width: 0;
}
}
.address-map {
width: 100%;
min-width: 0;
height: 440px;
}
.address-map-status {
@@ -931,11 +931,11 @@
/><el-button type="primary" @click="searchTransportMapKeyword">搜索</el-button>
</div>
<div class="map-picker-content">
<div ref="transportMap" class="shipping-template-page__map" />
<map-search-results
:results="transportMapSearchResults"
@select="selectTransportMapSearchResult"
/>
<div ref="transportMap" class="shipping-template-page__map" />
</div>
<div class="shipping-template-page__map-info">{{ transportMapStatus }}</div>
<template #footer
@@ -1098,6 +1098,7 @@ import {
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { getDictionary } from '@/api/system/dictbiz';
import { packageOptions } from '@/option/business/common';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate';
import { Location } from '@element-plus/icons-vue';
@@ -2615,12 +2616,7 @@ export default {
.then(() => this.ensureTransportMapGeocoder())
.then(() => this.runTransportMapGeocode('location', keyword))
.then(result => {
this.transportMapSearchResults = (result.geocodes || []).map((item, index) => ({
id: item.id || index,
name: item.formattedAddress || keyword,
address: item.formattedAddress || keyword,
location: item.location,
}));
this.transportMapSearchResults = normalizeMapSearchResults(result, keyword);
const point = this.resolveTransportMapPoint(result);
if (!point) throw new Error('未找到匹配地址');
return this.$nextTick().then(() => {
@@ -3754,6 +3750,12 @@ export default {
.shipping-template-page__map-toolbar {
display: flex;
gap: 8px;
margin-bottom: 12px;
.el-input {
flex: 1;
min-width: 0;
}
}
.shipping-template-page__dialog-search {
margin-bottom: 8px;
@@ -3766,11 +3768,9 @@ export default {
justify-content: flex-end;
}
.shipping-template-page__map {
flex: 1 1 auto;
width: 100%;
min-width: 0;
height: 480px;
margin-top: 12px;
}
:deep(.shipping-template-page__map > .amap-container) {
width: 100% !important;
@@ -637,13 +637,14 @@
<span>|</span>
{{ detailRow.planNo || '-' }}
</h2>
<span class="detail-status-text is-dispatching">
<span class="detail-status-text" :class="detailStatusTextClass">
{{ displayStatus(detailRow, 'businessStatus') }}
</span>
<span class="detail-status-text detail-transport-type">
{{
detailRow.transportTypeName ||
formatTransportPlanDetailTransportType(detailRow.transportType)
formatTransportPlanDetailTransportType(detailRow.transportType) ||
'-'
}}
</span>
</div>
@@ -1231,7 +1232,7 @@
{{ dispatchDialogStatusText }}
</el-tag>
<el-tag type="primary" effect="light">
{{ formatDispatchTransportType(dispatchRow.transportType) || '公路运输' }}
{{ formatDispatchTransportType(dispatchRow.transportType) || '-' }}
</el-tag>
</div>
</div>
@@ -1931,7 +1932,7 @@
class="transport-plan-page__dispatch-quantity-hint"
>
剩余数量{{
formatDispatchQuantity(dispatchItemRemainingQuantity(dispatchItemForm))
formatDispatchQuantity(dispatchItemHintRemainingQuantity(dispatchItemForm))
}}
{{ dispatchItemForm.quantityUnit || '吨' }}
</div>
@@ -1997,7 +1998,11 @@
</el-input>
</el-form-item>
<el-form-item label="其他费用合计">
<el-input v-model="dispatchItemForm.otherFeeTotal" placeholder="请输入">
<el-input
v-model="dispatchItemForm.otherFeeTotal"
placeholder="请输入"
@input="value => handleDispatchNumberInput('otherFeeTotal', value)"
>
<template #suffix>{{ dispatchItemFreightCurrencyLabel }}</template>
</el-input>
</el-form-item>
@@ -2547,8 +2552,8 @@
</el-button>
</div>
<div class="map-picker-content">
<map-search-results :results="transportMapSearchResults" @select="selectTransportMapSearchResult" />
<div ref="transportAmap" class="transport-plan-page__map"></div>
<map-search-results :results="transportMapSearchResults" @select="selectTransportMapSearchResult" />
</div>
<div class="transport-plan-page__map-info">
<span>{{ transportMapStatus }}</span>
@@ -2729,13 +2734,14 @@ import {
getList as getProjectList,
} from '@/api/business/project-apply';
import { getList as getCommonRouteList } from '@/api/business/common-route';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
import { getDictionary } from '@/api/system/dictbiz';
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { addressTypeOptions } from '@/option/base/common-address';
import { packageOptions } from '@/option/business/common';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate';
@@ -3284,6 +3290,23 @@ export default {
isTransportPlanDetailLayout() {
return this.detailBox;
},
detailStatusTextClass() {
const status = this.detailRow?.businessStatus;
if (status === 2 || status === '2' || status === 'completed') {
return 'status-text-success';
}
if (status === 1 || status === '1' || status === 'dispatching') {
return 'is-dispatching';
}
if (
status === 3 ||
status === '3' ||
['rejected', 'change_rejected', 'cancelled', 'withdrawn'].includes(status)
) {
return 'is-danger';
}
return 'is-info';
},
transportPlanGoodsText() {
const text = this.formatTransportPlanGoodsInfo(this.detailRow);
return text && text !== '-' ? text : '暂无货物信息';
@@ -3526,17 +3549,19 @@ export default {
},
dispatchItemFreightTotal() {
if (this.dispatchItemForm.taskEntryMode !== 'full') {
const otherFeeTotal = Number(this.dispatchItemForm.otherFeeTotal || 0);
const otherFeeRaw = this.normalizeDispatchFeeValue(this.dispatchItemForm.otherFeeTotal);
const otherFeeTotal = Number(otherFeeRaw || 0);
const freight = this.dispatchItemFreightSubtotal;
const hasFreight = freight !== '';
const hasOtherFee = this.dispatchItemForm.otherFeeTotal !== '';
const hasOtherFee = otherFeeRaw !== '';
if (!hasFreight && !hasOtherFee) return '';
const total = (hasFreight ? Number(freight) : 0) + (hasOtherFee ? otherFeeTotal : 0);
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)));
}
const otherFeeTotal = Number(this.dispatchItemForm.otherFeeTotal || 0);
const otherFeeRaw = this.normalizeDispatchFeeValue(this.dispatchItemForm.otherFeeTotal);
const otherFeeTotal = Number(otherFeeRaw || 0);
const freightSubtotal = this.dispatchItemFreightSubtotal;
const hasOtherFee = this.dispatchItemForm.otherFeeTotal !== '';
const hasOtherFee = otherFeeRaw !== '';
if (freightSubtotal === '' && !hasOtherFee) return '';
const total = Number(freightSubtotal || 0) + (hasOtherFee ? otherFeeTotal : 0);
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)));
@@ -4841,10 +4866,17 @@ export default {
},
fetchTaskDriverSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim();
const carrier = this.getDispatchCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.taskDriverLoading = true;
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
.then(res => {
const records = extractRecords(res);
fetchDriversByCarrierOrganizations(
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
carrier
)
.then(records => {
this.taskDriverOptions = records;
callback(
records.map(item => ({
@@ -4863,10 +4895,17 @@ export default {
},
fetchTaskEscortSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim();
const carrier = this.getDispatchCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.taskEscortLoading = true;
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '押运员' })
.then(res => {
const records = extractRecords(res);
fetchDriversByCarrierOrganizations(
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
carrier
)
.then(records => {
this.taskEscortOptions = records;
callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' })));
})
@@ -5738,12 +5777,7 @@ export default {
this.ensureTransportAmapGeocoder()
.then(() => this.runAmapGeocode('location', keyword))
.then(result => {
this.transportMapSearchResults = (result.geocodes || []).map((item, index) => ({
id: item.id || index,
name: item.formattedAddress || keyword,
address: item.formattedAddress || keyword,
location: item.location,
}));
this.transportMapSearchResults = normalizeMapSearchResults(result, keyword);
const point = this.resolveMapPoint(result);
if (!point) {
this.transportMapStatus = '未找到匹配地址';
@@ -7241,11 +7275,13 @@ export default {
freightInfo.freightItems?.[0]?.unitPrice ??
'';
const otherFeeTotal =
item.otherFeeTotal ??
plan.otherFeeTotal ??
freightInfo.otherFeeTotal ??
freightInfo.otherFreightAmount ??
'';
this.normalizeDispatchFeeValue(
item.otherFeeTotal ??
plan.otherFeeTotal ??
freightInfo.otherFeeTotal ??
freightInfo.otherFreightAmount ??
''
);
const quantity = Number(
item.quantity ||
item.cargoQuantity ||
@@ -7560,6 +7596,9 @@ export default {
...baseRow,
...(index >= 0 ? row : {}),
};
this.dispatchItemForm.otherFeeTotal = this.normalizeDispatchFeeValue(
this.dispatchItemForm.otherFeeTotal
);
this.dispatchItemForm.carrierType = this.dispatchItemForm.carrierType || '承运商';
this.loadDispatchCarrierOptions(this.dispatchRow).then(() => {
this.applyDispatchSelfCarrierDefaults();
@@ -7611,6 +7650,7 @@ export default {
this.dispatchItemForm.carrierName = '';
this.dispatchItemForm.carrierId = '';
this.dispatchItemForm.carrierContractId = '';
this.clearDispatchDriverVehicleFields();
if (nextValue === '承运商') {
this.dispatchItemForm.trailerVehicleNo = '';
this.dispatchItemForm.escortName = '';
@@ -7798,12 +7838,29 @@ export default {
this.dispatchItemForm.carrierContractId = carrier?.carrierContractId || '';
this.dispatchItemForm.carrierId = carrier?.carrierId || '';
this.dispatchItemForm.carrierName = carrier?.carrierName || '';
return;
} else {
this.dispatchItemForm.carrierContractId = '';
this.dispatchItemForm.carrierId = '';
this.dispatchItemForm.carrierName = carrier?.carrierName || value || '';
}
this.dispatchItemForm.carrierContractId = '';
this.dispatchItemForm.carrierId =
this.dispatchItemForm.carrierType === '自运' ? '' : carrier?.id || '';
this.dispatchItemForm.carrierName = carrier?.carrierName || value || '';
this.clearDispatchDriverVehicleFields();
},
clearDispatchDriverVehicleFields() {
this.dispatchItemForm.driverId = '';
this.dispatchItemForm.driverName = '';
this.dispatchItemForm.driverPhone = '';
this.dispatchItemForm.vehicleNo = '';
this.dispatchItemForm.trailerVehicleNo = '';
this.dispatchItemForm.escortName = '';
this.dispatchItemForm.escortPhone = '';
this.taskDriverOptions = [];
this.taskEscortOptions = [];
},
getDispatchCarrierFilter() {
return {
carrierId: this.dispatchItemForm.carrierId || '',
carrierName: this.dispatchItemForm.carrierName || '',
};
},
handleDispatchCargoQuantityInput(row, value) {
const text = String(value || '').replace(/[^\d.]/g, '');
@@ -7820,6 +7877,11 @@ export default {
this.dispatchItemForm[prop] =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
},
normalizeDispatchFeeValue(value) {
if (value === undefined || value === null || value === '') return '';
if (Number(value) === -1) return '';
return value;
},
addDispatchCargoRow(index = -1) {
const nextRow = this.normalizeTransportCargoRow({ priceUnit: '元/吨' });
if (index > -1) this.dispatchItemCargoRows.splice(index + 1, 0, nextRow);
@@ -7972,6 +8034,12 @@ export default {
}, 0)
);
},
/** 精简录入提示:可调度余量扣减当前本次数量后的剩余 */
dispatchItemHintRemainingQuantity(row = {}) {
const available = this.dispatchItemRemainingQuantity(row);
const current = this.parseDispatchQuantity(row.quantity);
return Math.max(available - current, 0);
},
pruneDispatchPendingRows(rows = []) {
const planQuantities = new Map();
this.dispatchPlanGoodsRows.forEach(goods => {
@@ -8220,14 +8288,29 @@ export default {
}),
];
const firstGoods = goodsRows[0] || {};
const quantitySum = goodsRows.reduce(
(sum, goods) => sum + this.parseDispatchQuantity(goods.quantity),
0
);
const quantityText = quantitySum
? this.formatDispatchQuantity(quantitySum)
: firstGoods.quantity || this.dispatchItemForm.quantity || '';
const otherFeeTotal = this.normalizeDispatchFeeValue(this.dispatchItemForm.otherFeeTotal);
const nextRow = {
...this.dispatchItemForm,
goodsJson: JSON.stringify(goodsRows),
cargoType: firstGoods.cargoType || this.dispatchItemForm.cargoType || '',
cargoTypeCode: firstGoods.cargoTypeCode || this.dispatchItemForm.cargoTypeCode || '',
cargoTypePath: firstGoods.cargoTypePath || this.dispatchItemForm.cargoTypePath || [],
cargoName: firstGoods.cargoName || this.dispatchItemForm.cargoName || '',
quantity: firstGoods.quantity || this.dispatchItemForm.quantity || '',
cargoName:
goodsRows
.map(goods => goods.cargoName || goods.goodsName || '')
.filter(Boolean)
.join('.') ||
firstGoods.cargoName ||
this.dispatchItemForm.cargoName ||
'',
quantity: quantityText,
quantityUnit: firstGoods.quantityUnit || this.dispatchItemForm.quantityUnit || '',
unitPrice: firstGoods.unitPrice || '',
priceUnit: firstGoods.priceUnit || '',
@@ -8235,7 +8318,7 @@ export default {
freightJson: JSON.stringify({
currency: this.dispatchContractCurrency || this.dispatchItemForm.freightCurrency || 'RMB',
totalFreightAmount: this.dispatchItemFreightSubtotal,
otherFreightAmount: this.dispatchItemForm.otherFeeTotal || '',
otherFreightAmount: otherFeeTotal,
freightItems: goodsRows.map((cargo, index) => ({
cargoIndex: index,
cargoName: cargo.cargoName || '',
@@ -8251,7 +8334,7 @@ export default {
};
nextRow.cargoInfo = this.formatDispatchCargoInfo({}, nextRow);
nextRow.freight = this.dispatchItemFreightSubtotal;
nextRow.otherFeeTotal = this.dispatchItemForm.otherFeeTotal || '';
nextRow.otherFeeTotal = otherFeeTotal;
const quantityUnit = this.getDispatchQuantityUnit(nextRow);
const otherDispatchedQuantity = this.dispatchRows.reduce((total, row, index) => {
if (
@@ -9348,19 +9431,36 @@ export default {
}
}
:deep(.el-tag) {
height: 36px;
padding: 0 14px;
border: 0;
.detail-status-text {
display: inline-flex;
align-items: center;
height: 28px;
padding: 0 12px;
border-radius: 8px;
font-size: 14px;
line-height: 36px;
font-size: 13px;
line-height: 28px;
}
:deep(.is-dispatching) {
.status-text-success,
.is-dispatching {
color: #67c23a;
background: #e1f3d8;
}
.detail-transport-type {
color: #409eff;
background: #ecf5ff;
}
.is-danger {
color: #f56c6c;
background: #fef0f0;
}
.is-info {
color: #909399;
background: #f4f4f5;
}
}
&__transport-plan-detail-overview {
@@ -1939,12 +1939,17 @@
}}</strong
>
</div>
<div
v-if="!waybillIsCarrier(detailRow)"
class="waybill-manage-page__waybill-detail-field"
>
<div class="waybill-manage-page__waybill-detail-field">
<span>里程(km)</span
><strong>{{ normalizeMileageValue(detailRow.mileage) || '-' }}</strong>
><strong>{{ waybillDetailMileage(detailRow) || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>计划发货日期</span
><strong>{{ waybillDetailEstimatedStartTime(detailRow) || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>计划完成日期</span
><strong>{{ waybillDetailEstimatedEndTime(detailRow) || '-' }}</strong>
</div>
</template>
<template v-else-if="waybillIsNonRoadTransport(detailRow)">
@@ -1966,12 +1971,32 @@
<div class="waybill-manage-page__waybill-detail-field">
<span>仓位</span><strong>{{ detailRow.cabinNo || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>预计发货日期</span
><strong>{{ waybillDetailEstimatedStartTime(detailRow) || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>预计完成日期</span
><strong>{{ waybillDetailEstimatedEndTime(detailRow) || '-' }}</strong>
</div>
</template>
<template v-else>
<div class="waybill-manage-page__waybill-detail-field">
<span>{{ waybillVehicleNoLabel(detailRow) }}</span>
<strong>{{ detailRow.vehicleNo || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>里程(km)</span
><strong>{{ waybillDetailMileage(detailRow) || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>计划发货日期</span
><strong>{{ waybillDetailEstimatedStartTime(detailRow) || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>计划完成日期</span
><strong>{{ waybillDetailEstimatedEndTime(detailRow) || '-' }}</strong>
</div>
</template>
<div
class="waybill-manage-page__waybill-detail-field"
@@ -2851,8 +2876,8 @@
</el-button>
</div>
<div class="map-picker-content">
<map-search-results :results="transportMapSearchResults" @select="selectTransportMapSearchResult" />
<div ref="transportAmap" class="waybill-manage-page__map"></div>
<map-search-results :results="transportMapSearchResults" @select="selectTransportMapSearchResult" />
</div>
<div class="waybill-manage-page__map-info">
<span>{{ transportMapStatus }}</span>
@@ -3112,6 +3137,9 @@ import {
import { getPunchRecords as getWaybillPunchRecords, locateVehicle, trackVehicle } from '@/api/business/waybill-manage';
import { getMkPublicDetail } from '@/api/mk-process';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import {
fetchDriversByCarrierOrganizations,
} from '@/utils/carrier-org-resource';
import { getDictionary } from '@/api/system/dictbiz';
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { addressTypeOptions } from '@/option/base/common-address';
@@ -3120,11 +3148,12 @@ import { formatUpdateUserName } from '@/utils/audit';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { getToken } from '@/utils/auth';
import { openImportDialog } from '@/utils/import-excel';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate';
import { InfoFilled, Location, OfficeBuilding, Search, WarnTriangleFilled } from '@element-plus/icons-vue';
import { ElImageViewer } from 'element-plus';
import { ElAutocomplete, ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue';
import {
fallbackPlugin,
@@ -3634,6 +3663,7 @@ export default {
this.detailPageLocked = this.isStandaloneWaybillDetailPage;
this.formModeLocked = this.$route.query.mode || '';
if (this.isPublicWaybillView) return;
this.setupSearchAutocompletes();
if (this.config.enableAllDept && this.isAdmin) {
this.allDept = 1;
}
@@ -4292,8 +4322,12 @@ export default {
) {
return false;
}
// config.canCancel / isInProgressStatus
if (typeof this.config.canCancel === 'function' && !this.config.canCancel(row)) {
return false;
}
const status = this.statusValue(row);
return ['pending', 'processing', 'running'].includes(status);
return status === 'pending';
},
canComplete(row) {
if (
@@ -4524,6 +4558,42 @@ export default {
waybillIsCarrier(row = {}) {
return String(row.carrierTypeName || row.carrierType || '').trim() === '承运商';
},
waybillDetailTaskInfo(row = {}) {
return this.parseJsonObject(row.taskInfoJson || row.taskInfo || {});
},
waybillDetailMileage(row = {}) {
const taskInfo = this.waybillDetailTaskInfo(row);
const goodsRows = this.parseJsonArray(row.goodsJson);
const firstGoods = goodsRows[0] || {};
return this.normalizeMileageValue(
row.mileage || taskInfo.mileage || firstGoods.mileage || ''
);
},
waybillDetailEstimatedStartTime(row = {}) {
const taskInfo = this.waybillDetailTaskInfo(row);
const value =
row.estimatedStartTime ||
row.planStartDate ||
taskInfo.estimatedStartTime ||
taskInfo.planStartDate ||
'';
return this.formatWaybillDetailDate(value);
},
waybillDetailEstimatedEndTime(row = {}) {
const taskInfo = this.waybillDetailTaskInfo(row);
const value =
row.estimatedEndTime ||
row.planEndDate ||
taskInfo.estimatedEndTime ||
taskInfo.planEndDate ||
'';
return this.formatWaybillDetailDate(value);
},
formatWaybillDetailDate(value) {
const text = String(value || '').trim();
if (!text) return '';
return text.slice(0, 10);
},
waybillVehicleNoLabel(row = {}) {
return this.waybillIsRoadTransport(row) ? '车牌号' : '船/航/班列号';
},
@@ -6398,6 +6468,15 @@ export default {
this.form.carrierName = '';
this.form.carrierId = '';
this.form.carrierContractId = '';
this.form.driverId = '';
this.form.driverName = '';
this.form.driverPhone = '';
this.form.vehicleNo = '';
this.form.trailerVehicleNo = '';
this.form.escortName = '';
this.form.escortPhone = '';
this.taskDriverOptions = [];
this.taskEscortOptions = [];
if (nextValue === '承运商') {
this.form.trailerVehicleNo = '';
this.form.escortName = '';
@@ -6651,17 +6730,36 @@ export default {
this.form.carrierContractId = carrier?.carrierContractId || '';
this.form.carrierId = carrier?.carrierId || '';
this.form.carrierName = carrier?.carrierName || '';
return;
} else {
this.form.carrierId = carrier?.carrierContractId ? '' : carrier?.id || '';
this.form.carrierName = value || '';
}
this.form.carrierId = carrier?.carrierContractId ? '' : carrier?.id || '';
this.form.carrierName = value || '';
// /
this.form.driverId = '';
this.form.driverName = '';
this.form.driverPhone = '';
this.form.vehicleNo = '';
this.form.trailerVehicleNo = '';
this.form.escortName = '';
this.form.escortPhone = '';
this.taskDriverOptions = [];
this.taskEscortOptions = [];
},
getTaskCarrierFilter() {
return {
carrierId: this.form.carrierId || '',
carrierName: this.form.carrierName || '',
};
},
loadTaskDriverOptions() {
if (!this.taskInfoFormEnabled || this.taskDriverLoading) return Promise.resolve([]);
this.taskDriverLoading = true;
return getDriverList(1, 9999, { posts: '司机' })
.then(res => {
this.taskDriverOptions = extractRecords(res);
return fetchDriversByCarrierOrganizations(
{ size: 9999, posts: '司机' },
this.getTaskCarrierFilter()
)
.then(records => {
this.taskDriverOptions = records;
return this.taskDriverOptions;
})
.finally(() => {
@@ -6670,10 +6768,17 @@ export default {
},
fetchTaskDriverSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim();
const carrier = this.getTaskCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.taskDriverLoading = true;
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
.then(res => {
const records = extractRecords(res);
fetchDriversByCarrierOrganizations(
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
carrier
)
.then(records => {
this.taskDriverOptions = records;
callback(
records.map(item => ({
@@ -6692,10 +6797,17 @@ export default {
},
fetchTaskEscortSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim();
const carrier = this.getTaskCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.taskEscortLoading = true;
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '押运员' })
.then(res => {
const records = extractRecords(res);
fetchDriversByCarrierOrganizations(
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
carrier
)
.then(records => {
this.taskEscortOptions = records;
callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' })));
})
@@ -7835,12 +7947,7 @@ export default {
this.ensureTransportAmapGeocoder()
.then(() => this.runAmapGeocode('location', keyword))
.then(result => {
this.transportMapSearchResults = (result.geocodes || []).map((item, index) => ({
id: item.id || index,
name: item.formattedAddress || keyword,
address: item.formattedAddress || keyword,
location: item.location,
}));
this.transportMapSearchResults = normalizeMapSearchResults(result, keyword);
const point = this.resolveMapPoint(result);
if (!point) {
this.transportMapStatus = '未找到匹配地址';
@@ -8463,6 +8570,123 @@ export default {
this.page.currentPage = 1;
this.onLoad(this.page);
},
setupSearchAutocompletes() {
const fields = [
{
prop: 'projectName',
placeholder: '请选择或输入项目名称',
fetch: (queryString, callback) =>
this.fetchSearchNameSuggestions(queryString, callback, {
request: keyword =>
getProjectList(1, 20, keyword ? { projectName: keyword } : {}),
getValue: item => item.projectName || item.name || '',
}),
},
{
prop: 'customerName',
placeholder: '请选择或输入客户名称',
fetch: (queryString, callback) =>
this.fetchSearchNameSuggestions(queryString, callback, {
request: keyword =>
getCustomerArchiveList(1, 20, {
...(keyword ? { fullName: keyword } : {}),
customerType: '客户',
}),
getValue: item => item.fullName || item.customerName || item.name || '',
}),
},
{
prop: 'cargoType',
placeholder: '请选择或输入货物类型',
fetch: (queryString, callback) =>
this.fetchSearchNameSuggestions(queryString, callback, {
request: keyword =>
getCargoTypeList(1, 20, keyword ? { cargoName: keyword } : {}),
getValue: item => item.cargoName || item.typeName || item.name || item.label || '',
}),
},
{
prop: 'carrierName',
placeholder: '请选择或输入承运商名称',
fetch: (queryString, callback) =>
this.fetchSearchNameSuggestions(queryString, callback, {
request: keyword =>
getCustomerArchiveList(1, 20, {
...(keyword ? { fullName: keyword } : {}),
customerType: '承运商',
}),
getValue: item =>
item.fullName || item.carrierName || item.customerName || item.name || '',
}),
},
{
prop: 'driverName',
placeholder: '请选择或输入司机名称',
fetch: (queryString, callback) =>
this.fetchSearchNameSuggestions(queryString, callback, {
request: keyword =>
getDriverList(1, 20, {
...(keyword ? { driverName: keyword } : {}),
posts: '司机',
}),
getValue: item => item.driverName || item.name || '',
}),
},
{
prop: 'planName',
placeholder: '请选择或输入计划名称',
fetch: (queryString, callback) =>
this.fetchSearchNameSuggestions(queryString, callback, {
request: keyword =>
getShippingPlanList(1, 20, keyword ? { planName: keyword } : {}),
getValue: item => item.planName || item.name || '',
}),
},
];
fields.forEach(field => {
const column = this.findColumn?.(this.option.column, field.prop);
if (!column) return;
column.searchPlaceholder = field.placeholder;
column.renderSearch = scope =>
this.renderSearchAutocomplete(scope, field.prop, field.placeholder, field.fetch);
});
},
renderSearchAutocomplete(scope, prop, placeholder, fetchSuggestions) {
return h(ElAutocomplete, {
modelValue: scope.row?.[prop] ?? '',
'onUpdate:modelValue': value => {
if (scope.row) scope.row[prop] = value ?? '';
},
fetchSuggestions,
debounce: 300,
clearable: true,
triggerOnFocus: true,
placeholder,
style: 'width: 100%',
valueKey: 'value',
fitInputWidth: true,
});
},
fetchSearchNameSuggestions(queryString, callback, { request, getValue }) {
const keyword = String(queryString || '').trim();
Promise.resolve(request(keyword))
.then(res => {
const records = extractRecords(res);
const seen = new Set();
const options = [];
records.forEach(item => {
const value = String(getValue(item) || '').trim();
if (!value || seen.has(value)) return;
seen.add(value);
options.push({ ...item, value });
});
callback(options);
})
.catch(error => {
window.console.log(error);
callback([]);
});
},
searchChange(params, done) {
this.query = {
...params,
@@ -8728,10 +8952,21 @@ export default {
},
fetchReassignDriverSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim();
const row = this.reassignDrawer.row || {};
const carrier = {
carrierId: row.carrierId || '',
carrierName: row.carrierName || '',
};
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.reassignDrawer.driverLoading = true;
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
.then(res => {
const records = extractRecords(res);
fetchDriversByCarrierOrganizations(
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
carrier
)
.then(records => {
this.reassignDriverOptions = records;
callback(
records.map(item => ({
+556 -81
View File
@@ -289,29 +289,53 @@
<el-table-column label="承运类型" prop="carrierType" min-width="120" show-overflow-tooltip />
<el-table-column label="发货地" prop="departureAddress" min-width="180">
<template #default="{ row }">
<el-tooltip :content="formatFullRouteAddress(row, 'departure')" placement="top">
<el-tooltip
v-if="formatFullRouteAddress(row, 'departure')"
:content="formatFullRouteAddress(row, 'departure')"
placement="top"
:show-after="120"
>
<div class="loading-manage-page__address-cell">
{{ formatRouteAddress(row, 'departure') }}
</div>
</el-tooltip>
<div v-else class="loading-manage-page__address-cell">
{{ formatRouteAddress(row, 'departure') }}
</div>
</template>
</el-table-column>
<el-table-column label="途经地" prop="transitAddress" min-width="190">
<template #default="{ row }">
<el-tooltip :content="formatFullRouteAddress(row, 'transit')" placement="top">
<el-tooltip
v-if="formatFullRouteAddress(row, 'transit')"
:content="formatFullRouteAddress(row, 'transit')"
placement="top"
:show-after="120"
>
<div class="loading-manage-page__address-cell">
{{ formatRouteAddress(row, 'transit') }}
</div>
</el-tooltip>
<div v-else class="loading-manage-page__address-cell">
{{ formatRouteAddress(row, 'transit') }}
</div>
</template>
</el-table-column>
<el-table-column label="到货地" prop="arrivalAddress" min-width="180">
<template #default="{ row }">
<el-tooltip :content="formatFullRouteAddress(row, 'arrival')" placement="top">
<el-tooltip
v-if="formatFullRouteAddress(row, 'arrival')"
:content="formatFullRouteAddress(row, 'arrival')"
placement="top"
:show-after="120"
>
<div class="loading-manage-page__address-cell">
{{ formatRouteAddress(row, 'arrival') }}
</div>
</el-tooltip>
<div v-else class="loading-manage-page__address-cell">
{{ formatRouteAddress(row, 'arrival') }}
</div>
</template>
</el-table-column>
<el-table-column label="承运商" prop="carrierName" min-width="160" show-overflow-tooltip />
@@ -386,26 +410,74 @@
v-if="isStandalonePage"
class="loading-manage-dialog loading-manage-form-dialog loading-manage-form-page"
>
<div class="archive-page-form__title">{{
isStandaloneDetailPage ? detailPageTitle : formPageTitle
}}</div>
<div v-if="!isStandaloneDetailPage" class="archive-page-form__title">{{ formPageTitle }}</div>
<div v-loading="dialogLoading" class="loading-manage-dialog__body">
<div v-if="dialogReadonly" class="loading-detail">
<section-card title="配载单详情">
<div class="loading-detail__summary">
<div class="loading-detail__heading">配载单详情</div>
<div>{{ dialogForm.loadingNo || '-' }}</div>
<el-tag :type="detailStatusType">{{ statusText(dialogForm) }}</el-tag>
<el-tag type="primary">{{ transportTypeLabel(dialogForm.transportType) }}</el-tag>
</div>
<div class="loading-detail__task-row">
<div class="loading-detail__field">
<span class="loading-detail__label">任务信息</span
><span class="loading-detail__value">{{ taskSummary }}</span>
<div class="loading-detail__task-main">
<span class="loading-detail__label">任务信息</span>
<div class="loading-detail__task-grid">
<div class="loading-detail__field">
<span class="loading-detail__label">承运商</span>
<span class="loading-detail__value">{{ detailTaskField('carrierName') }}</span>
</div>
<div class="loading-detail__field">
<span class="loading-detail__label">司机</span>
<span class="loading-detail__value">{{ detailTaskField('driverName') }}</span>
</div>
<div class="loading-detail__field">
<span class="loading-detail__label">车牌号</span>
<span class="loading-detail__value">{{ detailTaskField('vehicleNo') }}</span>
</div>
<div class="loading-detail__field">
<span class="loading-detail__label">手机号</span>
<span class="loading-detail__value">{{ detailTaskField('driverPhone') }}</span>
</div>
<div class="loading-detail__field">
<span class="loading-detail__label">里程</span>
<span class="loading-detail__value">{{
detailTaskMileage ? `${detailTaskMileage} km` : '-'
}}</span>
</div>
<div class="loading-detail__field">
<span class="loading-detail__label">预计发货日期</span>
<span class="loading-detail__value">{{
detailTaskDate('estimatedStartDate')
}}</span>
</div>
<div class="loading-detail__field">
<span class="loading-detail__label">预计完成日期</span>
<span class="loading-detail__value">{{
detailTaskDate('estimatedEndDate')
}}</span>
</div>
<template v-if="isSelfCarrierDetail">
<div class="loading-detail__field">
<span class="loading-detail__label">挂车车牌号</span>
<span class="loading-detail__value">{{
detailTaskField('trailerVehicleNo')
}}</span>
</div>
<div class="loading-detail__field">
<span class="loading-detail__label">押运人</span>
<span class="loading-detail__value">{{ detailTaskField('escortName') }}</span>
</div>
<div class="loading-detail__field">
<span class="loading-detail__label">押运人手机号</span>
<span class="loading-detail__value">{{ detailTaskField('escortPhone') }}</span>
</div>
</template>
</div>
</div>
<div class="loading-detail__field">
<span class="loading-detail__label">附件</span
><span class="loading-detail__value"
<div class="loading-detail__field loading-detail__attachment-field">
<span class="loading-detail__label">附件</span>
<span class="loading-detail__value"
><el-link
v-for="file in detailAttachments"
:key="file.url || file.name"
@@ -441,7 +513,19 @@
>
</div>
<div class="loading-detail__step-address">
{{ node.displayAddress || formatAddressText(node.address) }}
<el-tooltip
v-if="formatDetailRouteTooltip(node)"
:content="formatDetailRouteTooltip(node)"
placement="top"
:show-after="120"
>
<span class="loading-detail__step-address-text">{{
formatDetailRouteShort(node)
}}</span>
</el-tooltip>
<span v-else class="loading-detail__step-address-text">{{
formatDetailRouteShort(node)
}}</span>
</div>
<div class="loading-detail__step-tags">
<el-tag v-for="tag in node.tags || []" :key="tag" size="small">{{ tag }}</el-tag>
@@ -1175,9 +1259,10 @@
</el-form-item>
<el-form-item label="里程km">
<el-input
v-model="dialogForm.mileage"
:model-value="normalizeLoadingMileage(dialogForm.mileage)"
clearable
placeholder="请输入"
:disabled="dialogReadonly"
@input="handleMileageInput"
/>
</el-form-item>
@@ -1408,6 +1493,7 @@ import { getList as getContractList } from '@/api/business/contract-manage';
import { getParentOptions as getCargoTypeOptions } from '@/api/base/cargo-type';
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
import { getList as getProcessConfigList } from '@/api/business/process-config';
import { getList as getCommonAddressList } from '@/api/base/common-address';
import {
@@ -1427,6 +1513,7 @@ const defaultCandidateTransportType = '';
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
const loadingCreateWaybillsStorageKey = 'loading-manage-create-waybills';
const loadingManageListRefreshKey = 'loading-manage-list-refresh';
let detailAmapLoader;
const createSearchForm = () => ({
@@ -1511,6 +1598,14 @@ const normalizeLoadingMileage = value => {
return value;
};
// 提交时未填写里程传 null,禁止自动填充 -1
const toSubmitMileage = value => {
const normalized = normalizeLoadingMileage(value);
if (normalized === '') return null;
const number = Number(normalized);
return Number.isFinite(number) ? number : null;
};
const createCopiedDialogForm = (detail = {}) => {
const form = createDialogForm();
Object.keys(form).forEach(prop => {
@@ -1691,6 +1786,12 @@ export default {
.join(' / ') || '-'
);
},
isSelfCarrierDetail() {
return String(this.dialogForm.carrierType || '').trim() === '自运';
},
detailTaskMileage() {
return normalizeLoadingMileage(this.dialogForm.mileage);
},
detailAttachments() {
return parseJsonArray(this.dialogForm.attachmentsJson || this.dialogForm.attachmentJson).map(
item =>
@@ -1748,12 +1849,29 @@ export default {
deactivated() {
this.closeInnerDialogs();
},
activated() {
if (this.isStandalonePage) return;
if (sessionStorage.getItem(loadingManageListRefreshKey)) {
sessionStorage.removeItem(loadingManageListRefreshKey);
this.loadTable();
}
},
beforeUnmount() {
this.closeInnerDialogs();
},
watch: {
$route(to, from) {
if (!this.isStandalonePage) return;
if (!this.isStandalonePage) {
// 从表单/详情返回列表时刷新,避免草稿确认后仍显示旧状态
if (
from?.path?.includes('/loading-manage/form') ||
from?.path?.includes('/loading-manage/detail')
) {
sessionStorage.removeItem(loadingManageListRefreshKey);
this.loadTable();
}
return;
}
this.syncStandaloneTagTitle();
if (this.isStandaloneDetailPage) {
// 记录变化或上一个页面不是详情态(例如从表单页/列表页进来)时重新拉取详情
@@ -1982,6 +2100,157 @@ export default {
value.includes('highway')
);
},
// 与总单详情一致:公路地址压缩为「市 区县」
formatRoadLocation(value) {
const text = String(value || '')
.replace(/[\/\s]+/g, '')
.trim();
if (!text) return '';
const parts = this.parseWaybillAddress(text);
let city = String(parts.city || '').trim();
let district = String(parts.district || '').trim();
let detail = String(parts.detailAddress || '').trim();
// 自治州/地区/盟下还有「XX市 + 区县」时,以市级作为展示主体
if (/(?:自治州|地区|盟)$/.test(city) && detail) {
const nestedCityMatch = detail.match(/^(.+?市)/);
if (nestedCityMatch) {
city = nestedCityMatch[1];
detail = detail.slice(nestedCityMatch[1].length);
const nestedDistrictMatch = detail.match(
/^(.+?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗))/
);
if (nestedDistrictMatch) district = nestedDistrictMatch[1];
} else if (!district) {
const nestedDistrictMatch = detail.match(
/^(.+?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗))/
);
if (nestedDistrictMatch) district = nestedDistrictMatch[1];
}
}
// 市级后紧跟区县,但 parse 未拆出时再补一次
if (city && !district && detail) {
const nestedDistrictMatch = detail.match(
/^(.+?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗))/
);
if (nestedDistrictMatch) district = nestedDistrictMatch[1];
}
if (city && district) {
// 兼容「梅县区」:命中「县」后还需保留后面的「区」
if (district.endsWith('县') && detail.startsWith(`${district}区`)) {
district = `${district}区`;
}
return `${city} ${district}`;
}
if (city) return city;
if (district) return district;
// 兜底压缩
const withoutProvince = text.replace(/^.*?(?:省|自治区|特别行政区)/, '');
const cityMatch = withoutProvince.match(/.+?(?:市|自治州|州(?!市)|盟(?!市)|地区)/);
if (!cityMatch) return text;
const fallbackCity = cityMatch[0];
const districtSource = withoutProvince.slice(fallbackCity.length);
const districtMatch = districtSource.match(
/^[^市州盟]*?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/
);
if (!districtMatch) {
// 州后面是县级市时,继续解析市 + 区
const nestedCityMatch = districtSource.match(/^(.+?市)/);
if (nestedCityMatch) {
const nestedDistrictSource = districtSource.slice(nestedCityMatch[1].length);
const nestedDistrictMatch = nestedDistrictSource.match(
/^[^市州盟]*?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/
);
if (nestedDistrictMatch) {
return `${nestedCityMatch[1]} ${nestedDistrictMatch[0]}`;
}
return nestedCityMatch[1];
}
return fallbackCity;
}
const fallbackDistrict = `${districtMatch[0]}${
districtSource.slice(districtMatch[0].length).startsWith('区') ? '区' : ''
}`;
return `${fallbackCity} ${fallbackDistrict}`;
},
isStationLikeName(value) {
const text = String(value || '').trim();
if (!text) return false;
return (
/(?:火车站|高铁站|客运站|货运站|港口|码头|机场|空港|航站楼)$/.test(text) ||
/(?<!市|州|盟|区|县|旗)站$/.test(text)
);
},
// 详情运输路线短展示:公路仅市区,非公路仅名称
formatDetailRouteShort(node = {}) {
const name = String(node.name || '').trim();
const address = String(node.address || '').trim();
const displayAddress = String(node.displayAddress || '').trim();
if (!this.isRoadTransportType(this.dialogForm.transportType)) {
if (name) return name;
if (this.isStationLikeName(displayAddress)) return displayAddress;
if (this.isStationLikeName(address)) return address;
return displayAddress || address || '-';
}
const enriched = this.enrichRouteNodeRegion(node);
// 合并节点地址与运单上的市/区字段,避免只解析到市
const region = this.mergeRouteAddressParts(enriched.cityName, enriched.districtName, '');
const source = this.mergeRouteAddressParts(
address,
displayAddress,
region,
enriched.name || name
);
const formatted = this.formatRoadLocation(source);
if (enriched.cityName && enriched.districtName) {
return `${enriched.cityName} ${enriched.districtName}`;
}
if (/(?:市|州|盟|区|县|旗)/.test(formatted)) return formatted;
return formatted || name || displayAddress || address || '-';
},
// 公路悬停展示完整地址;非公路不展示 tooltip
formatDetailRouteTooltip(node = {}) {
if (!this.isRoadTransportType(this.dialogForm.transportType)) return '';
const enriched = this.enrichRouteNodeRegion(node);
const region = this.mergeRouteAddressParts(enriched.cityName, enriched.districtName, '');
const full = this.mergeRouteAddressParts(
String(node.address || '').trim(),
String(node.displayAddress || '').trim(),
region,
String(node.name || enriched.name || '').trim()
);
const short = this.formatDetailRouteShort(node);
if (!full) return '';
return full === short ? '' : full;
},
// 按装/卸标签从已选运单回填市、区,补充 routeJson 中缺失的区县
enrichRouteNodeRegion(node = {}) {
const tags = Array.isArray(node.tags) ? node.tags : [];
const result = { cityName: '', districtName: '', name: '' };
for (const tag of tags) {
const text = String(tag || '');
const loadMatch = text.match(/^装(\d+)$/);
const unloadMatch = text.match(/^卸(\d+)$/);
const index = Number((loadMatch || unloadMatch)?.[1] || 0) - 1;
if (index < 0) continue;
const row = this.selectedWaybillRows[index];
if (!row) continue;
const prefix = loadMatch ? 'departure' : 'arrival';
result.cityName = String(
row[`${prefix}CityName`] || row[`${prefix}City`] || ''
).trim();
result.districtName = String(
row[`${prefix}DistrictName`] || row[`${prefix}District`] || ''
).trim();
result.name = String(row[`${prefix}Name`] || '').trim();
if (result.cityName || result.districtName || result.name) break;
}
return result;
},
splitText(value) {
if (!value) return [];
return String(value)
@@ -2019,37 +2288,72 @@ export default {
formatRouteAddress(row, type) {
const prefix =
type === 'departure' ? 'departure' : type === 'arrival' ? 'arrival' : 'transit';
if (type !== 'transit') {
const address = row[`${prefix}Address`];
const region = this.routeProvinceName(row, prefix);
const district = row[`${prefix}DistrictName`];
const source = this.mergeRouteAddressParts(region, district, address);
return this.formatProvinceCityDistrict(source) || '-';
if (type === 'transit') {
const values = this.splitText(row.transitAddress || row.transitName || '')
.map(value => this.formatListRouteShort(row, value, value))
.filter(value => value && value !== '-');
return [...new Set(values)].join('') || '-';
}
const values = [
row[`${prefix}RegionName`],
row[`${prefix}DistrictName`],
row[`${prefix}Address`],
]
.filter(Boolean)
.flatMap(value => this.splitText(value))
.map(value => this.formatProvinceCityDistrict(value))
.filter(Boolean);
return [...new Set(values)].join(',') || '-';
const name = String(row[`${prefix}Name`] || '').trim();
const address = String(row[`${prefix}Address`] || '').trim();
const cityName = String(
row[`${prefix}CityName`] || row[`${prefix}City`] || ''
).trim();
const districtName = String(
row[`${prefix}DistrictName`] || row[`${prefix}District`] || ''
).trim();
if (
this.isRoadTransportType(row?.transportType) &&
cityName &&
districtName
) {
return `${cityName} ${districtName}`;
}
const region = this.mergeRouteAddressParts(
this.routeProvinceName(row, prefix),
cityName,
districtName
);
const source = this.mergeRouteAddressParts(region, '', address || name);
return this.formatListRouteShort(row, name, source || address || name);
},
formatFullRouteAddress(row, type) {
if (!this.isRoadTransportType(row?.transportType)) return '';
const prefix =
type === 'departure' ? 'departure' : type === 'arrival' ? 'arrival' : 'transit';
if (type !== 'transit') {
const address = row[`${prefix}Address`];
const region = this.routeProvinceName(row, prefix);
const district = row[`${prefix}DistrictName`];
return this.mergeRouteAddressParts(region, district, address) || '-';
if (type === 'transit') {
const values = this.splitText(row.transitAddress || '')
.map(value => String(value || '').trim())
.filter(Boolean);
const full = [...new Set(values)].join('');
const short = this.formatRouteAddress(row, 'transit');
return full && full !== short ? full : '';
}
const values = [row.transitRegionName, row.transitDistrictName, row.transitAddress]
.filter(Boolean)
.flatMap(value => this.splitText(value));
return [...new Set(values)].join('') || '-';
const name = String(row[`${prefix}Name`] || '').trim();
const address = String(row[`${prefix}Address`] || '').trim();
const region = this.mergeRouteAddressParts(
this.routeProvinceName(row, prefix),
row[`${prefix}CityName`] || row[`${prefix}City`],
row[`${prefix}DistrictName`] || row[`${prefix}District`]
);
const full = this.mergeRouteAddressParts(region, '', address || name);
const short = this.formatRouteAddress(row, type);
if (!full) return '';
return full === short ? '' : full;
},
// 列表短地址:公路「市 区县」,非公路仅名称
formatListRouteShort(row = {}, name = '', address = '') {
const rawName = String(name || '').trim();
const rawAddress = String(address || '').trim();
if (!this.isRoadTransportType(row?.transportType)) {
if (rawName) return rawName;
if (this.isStationLikeName(rawAddress)) return rawAddress;
return rawAddress || '-';
}
const source = rawAddress || rawName;
const formatted = this.formatRoadLocation(source);
if (/(?:市|州|盟)/.test(formatted)) return formatted;
return formatted || rawName || rawAddress || '-';
},
formatWaybillAddress(row, type) {
const prefix = type === 'departure' ? 'departure' : 'arrival';
@@ -2275,7 +2579,8 @@ export default {
this.dialogForm = row.copy
? createCopiedDialogForm(detail)
: { ...createDialogForm(), ...detail };
this.dialogForm.mileage = normalizeLoadingMileage(this.dialogForm.mileage);
this.applyLoadingTaskInfoFromDetail(this.dialogForm);
this.applyLoadingMileageFromDetail(this.dialogForm);
await this.restoreWaybillRows(
this.dialogForm.waybillIdsJson,
this.dialogForm.loadingSubNos
@@ -2612,6 +2917,8 @@ export default {
...this.buildQueryParams(this.candidateQuery),
businessStatus: 'pending',
onlyUnassignedLoading: 1,
// 仅查当前账户所属组织及子组织下的运单
allDept: 0,
...(this.dialogMode === 'add' ? { transportType: 'road' } : {}),
};
const transportType = this.getCandidateTransportTypeQuery(query.transportType);
@@ -2706,12 +3013,16 @@ export default {
.map(row => row.waybillNo)
.filter(Boolean)
.join('');
this.dialogForm.waybillIdsJson = JSON.stringify(
rows
.map(row => row.id)
.filter(Boolean)
.map(id => String(id))
);
const rowIds = rows
.map(row => row.id)
.filter(Boolean)
.map(id => String(id));
// 回显失败仅有运单号时,保留原 waybillIdsJson,避免确认提交清空运单绑定
if (rowIds.length) {
this.dialogForm.waybillIdsJson = JSON.stringify(rowIds);
} else if (!String(this.dialogForm.waybillIdsJson || '').trim()) {
this.dialogForm.waybillIdsJson = '[]';
}
this.dialogForm.projectName = uniqueText(rows, 'projectName');
this.dialogForm.customerName = uniqueText(rows, 'customerName');
this.dialogForm.originalNo = uniqueText(rows, 'originalNo');
@@ -2838,29 +3149,95 @@ export default {
remark: item.remark || '',
};
},
applyLoadingMileageFromDetail(form = this.dialogForm) {
let taskMileage = '';
try {
const taskInfo = JSON.parse(form.taskInfoJson || '{}');
taskMileage = taskInfo?.mileage;
} catch (error) {
taskMileage = '';
}
form.mileage = normalizeLoadingMileage(
form.mileage !== undefined && form.mileage !== null && form.mileage !== ''
? form.mileage
: taskMileage
);
},
applyLoadingTaskInfoFromDetail(form = this.dialogForm) {
let taskInfo = {};
try {
taskInfo = JSON.parse(form.taskInfoJson || '{}') || {};
} catch (error) {
taskInfo = {};
}
if (!taskInfo || typeof taskInfo !== 'object' || Array.isArray(taskInfo)) return;
const fields = [
'carrierType',
'carrierName',
'carrierContractId',
'driverName',
'driverPhone',
'vehicleNo',
'trailerVehicleNo',
'escortName',
'escortPhone',
'estimatedStartDate',
'estimatedEndDate',
'taskRemark',
];
fields.forEach(prop => {
if (
(form[prop] === undefined || form[prop] === null || form[prop] === '') &&
taskInfo[prop] !== undefined &&
taskInfo[prop] !== null &&
taskInfo[prop] !== ''
) {
form[prop] = taskInfo[prop];
}
});
if (
(form.mileage === undefined || form.mileage === null || form.mileage === '') &&
taskInfo.mileage !== undefined
) {
form.mileage = normalizeLoadingMileage(taskInfo.mileage);
}
},
detailTaskField(prop) {
const value = String(this.dialogForm?.[prop] ?? '').trim();
return value || '-';
},
detailTaskDate(prop) {
const value = String(this.dialogForm?.[prop] ?? '').trim();
return value ? value.slice(0, 10) : '-';
},
normalizePayload() {
this.syncRouteAddressFields();
this.rebuildSummaryFromWaybills(false);
this.dialogForm.mileage = normalizeLoadingMileage(this.dialogForm.mileage);
const mileage = toSubmitMileage(this.dialogForm.mileage);
const taskInfo = {
carrierType: this.dialogForm.carrierType,
carrierName: this.dialogForm.carrierName,
carrierContractId: this.dialogForm.carrierContractId,
driverName: this.dialogForm.driverName,
driverPhone: this.dialogForm.driverPhone,
vehicleNo: this.dialogForm.vehicleNo,
trailerVehicleNo: this.dialogForm.trailerVehicleNo,
escortName: this.dialogForm.escortName,
escortPhone: this.dialogForm.escortPhone,
estimatedStartDate: this.dialogForm.estimatedStartDate,
estimatedEndDate: this.dialogForm.estimatedEndDate,
taskRemark: this.dialogForm.taskRemark,
};
if (mileage !== null) {
taskInfo.mileage = mileage;
}
return {
...this.dialogForm,
mileage,
routeJson: JSON.stringify(this.routeNodes),
goodsJson: JSON.stringify(this.cargoRows),
taskInfoJson: JSON.stringify({
carrierType: this.dialogForm.carrierType,
carrierName: this.dialogForm.carrierName,
carrierContractId: this.dialogForm.carrierContractId,
driverName: this.dialogForm.driverName,
driverPhone: this.dialogForm.driverPhone,
vehicleNo: this.dialogForm.vehicleNo,
trailerVehicleNo: this.dialogForm.trailerVehicleNo,
escortName: this.dialogForm.escortName,
escortPhone: this.dialogForm.escortPhone,
mileage: this.dialogForm.mileage,
estimatedStartDate: this.dialogForm.estimatedStartDate,
estimatedEndDate: this.dialogForm.estimatedEndDate,
taskRemark: this.dialogForm.taskRemark,
}),
taskInfoJson: JSON.stringify(taskInfo),
};
},
validatePayload(draftMode = false) {
@@ -2915,7 +3292,7 @@ export default {
return true;
},
async saveDraft() {
if (!this.validatePayload(true)) return;
// 暂存不做任何业务校验,直接保存
this.dialogSaving = 'draft';
try {
const res = await loadingApi.saveDraft(this.normalizePayload());
@@ -2923,7 +3300,7 @@ export default {
...this.dialogForm,
...(res.data?.data || res.data || {}),
};
this.dialogForm.mileage = normalizeLoadingMileage(this.dialogForm.mileage);
this.applyLoadingMileageFromDetail(this.dialogForm);
ElMessage.success('暂存成功');
this.loadTable();
} finally {
@@ -2934,7 +3311,11 @@ export default {
if (!this.validatePayload(false)) return;
this.dialogSaving = 'submit';
try {
const payload = this.normalizePayload();
const payload = {
...this.normalizePayload(),
// 确认生成正式配载单(待执行),覆盖草稿状态
businessStatus: 'pending',
};
if (this.dialogMode === 'reassign') {
await loadingApi.reassign(payload);
ElMessage.success('派单成功');
@@ -2942,6 +3323,7 @@ export default {
await loadingApi.submit(payload);
ElMessage.success('确认成功');
}
sessionStorage.setItem(loadingManageListRefreshKey, '1');
this.closeLoadingDialog();
this.loadTable();
} finally {
@@ -3027,6 +3409,7 @@ export default {
this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = '';
this.dialogForm.carrierId = '';
this.clearDialogDriverVehicleFields();
if (this.dialogForm.carrierType === '承运商') {
this.dialogForm.trailerVehicleNo = '';
this.dialogForm.escortName = '';
@@ -3046,6 +3429,23 @@ export default {
this.dialogForm.carrierContractId =
this.dialogForm.carrierType === '承运商' && contract?.id ? contract.id : '';
this.dialogForm.carrierName = contract?.carrierName || '';
this.dialogForm.carrierId = contract?.carrierId || contract?.customerId || '';
this.clearDialogDriverVehicleFields();
},
clearDialogDriverVehicleFields() {
this.dialogForm.driverName = '';
this.dialogForm.driverPhone = '';
this.dialogForm.vehicleNo = '';
this.dialogForm.trailerVehicleNo = '';
this.dialogForm.escortName = '';
this.dialogForm.escortPhone = '';
this.driverOptions = [];
},
getDialogCarrierFilter() {
return {
carrierId: this.dialogForm.carrierId || '',
carrierName: this.dialogForm.carrierName || '',
};
},
handleDriverChange(value) {
const driver = this.driverOptions.find(item => (item.driverName || item.name) === value);
@@ -3057,7 +3457,7 @@ export default {
}
},
fetchDriverSuggestions(queryString, callback) {
this.loadDriverOptions(String(queryString || '').trim())
this.loadDialogDriverOptions(String(queryString || '').trim())
.then(() =>
callback(
this.driverOptions.map(item => ({
@@ -3080,11 +3480,19 @@ export default {
},
fetchEscortSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim();
const carrier = this.getDialogCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.escortLoading = true;
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '押运员' })
.then(res => {
fetchDriversByCarrierOrganizations(
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
carrier
)
.then(records => {
callback(
unwrapRecords(res).map(item => ({
records.map(item => ({
...item,
value: item.driverName || item.name || '',
}))
@@ -3104,6 +3512,7 @@ export default {
handleMileageInput(value) {
this.dialogForm.mileage = String(value || '').replace(/[^\d.]/g, '');
},
normalizeLoadingMileage,
transportTypeLabel(value) {
if (!value) return '-';
return (
@@ -3153,12 +3562,29 @@ export default {
this.driverLoading = true;
try {
this.driverOptions = unwrapRecords(
await getDriverList(1, 20, { driverName: keyword, posts: '司机' })
await getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
);
} finally {
this.driverLoading = false;
}
},
async loadDialogDriverOptions(keyword = '') {
const carrier = this.getDialogCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
this.driverOptions = [];
return [];
}
this.driverLoading = true;
try {
this.driverOptions = await fetchDriversByCarrierOrganizations(
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
carrier
);
return this.driverOptions;
} finally {
this.driverLoading = false;
}
},
async loadCarrierOptions(keyword = '') {
this.carrierLoading = true;
try {
@@ -3220,15 +3646,39 @@ export default {
} else {
this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = '';
this.dialogForm.carrierId = '';
}
return this.taskCarrierOptions;
}
if (this.dialogForm.carrierType === '自运' && contracts.length) {
this.taskCarrierOptions = contracts;
const current = contracts.find(item => item.carrierName === this.dialogForm.carrierName);
this.taskCarrierOptions = contracts
.map(item => ({
...item,
carrierName:
item.partyB ||
item.partyBName ||
item.contractPartyB ||
item.customerContractPartyB ||
item.carrierName ||
'',
carrierId:
item.partyBId ||
item.partyBUserId ||
item.contractPartyBId ||
item.customerContractPartyBId ||
item.carrierId ||
'',
}))
.filter(item => item.carrierName);
const current = this.taskCarrierOptions.find(
item => item.carrierName === this.dialogForm.carrierName
);
this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = current?.carrierName || contracts[0].carrierName || '';
return contracts;
this.dialogForm.carrierName =
current?.carrierName || this.taskCarrierOptions[0]?.carrierName || '';
this.dialogForm.carrierId =
current?.carrierId || this.taskCarrierOptions[0]?.carrierId || '';
return this.taskCarrierOptions;
}
if (this.dialogForm.carrierType !== '自运') {
this.taskCarrierOptions = [];
@@ -3262,6 +3712,7 @@ export default {
}));
this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = partyBNames[0] || '';
this.dialogForm.carrierId = '';
return this.taskCarrierOptions;
} catch (error) {
if (requestId !== this.taskCarrierRequestId) return [];
@@ -3611,11 +4062,6 @@ export default {
line-height: 28px;
}
}
.loading-detail__heading {
font-size: 20px;
font-weight: 600;
margin-right: 8px;
}
.loading-detail__route-title {
display: inline-flex;
align-items: center;
@@ -3627,9 +4073,24 @@ export default {
}
.loading-detail__task-row {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: minmax(0, 1fr) 220px;
gap: 24px;
padding-bottom: 4px;
align-items: start;
}
.loading-detail__task-main {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.loading-detail__task-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px 24px;
}
.loading-detail__attachment-field {
min-width: 0;
}
.loading-detail__field {
display: flex;
@@ -3645,6 +4106,8 @@ export default {
flex-wrap: wrap;
gap: 8px;
text-align: left;
color: #303133;
word-break: break-all;
}
.loading-detail__steps {
display: flex;
@@ -3701,6 +4164,14 @@ export default {
overflow: hidden;
text-overflow: ellipsis;
}
.loading-detail__step-address-text {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: bottom;
cursor: default;
}
.loading-detail__step-tags {
display: flex;
justify-content: center;
@@ -3887,5 +4358,9 @@ export default {
.loading-detail__bottom-grid {
grid-template-columns: 1fr;
}
.loading-detail__task-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
+96 -7
View File
@@ -11,7 +11,7 @@
{{ planData.businessStatusName || '-' }}
</span>
<span class="detail-status-text detail-transport-type">
{{ planData.transportTypeName || '公路运输' }}
{{ transportTypeLabel }}
</span>
</div>
<div class="transport-plan-dispatch-page__summary-grid">
@@ -123,16 +123,20 @@
</el-button>
</template>
<el-table
:data="dispatchList"
:data="pagedDispatchList"
border
height="100%"
row-key="id"
class="transport-plan-dispatch-page__table"
>
<el-table-column type="index" label="序号" width="70" align="center" fixed="left" />
<el-table-column label="序号" width="70" align="center" fixed="left">
<template #default="{ $index }">
{{ dispatchRowIndex($index) }}
</template>
</el-table-column>
<el-table-column label="运输方式" min-width="180" align="center" show-overflow-tooltip>
<template #default="{ row }">
{{ row.transportTypeName || '-' }}
{{ formatTransportType(row.transportTypeName || row.transportType) }}
</template>
</el-table-column>
<el-table-column prop="carrierType" label="承运类型" min-width="150" align="center" show-overflow-tooltip />
@@ -177,6 +181,17 @@
</template>
</el-table-column>
</el-table>
<div class="transport-plan-dispatch-page__pagination">
<el-pagination
v-model:current-page="dispatchPage.currentPage"
v-model:page-size="dispatchPage.pageSize"
:page-sizes="dispatchPage.pageSizes"
layout="total, sizes, prev, pager, next"
:total="dispatchList.length"
@current-change="handleDispatchCurrentChange"
@size-change="handleDispatchSizeChange"
/>
</div>
</section-card>
<!-- 底部按钮 -->
@@ -202,6 +217,7 @@
<script>
import { getDetail, dispatch } from '@/api/business/transport-plan';
import { getDictionary } from '@/api/system/dictbiz';
import SectionCard from '@/components/section-card/main.vue';
import TransportPlanPage from './components/transport-plan-page.vue';
import * as api from '@/api/business/transport-plan';
@@ -209,6 +225,12 @@ import { config, option } from '@/option/business/transport-plan';
const TRANSPORT_PLAN_QUANTITY_UNIT = '吨';
const normalizeDictOptions = (list = []) =>
(Array.isArray(list) ? list : []).map(item => ({
label: item.dictValue || item.label || '',
value: item.dictKey ?? item.value ?? '',
}));
export default {
name: 'TransportPlanDispatch',
components: {
@@ -223,6 +245,12 @@ export default {
dispatchList: [],
attachmentList: [],
transportPlanPageVisible: false,
transportTypeOptions: [],
dispatchPage: {
currentPage: 1,
pageSize: 10,
pageSizes: [10, 20, 50, 100],
},
api,
config,
option,
@@ -232,6 +260,15 @@ export default {
planId() {
return this.$route.query.planId;
},
transportTypeLabel() {
return this.formatTransportType(
this.planData.transportTypeName || this.planData.transportType
);
},
pagedDispatchList() {
const start = (this.dispatchPage.currentPage - 1) * this.dispatchPage.pageSize;
return this.dispatchList.slice(start, start + this.dispatchPage.pageSize);
},
statusTextClass() {
const status = this.planData.businessStatus;
if (status === 2 || status === '2') return 'status-text-success';
@@ -306,13 +343,34 @@ export default {
},
},
created() {
this.loadPlanData();
this.loadTransportTypeOptions().finally(() => {
this.loadPlanData();
});
},
mounted() {
// 初始化 transport-plan-page 组件用于打开弹窗
this.transportPlanPageVisible = true;
},
methods: {
loadTransportTypeOptions() {
return getDictionary({ code: 'transport_type' })
.then(res => {
this.transportTypeOptions = normalizeDictOptions(res.data?.data || []);
})
.catch(() => {
this.transportTypeOptions = [];
});
},
formatTransportType(value) {
const transportType = String(value || '').trim();
if (!transportType) return '-';
// 已是中文标签时直接展示
if (/[\u4e00-\u9fff]/.test(transportType)) return transportType;
const item = this.transportTypeOptions.find(
option => String(option.value ?? '').toLowerCase() === transportType.toLowerCase()
);
return item?.label || transportType;
},
loadPlanData() {
if (!this.planId) {
this.$message.error('缺少计划ID参数');
@@ -327,6 +385,7 @@ export default {
this.planData = res.data.data || {};
this.attachmentList = this.parseAttachments(this.planData.attachmentsJson);
this.dispatchList = this.parseDispatchList(this.planData);
this.dispatchPage.currentPage = 1;
} else {
this.$message.error(res.data?.msg || '加载数据失败');
}
@@ -448,8 +507,30 @@ export default {
}
this.openPageDispatchItemDialog(-1);
},
dispatchRowIndex(index) {
return (this.dispatchPage.currentPage - 1) * this.dispatchPage.pageSize + index + 1;
},
resolveDispatchGlobalIndex(pageIndex) {
return (this.dispatchPage.currentPage - 1) * this.dispatchPage.pageSize + pageIndex;
},
syncDispatchPage() {
const maxPage = Math.max(
1,
Math.ceil(this.dispatchList.length / this.dispatchPage.pageSize) || 1
);
if (this.dispatchPage.currentPage > maxPage) {
this.dispatchPage.currentPage = maxPage;
}
},
handleDispatchCurrentChange(currentPage) {
this.dispatchPage.currentPage = currentPage;
},
handleDispatchSizeChange(pageSize) {
this.dispatchPage.pageSize = pageSize;
this.dispatchPage.currentPage = 1;
},
handleEdit(row, index) {
this.openPageDispatchItemDialog(index, row);
this.openPageDispatchItemDialog(this.resolveDispatchGlobalIndex(index), row);
},
openPageDispatchItemDialog(index = -1, row) {
this.$nextTick(() => {
@@ -469,6 +550,7 @@ export default {
originalSave.apply(component, args);
if (!component.dispatchItemBox) {
this.dispatchList = [...(component.dispatchRows || [])];
this.syncDispatchPage();
}
};
component.__dispatchListSaveSynced = true;
@@ -479,7 +561,8 @@ export default {
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.dispatchList.splice(index, 1);
this.dispatchList.splice(this.resolveDispatchGlobalIndex(index), 1);
this.syncDispatchPage();
this.$message.success('删除成功');
});
},
@@ -782,6 +865,12 @@ export default {
flex: 1;
}
&__pagination {
display: flex;
justify-content: flex-end;
padding-top: 12px;
}
&__actions {
display: flex;
gap: 8px;