1、调整凭证

2、调整运单
This commit is contained in:
2026-09-08 17:50:34 +08:00
parent a7315b4491
commit cba2ad3b3b
43 changed files with 5730 additions and 1119 deletions
@@ -2047,7 +2047,7 @@
v-model="attachmentRows"
:readonly="dialogReadonly"
:file-types="attachmentFileTypes"
:max-size="500"
:max-size="50"
:show-file-list="false"
button-text="上传附件"
@change="handleAttachmentChange"
@@ -4265,7 +4265,7 @@
<vehicle-attachment-upload
v-model="dispatchItemAttachmentRows"
:file-types="attachmentFileTypes"
:max-size="500"
:max-size="50"
:show-file-list="false"
button-text="上传附件"
@change="handleDispatchItemAttachmentChange"
@@ -4965,7 +4965,10 @@
搜索
</el-button>
</div>
<div ref="transportAmap" class="business-crud-page__map"></div>
<div class="map-picker-content">
<map-search-results :results="transportMapSearchResults" @select="selectTransportMapSearchResult" />
<div ref="transportAmap" class="business-crud-page__map"></div>
</div>
<div class="business-crud-page__map-info">
<span>{{ transportMapStatus }}</span>
<span v-if="transportMapSelected.regionName"
@@ -5500,7 +5503,7 @@ const defaultDispatchRow = () => ({
remark: '',
});
const taskCarrierTypes = ['承运商', '自运', '网货平台'];
const taskCarrierTypes = ['承运商', '自运'];
export default {
components: {
@@ -5735,6 +5738,7 @@ export default {
transportMapKeyword: '',
transportMapStatus: '可搜索地址或点击地图选点',
transportMapSelected: {},
transportMapSearchResults: [],
transportAmap: null,
transportAmapMarker: null,
transportAmapGeocoder: null,
@@ -6094,7 +6098,7 @@ export default {
return this.taskCarrierTypes;
},
taskCarrierLabel() {
return this.form.carrierType === '网货平台' ? '承运方' : '承运商';
return '承运商';
},
taskCarrierValue: {
get() {
@@ -6258,7 +6262,7 @@ export default {
return this.isDispatchCarrierRequired(this.dispatchItemForm.carrierType);
},
dispatchCarrierLabel() {
return this.dispatchItemForm.carrierType === '网货平台' ? '承运方' : '承运商';
return '承运商';
},
dispatchCarrierSelection: {
get() {
@@ -7155,7 +7159,7 @@ export default {
this.$message.info('当前运单暂无详情数据');
return;
}
this.$router.push({ path: '/business/waybill-manage', query: { detailId: row.id } });
this.$router.push({ path: '/business/waybill-manage/detail', query: { id: row.id } });
},
transportPlanWaybillIndex(index) {
return (
@@ -8793,7 +8797,7 @@ export default {
}
},
isTaskCarrierRequired(carrierType) {
return ['承运商', '网货平台'].includes(String(carrierType || '').trim());
return String(carrierType || '').trim() === '承运商';
},
resetWaybillCarrierContractState() {
if (!this.isWaybillDetailLayout) return;
@@ -9267,7 +9271,7 @@ export default {
['vehicleNo', this.transportVehicleNoLabel],
];
if (this.isTaskCarrierRequired(row.carrierType)) {
requiredFields.push(['carrierName', row.carrierType === '网货平台' ? '承运方' : '承运商']);
requiredFields.push(['carrierName', '承运商']);
}
if (this.isWaybillDetailLayout && row.carrierType === '承运商') {
requiredFields.push(['carrierContractId', '承运商合同']);
@@ -10106,6 +10110,12 @@ 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,
}));
const point = this.resolveMapPoint(result);
if (!point) {
this.transportMapStatus = '未找到匹配地址';
@@ -10126,6 +10136,11 @@ export default {
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
});
},
selectTransportMapSearchResult(item) {
if (item && item.location) {
this.pickTransportMapPoint(item.location, item.address || item.name || '');
}
},
pickTransportMapPoint(lnglat, keyword) {
const longitude = this.getPointLng(lnglat);
const latitude = this.getPointLat(lnglat);
@@ -12671,7 +12686,7 @@ export default {
this.loadDispatchCarrierOptions(this.dispatchRow);
},
isDispatchCarrierRequired(carrierType) {
return ['承运商', '网货平台'].includes(String(carrierType || '').trim());
return String(carrierType || '').trim() === '承运商';
},
getProjectCarrierOptions(project = {}) {
const rows = this.parseJsonArray(project.carrierJson);
@@ -13058,7 +13073,7 @@ export default {
],
];
if (this.isDispatchCarrierRequired(row.carrierType)) {
requiredFields.push(['carrierName', row.carrierType === '网货平台' ? '承运方' : '承运商']);
requiredFields.push(['carrierName', '承运商']);
}
if (row.carrierType === '承运商') {
requiredFields.push(['carrierContractId', '承运商合同']);
@@ -144,7 +144,9 @@ export default {
: [row.captainName, vehicle];
return values.filter(Boolean).join(' / ') || '-';
},
openWaybill(row) { this.$router.push({ path: '/business/waybill-manage', query: { detailId: row.id } }); },
openWaybill(row) {
this.$router.push({ path: '/business/waybill-manage/detail', query: { id: row.id } });
},
openPlan(row) { this.$router.push({ path: '/business/transport-plan', query: { detailId: row.id } }); },
waybillStatusName(value) { return ({ pending: '待执行', waiting: '待执行', running: '进行中', processing: '进行中', completed: '已完成', cancelled: '已取消' })[value] || value || '-'; },
waybillStatusType(value) { return ({ pending: 'info', waiting: 'info', running: 'warning', processing: 'warning', completed: 'success', cancelled: 'danger' })[value] || 'info'; },
@@ -110,7 +110,7 @@
<template v-if="route.documentType === '运单'">
<div class="goods-heading"><h3>任务信息</h3></div>
<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-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-group></el-form-item></el-form>
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form carrier-form">
<el-row :gutter="16">
<template v-if="isRoad(route)">
@@ -862,7 +862,7 @@ export default {
if (route.documentType === '运单') {
if (this.isRoad(route)) {
if (route.carrierType === '承运商' && (!route.carrierContractId || !route.carrierName || !route.vehicleNo)) return this.$message.warning('请选择承运商并填写车牌号');
if (route.carrierType !== '承运商' && (!route.driverName || !route.driverPhone || !route.vehicleNo)) return this.$message.warning('请补全自运或网货平台的车辆与人员信息');
if (route.carrierType !== '承运商' && (!route.driverName || !route.driverPhone || !route.vehicleNo)) return this.$message.warning('请补全自运的车辆与人员信息');
} else if (!route.vehicleNo || (route.carrierType === '承运商' && (!route.carrierContractId || !route.carrierName))) {
return this.$message.warning('请补全非公路运输的承运信息');
}
@@ -132,7 +132,7 @@
<div class="goods-heading">
<h3>货物信息</h3>
<div class="goods-toolbar">
<el-link type="primary" @click="cargoImportVisible = true">导入货物</el-link>
<!-- <el-link type="primary" @click="cargoImportVisible = true">导入货物</el-link> -->
<el-link type="primary" @click="openCommonCargoDialog">常用货物</el-link>
</div>
</div>
@@ -283,7 +283,7 @@
<vehicle-attachment-upload
v-model="attachmentRows"
:file-types="attachmentFileTypes"
:max-size="500"
:max-size="50"
:show-file-list="false"
button-text="上传附件"
@change="handleAttachmentChange"
@@ -401,7 +401,10 @@
>搜索</el-button
>
</div>
<div ref="addressMap" class="address-map" />
<div class="map-picker-content">
<map-search-results :results="mapSearchResults" @select="selectAddressMapSearchResult" />
<div ref="addressMap" class="address-map" />
</div>
<div class="address-map-status">
{{ mapStatus
}}<span v-if="mapSelected.regionName"> 行政区划:{{ mapSelected.regionName }}</span>
@@ -639,6 +642,7 @@ export default {
mapKeyword: '',
mapStatus: '可搜索地址或点击地图选点',
mapSelected: {},
mapSearchResults: [],
addressMapInstance: null,
addressMapMarker: null,
addressMapGeocoder: null,
@@ -1281,12 +1285,24 @@ export default {
)
)
.then(result => {
const point = result?.geocodes?.[0]?.location;
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;
if (!point) return this.$message.warning('地图搜索无匹配地址');
this.pickAddressMap(point, this.mapKeyword);
})
.catch(() => this.$message.error('地图搜索失败'));
},
selectAddressMapSearchResult(item) {
if (item && item.location) {
this.pickAddressMap(item.location, item.address || item.name || '');
}
},
ensureAddressGeocoder() {
if (this.addressMapGeocoder) return Promise.resolve(this.addressMapGeocoder);
return new Promise(resolve =>
@@ -223,7 +223,7 @@
>
<span>货物信息</span>
<div v-if="!dialogReadonly" class="shipping-template-page__section-actions">
<el-link type="primary" @click="cargoImportBox = true">导入货物</el-link>
<!-- <el-link type="primary" @click="cargoImportBox = true">导入货物</el-link> -->
<el-link type="primary" @click="openCommonCargoDialog">常用货物</el-link>
</div>
</div>
@@ -233,7 +233,10 @@
<div class="shipping-template-page__cargo-wrap">
<el-table :data="transportCargoRows" border empty-text="暂无货物信息">
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="货物类型" min-width="200" align="center">
<el-table-column min-width="200" align="center">
<template #header>
<span>货物类型<span class="goods-required-mark">*</span></span>
</template>
<template #default="{ row }">
<el-cascader
v-model="row.cargoTypePath"
@@ -482,7 +485,7 @@
v-model="attachmentRows"
:readonly="dialogReadonly"
:file-types="attachmentFileTypes"
:max-size="500"
:max-size="50"
:show-file-list="false"
button-text="上传附件"
@change="handleAttachmentChange"
@@ -925,7 +928,13 @@
@keyup.enter="searchTransportMapKeyword"
/><el-button type="primary" @click="searchTransportMapKeyword">搜索</el-button>
</div>
<div ref="transportMap" class="shipping-template-page__map" />
<div class="map-picker-content">
<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
><el-button @click="transportMapBox = false">取消</el-button
@@ -946,12 +955,45 @@
width="1200px"
@opened="loadCommonCargoList"
>
<el-form :model="commonCargoQuery" inline
><el-form-item label="货物名称"
><el-input v-model="commonCargoQuery.cargoName" clearable /></el-form-item
><el-button type="primary" @click="handleCommonCargoSearch">查询</el-button
><el-button @click="handleCommonCargoReset">重置</el-button></el-form
>
<div class="shipping-template-page__dialog-search">
<el-form :model="commonCargoQuery" label-position="right" label-width="100px">
<el-row :gutter="16">
<el-col :span="6">
<el-form-item label="货物名称">
<el-input v-model="commonCargoQuery.cargoName" clearable placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="货物类型">
<el-cascader
v-model="commonCargoQuery.cargoTypePath"
:options="cargoTypeOptions"
:props="cargoTypeCascaderProps"
placeholder="请选择"
clearable
filterable
:loading="cargoTypeLoading"
@visible-change="visible => visible && ensureCargoTypeOptions()"
@change="handleCommonCargoTypeChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="规格型号">
<el-input
v-model="commonCargoQuery.specificationModel"
clearable
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :span="6" class="shipping-template-page__dialog-search-actions">
<el-button type="primary" @click="handleCommonCargoSearch">查询</el-button>
<el-button @click="handleCommonCargoReset">重置</el-button>
</el-col>
</el-row>
</el-form>
</div>
<el-table
:data="commonCargoRows"
border
@@ -1001,35 +1043,6 @@
>
</el-dialog>
<el-dialog
title="导入货物"
v-model="cargoImportBox"
append-to-body
width="560px"
@closed="cargoImportRows = []"
>
<el-upload
action="#"
accept=".xls,.xlsx"
:auto-upload="false"
:show-file-list="false"
:disabled="cargoImportLoading"
:on-change="handleCargoImportFileChange"
><el-button type="primary" :loading="cargoImportLoading">上传</el-button></el-upload
>
<el-link type="primary" @click="handleCargoImportTemplate">下载模板</el-link>
<template #footer
><el-button @click="closeCargoImportDialog">关闭</el-button
><el-button
type="primary"
:loading="cargoImportLoading"
:disabled="!cargoImportRows.length"
@click="confirmCargoImport"
>确定</el-button
></template
>
</el-dialog>
<el-dialog
v-model="shippingTemplateProcessConfigBox"
title="过程配置"
@@ -1066,7 +1079,7 @@
</template>
<script>
import { exportBlob, importBlob } from '@/api/common';
import { exportBlob } from '@/api/common';
import { getList as getCommonAddressList } from '@/api/base/common-address';
import { getList as getAirportMasterList } from '@/api/base/airport-master';
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
@@ -1320,6 +1333,7 @@ export default {
transportMapLoading: false,
transportMapStatus: '可搜索地址或点击地图选点',
transportMapSelected: {},
transportMapSearchResults: [],
transportMapInstance: null,
transportMapMarker: null,
transportMapGeocoder: null,
@@ -1329,9 +1343,6 @@ export default {
commonCargoRows: [],
commonCargoSelected: [],
commonCargoPage: { pageSize: 10, currentPage: 1, total: 0 },
cargoImportBox: false,
cargoImportLoading: false,
cargoImportRows: [],
shippingTemplateProcessConfigBox: false,
shippingTemplateProcessConfigLoading: false,
shippingTemplateProcessConfigRows: [],
@@ -1780,6 +1791,9 @@ export default {
this.normalizeCargo(item)
);
if (!this.transportCargoRows.length) this.transportCargoRows = [defaultCargo()];
this.restoreCargoTypePaths();
// 编辑时先加载货物类型树,保证已有货物类型可以回显为级联路径。
this.ensureCargoTypeOptions().catch(() => {});
this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson);
this.shippingTemplateFreight = {
...defaultFreight(),
@@ -1846,7 +1860,11 @@ export default {
return false;
}
for (const [index, row] of this.transportCargoRows.entries()) {
if (!row.cargoType || !row.cargoName || !row.quantity || !row.quantityUnit) {
if (!String(row.cargoType || '').trim() && !this.cargoPath(row.cargoTypePath).length) {
this.$message.warning(`第${index + 1}行货物类型不能为空`);
return false;
}
if (!String(row.cargoName || '').trim() || !row.quantity || !row.quantityUnit) {
this.$message.warning(`第${index + 1}行货物信息不完整`);
return false;
}
@@ -2339,6 +2357,7 @@ export default {
regionName: this.form[`${prefix}Name`],
addressCode: this.form[`${prefix}AddressCode`],
});
this.transportMapSearchResults = [];
this.transportMapStatus = this.transportMapSelected.longitude
? '已加载当前选点,可重新选点'
: '可搜索地址或点击地图选点';
@@ -2403,9 +2422,18 @@ 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,
}));
const point = this.resolveTransportMapPoint(result);
if (!point) throw new Error('未找到匹配地址');
return this.pickMapPoint(point, keyword);
return this.$nextTick().then(() => {
this.transportMapInstance?.resize();
return this.pickMapPoint(point, keyword);
});
})
.catch(() => {
this.transportMapStatus = '地图搜索失败';
@@ -2415,6 +2443,11 @@ export default {
this.transportMapLoading = false;
});
},
selectTransportMapSearchResult(item) {
if (item && item.location) {
this.pickMapPoint(item.location, item.address || item.name || '');
}
},
pickMapPoint(point, keyword = '') {
const longitude = this.getTransportMapPointLng(point);
const latitude = this.getTransportMapPointLat(point);
@@ -2550,6 +2583,7 @@ export default {
.then(res => {
this.cargoTypeOptions = this.buildCargoTree(extractRecords(res));
this.cargoTypeFlatOptions = this.flattenCargo(this.cargoTypeOptions);
this.restoreCargoTypePaths();
return this.cargoTypeOptions;
})
.finally(() => {
@@ -2603,13 +2637,81 @@ export default {
cargoPath(path) {
return Array.isArray(path) ? path.slice(0, 2).map(String) : [];
},
findCargoTypeOption(row = {}) {
const path = this.cargoPath(row.cargoTypePath);
if (path.length) {
const pathKey = path.join('/');
const pathMatch = this.cargoTypeFlatOptions.find(item => item.path?.join('/') === pathKey);
if (pathMatch) return pathMatch;
}
const codeCandidates = [row.secondCargoTypeCode, row.cargoTypeCode, row.firstCargoTypeCode]
.map(value => String(value || '').trim())
.filter(Boolean);
const nameCandidates = [
row.secondCargoTypeName,
row.cargoType,
row.goodsType,
row.firstCargoTypeName,
]
.map(value => String(value || '').trim())
.filter(Boolean);
return (
this.cargoTypeFlatOptions.find(item =>
codeCandidates.some(
code => code === String(item.cargoCode || item.code || item.id || '').trim()
)
) ||
this.cargoTypeFlatOptions.find(item =>
nameCandidates.some(name => name === String(item.cargoName || '').trim())
)
);
},
resolveCargoTypePath(row = {}) {
const matched = this.findCargoTypeOption(row);
return matched?.path ? this.cargoPath(matched.path) : [];
},
restoreCargoTypePaths() {
if (!this.transportCargoRows.length || !this.cargoTypeFlatOptions.length) return;
this.transportCargoRows.forEach(row => {
const path = this.resolveCargoTypePath(row);
if (!path.length) return;
const item = this.cargoTypeFlatOptions.find(
option => option.path?.join('/') === path.join('/')
);
row.cargoTypePath = path;
row.cargoType =
item?.cargoName ||
row.cargoType ||
row.secondCargoTypeName ||
row.firstCargoTypeName ||
'';
row.cargoTypeCode =
item?.cargoCode ||
row.cargoTypeCode ||
row.secondCargoTypeCode ||
row.firstCargoTypeCode ||
'';
});
},
normalizeCargo(row = {}) {
const cargoTypePath = this.resolveCargoTypePath(row);
const cargoType = this.findCargoTypeOption({ ...row, cargoTypePath });
return {
...defaultCargo(),
...row,
cargoTypePath: this.cargoPath(row.cargoTypePath),
cargoType: row.cargoType || row.secondCargoTypeName || row.firstCargoTypeName || '',
cargoTypeCode: row.cargoTypeCode || row.secondCargoTypeCode || row.firstCargoTypeCode || '',
cargoTypePath,
cargoType:
cargoType?.cargoName ||
row.cargoType ||
row.secondCargoTypeName ||
row.firstCargoTypeName ||
'',
cargoTypeCode:
cargoType?.cargoCode ||
row.cargoTypeCode ||
row.secondCargoTypeCode ||
row.firstCargoTypeCode ||
'',
cargoName: row.cargoName || row.goodsName || '',
};
},
@@ -2623,7 +2725,7 @@ export default {
.filter(Boolean);
row.cargoTypePath = path;
row.cargoType = labels.at(-1) || '';
row.cargoTypeCode = item?.cargoCode || '';
row.cargoTypeCode = item?.cargoCode || item?.code || item?.id || '';
row.cargoName = '';
},
fetchCargoSuggestions(query, callback, row) {
@@ -2637,8 +2739,28 @@ export default {
)
.catch(() => callback([]));
},
selectCargoSuggestion(row, item) {
Object.assign(row, this.normalizeCargo(item));
async selectCargoSuggestion(row, item = {}) {
const cargoTypeOption = this.findCargoTypeOption(item);
const selectedCargo = {
...row,
...item,
cargoName: item.cargoName || item.name || item.label || item.value || row.cargoName || '',
};
try {
await this.ensureCargoTypeOptions();
} catch (error) {
// 货物类型树加载失败时仍保留货物名称,提交时会给出货物类型必填提示。
}
// 货物名称返回了类型信息时,以货物自身的类型覆盖当前行原有类型。
// 这样从一个类型切换到另一个类型时,级联选择器不会继续显示旧值。
const matchedCargoType = this.findCargoTypeOption(item) || cargoTypeOption;
if (matchedCargoType?.path?.length) {
selectedCargo.cargoTypePath = this.cargoPath(matchedCargoType.path);
selectedCargo.cargoType = matchedCargoType.cargoName || selectedCargo.cargoType;
selectedCargo.cargoTypeCode =
matchedCargoType.cargoCode || matchedCargoType.code || matchedCargoType.id || '';
}
Object.assign(row, this.normalizeCargo(selectedCargo));
row.value = undefined;
this.syncFreightItems();
},
@@ -2786,13 +2908,16 @@ export default {
},
openCommonCargoDialog() {
this.commonCargoSelected = [];
this.commonCargoQuery = {};
this.commonCargoPage.currentPage = 1;
this.commonCargoBox = true;
this.ensureCargoTypeOptions().catch(() => {});
},
loadCommonCargoList() {
this.commonCargoLoading = true;
const { cargoTypePath, ...query } = this.commonCargoQuery || {};
getCommonCargoList(this.commonCargoPage.currentPage, this.commonCargoPage.pageSize, {
...this.normalizeSearch(this.commonCargoQuery),
...this.normalizeSearch(query),
allDept: 0,
})
.then(res => {
@@ -2812,6 +2937,29 @@ export default {
this.commonCargoQuery = {};
this.handleCommonCargoSearch();
},
handleCommonCargoTypeChange(value) {
const path = this.cargoPath(value);
const firstCargoType = this.cargoTypeFlatOptions.find(
item => item.path?.join('/') === path.slice(0, 1).join('/')
);
const secondCargoType = this.cargoTypeFlatOptions.find(
item => item.path?.join('/') === path.join('/')
);
this.commonCargoQuery = {
...this.commonCargoQuery,
cargoTypePath: path,
firstCargoTypeName: firstCargoType?.cargoName || '',
firstCargoTypeCode: firstCargoType?.cargoCode || '',
secondCargoTypeName: path.length > 1 ? secondCargoType?.cargoName || '' : '',
secondCargoTypeCode: path.length > 1 ? secondCargoType?.cargoCode || '' : '',
};
if (!path.length) {
delete this.commonCargoQuery.firstCargoTypeName;
delete this.commonCargoQuery.firstCargoTypeCode;
delete this.commonCargoQuery.secondCargoTypeName;
delete this.commonCargoQuery.secondCargoTypeCode;
}
},
handleCommonCargoCurrentChange(page) {
this.commonCargoPage.currentPage = page;
this.loadCommonCargoList();
@@ -2823,54 +2971,16 @@ export default {
handleCommonCargoSelectionChange(rows) {
this.commonCargoSelected = rows || [];
},
selectCommonCargoRow(row) {
async selectCommonCargoRow(row) {
await this.ensureCargoTypeOptions().catch(() => {});
this.addCargoRow(-1, row);
this.commonCargoBox = false;
},
confirmCommonCargoSelection() {
async confirmCommonCargoSelection() {
await this.ensureCargoTypeOptions().catch(() => {});
this.commonCargoSelected.forEach(row => this.addCargoRow(-1, row));
this.commonCargoBox = false;
},
handleCargoImportFileChange(file) {
const raw = file.raw;
if (!raw || !/\.(xls|xlsx)$/i.test(raw.name || ''))
return this.$message.warning('请上传 Excel 文件');
this.cargoImportLoading = true;
importBlob('/blade-transport/shipping-template/import-goods', raw)
.then(async res => {
const type = res.headers?.['content-type'] || res.data?.type || '';
if (type.includes('excel')) {
downloadXls(
res.data,
`发货模板货物导入失败明细${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`
);
this.$message.warning('部分数据导入失败,已下载失败明细');
return;
}
const result = JSON.parse(await res.data.text());
if (result.code !== 200) throw new Error(result.msg);
this.cargoImportRows = result.data || [];
})
.catch(() => this.$message.error('货物导入失败,请检查文件格式'))
.finally(() => {
this.cargoImportLoading = false;
});
},
confirmCargoImport() {
this.cargoImportRows.forEach(row => this.addCargoRow(-1, row));
this.closeCargoImportDialog();
},
closeCargoImportDialog() {
if (!this.cargoImportLoading) {
this.cargoImportBox = false;
this.cargoImportRows = [];
}
},
handleCargoImportTemplate() {
exportBlob('/blade-transport/shipping-template/export-goods-template').then(res =>
downloadXls(res.data, '发货模板货物导入模板.xlsx')
);
},
handleAttachmentChange(list) {
const name = this.userInfo?.realName || this.userInfo?.userName || '';
const time = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
@@ -3044,6 +3154,10 @@ export default {
width: 100%;
overflow-x: auto;
}
.goods-required-mark {
margin-left: 2px;
color: #f56c6c;
}
.shipping-template-page__cargo-total {
display: flex;
justify-content: flex-start;
@@ -3388,10 +3502,27 @@ export default {
display: flex;
gap: 8px;
}
.shipping-template-page__dialog-search {
margin-bottom: 8px;
padding: 12px 12px 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.shipping-template-page__dialog-search-actions {
display: flex;
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;
height: 100% !important;
}
.shipping-template-page__map-info {
margin-top: 8px;
color: #606266;
@@ -301,7 +301,7 @@
>
<span>货物信息</span>
<div v-if="!dialogReadonly" class="transport-plan-page__section-actions">
<el-link type="primary" @click="cargoImportBox = true">导入货物</el-link>
<!-- <el-link type="primary" @click="cargoImportBox = true">导入货物</el-link> -->
<el-link type="primary" @click="openCommonCargoDialog">常用货物</el-link>
</div>
</div>
@@ -526,7 +526,7 @@
v-model="attachmentRows"
:readonly="dialogReadonly"
:file-types="attachmentFileTypes"
:max-size="500"
:max-size="50"
:show-file-list="false"
button-text="上传附件"
@change="handleAttachmentChange"
@@ -681,10 +681,8 @@
<div class="transport-plan-page__transport-plan-detail-route-point">
<b class="is-start">起</b>
<div>
<strong>{{
detailRow.departureName || detailRow.departureAddress || '-'
}}</strong>
<p>{{ formatTransportPlanProvinceCityDistrict(detailRow.departureAddress) }}</p>
<strong>{{ formatTransportPlanDetailRouteName('departure') }}</strong>
<p>{{ detailRow.departureAddress || '-' }}</p>
<p>
{{
[detailRow.departureContact, detailRow.departurePhone]
@@ -698,8 +696,8 @@
<div class="transport-plan-page__transport-plan-detail-route-point">
<b class="is-end">终</b>
<div>
<strong>{{ detailRow.arrivalName || detailRow.arrivalAddress || '-' }}</strong>
<p>{{ formatTransportPlanProvinceCityDistrict(detailRow.arrivalAddress) }}</p>
<strong>{{ formatTransportPlanDetailRouteName('arrival') }}</strong>
<p>{{ detailRow.arrivalAddress || '-' }}</p>
<p>
{{
[detailRow.arrivalContact, detailRow.arrivalPhone]
@@ -1558,7 +1556,7 @@
>
<span>货物信息</span>
<div class="transport-plan-page__section-actions">
<el-link type="primary" @click="cargoImportBox = true">导入货物</el-link>
<!-- <el-link type="primary" @click="cargoImportBox = true">导入货物</el-link> -->
<el-link type="primary" @click="openCommonCargoDialog">常用货物</el-link>
</div>
</div>
@@ -2201,7 +2199,7 @@
<vehicle-attachment-upload
v-model="dispatchItemAttachmentRows"
:file-types="attachmentFileTypes"
:max-size="500"
:max-size="50"
:show-file-list="false"
button-text="上传附件"
@change="handleDispatchItemAttachmentChange"
@@ -2518,7 +2516,10 @@
搜索
</el-button>
</div>
<div ref="transportAmap" class="transport-plan-page__map"></div>
<div class="map-picker-content">
<map-search-results :results="transportMapSearchResults" @select="selectTransportMapSearchResult" />
<div ref="transportAmap" class="transport-plan-page__map"></div>
</div>
<div class="transport-plan-page__map-info">
<span>{{ transportMapStatus }}</span>
<span v-if="transportMapSelected.regionName"
@@ -2689,7 +2690,10 @@ import { getList as getCargoTypeList } from '@/api/base/cargo-type';
import { getList as getPortTerminalList } from '@/api/base/port-terminal';
import { getList as getRailwayStationList } from '@/api/base/railway-station';
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
import { getList as getContractList } from '@/api/business/contract-manage';
import {
getDetail as getContractDetail,
getList as getContractList,
} from '@/api/business/contract-manage';
import {
getDetail as getProjectDetail,
getList as getProjectList,
@@ -2945,7 +2949,7 @@ const defaultDispatchRow = () => ({
remark: '',
});
const taskCarrierTypes = ['承运商', '自运', '网货平台'];
const taskCarrierTypes = ['承运商', '自运'];
export default {
components: {
@@ -3121,6 +3125,7 @@ export default {
transportMapKeyword: '',
transportMapStatus: '可搜索地址或点击地图选点',
transportMapSelected: {},
transportMapSearchResults: [],
transportAmap: null,
transportAmapMarker: null,
transportAmapGeocoder: null,
@@ -3446,7 +3451,7 @@ export default {
return this.isDispatchCarrierRequired(this.dispatchItemForm.carrierType);
},
dispatchCarrierLabel() {
return this.dispatchItemForm.carrierType === '网货平台' ? '承运方' : '承运商';
return '承运商';
},
dispatchCarrierSelection: {
get() {
@@ -3790,6 +3795,34 @@ export default {
const cityMatch = text.match(/市/);
return cityMatch ? text.slice(0, cityMatch.index + 1) : text;
},
formatTransportPlanRoadCityDistrict(value) {
const text = String(value || '').trim();
if (!text) return '-';
const cityMatch = text.match(/市/);
if (!cityMatch) return this.formatTransportPlanProvinceCityDistrict(text);
const cityEnd = cityMatch.index + 1;
const cityWithProvince = text.slice(0, cityEnd);
const city =
cityWithProvince.match(/(?:省|自治区|特别行政区)([^省]+市)$/)?.[1] || cityWithProvince;
const districtText = text.slice(cityEnd);
const districtMatch = districtText.match(
/^(?:.*(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗))/
);
if (!districtMatch) return city;
const district = districtMatch[0];
return district ? `${city} ${district}` : city;
},
formatTransportPlanDetailRouteName(type) {
const nameProp = type === 'departure' ? 'departureName' : 'arrivalName';
const addressProp = type === 'departure' ? 'departureAddress' : 'arrivalAddress';
const name = this.detailRow[nameProp] || this.detailRow[addressProp];
const transportMode = this.resolveTransportMode(this.detailRow.transportType);
return transportMode === 'road'
? this.formatTransportPlanRoadCityDistrict(name)
: name || '-';
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
},
@@ -3988,7 +4021,7 @@ export default {
this.$message.info('当前运单暂无详情数据');
return;
}
this.$router.push({ path: '/business/waybill-manage', query: { detailId: row.id } });
this.$router.push({ path: '/business/waybill-manage/detail', query: { id: row.id } });
},
transportPlanWaybillIndex(index) {
return (
@@ -4054,10 +4087,28 @@ export default {
request
.then(res => {
const detail = res?.data?.data || res?.data || row;
this.detailRow = detail;
this.ensureTransportPlanDetailTransportTypeName(detail);
this.applyFormDetail(detail);
return null;
if (!detail.contractId) {
this.detailRow = detail;
this.ensureTransportPlanDetailTransportTypeName(detail);
this.applyFormDetail(detail);
return null;
}
return getContractDetail(detail.contractId)
.then(contractRes => {
const contract = contractRes?.data?.data || contractRes?.data || {};
this.detailRow = {
...detail,
customerName:
contract.partyA || contract.customerName || detail.customerName || '',
};
this.ensureTransportPlanDetailTransportTypeName(this.detailRow);
this.applyFormDetail(this.detailRow);
})
.catch(() => {
this.detailRow = detail;
this.ensureTransportPlanDetailTransportTypeName(detail);
this.applyFormDetail(detail);
});
})
.finally(() => {
this.detailLoading = false;
@@ -4610,7 +4661,11 @@ export default {
this.form.settlementCurrency =
contract.settlementCurrency || contract.settlementCurrencyCode || contract.currency || '';
this.form.customerName =
contract.customerName || contract.customerNames || this.form.customerName || '';
contract.partyA ||
contract.customerName ||
contract.customerNames ||
this.form.customerName ||
'';
this.$refs.crud?.validateField?.('contractName');
},
loadDispatchContractCurrency(plan = {}) {
@@ -5505,6 +5560,12 @@ 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,
}));
const point = this.resolveMapPoint(result);
if (!point) {
this.transportMapStatus = '未找到匹配地址';
@@ -5525,6 +5586,11 @@ export default {
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
});
},
selectTransportMapSearchResult(item) {
if (item && item.location) {
this.pickTransportMapPoint(item.location, item.address || item.name || '');
}
},
pickTransportMapPoint(lnglat, keyword) {
const longitude = this.getPointLng(lnglat);
const latitude = this.getPointLat(lnglat);
@@ -6246,8 +6312,7 @@ export default {
},
handleImport() {
if (this.config.enableTransportPlanImport) {
this.transportPlanImportBox = true;
this.loadProjectOptions();
this.$router.push('/business/transport-plan/import');
return;
}
this.$message.warning('运输计划导入接口未配置');
@@ -6637,33 +6702,13 @@ export default {
this.$message.warning('运输计划数据异常,无法调度');
return;
}
this.dispatchRow = { ...row };
this.dispatchCarrierOptions = [];
this.dispatchCarrierContractOptions = [];
this.dispatchCarrierRequestId += 1;
this.dispatchBox = true;
this.dispatchLoading = true;
this.dispatchRows = [];
this.loadDispatchTransportTypeOptions();
const request =
this.api && typeof this.api.getDetail === 'function'
? this.api.getDetail(row.id)
: Promise.resolve({ data: { data: row } });
request
.then(res => {
const detail = {
...row,
...(res?.data?.data || {}),
};
this.dispatchRow = detail;
this.dispatchRows = this.buildDispatchRows(detail);
return this.loadDispatchContractCurrency(detail).then(() => {
this.loadDispatchCarrierOptions(detail);
});
})
.finally(() => {
this.dispatchLoading = false;
});
// 跳转到独立的调度页面
this.$router.push({
path: '/business/transport-plan-dispatch',
query: {
planId: row.id,
},
});
},
loadDispatchTransportTypeOptions() {
if (this.dispatchTransportTypeOptions.length || this.dispatchTransportTypeLoading) {
@@ -7308,7 +7353,7 @@ export default {
this.loadDispatchCarrierOptions(this.dispatchRow);
},
isDispatchCarrierRequired(carrierType) {
return ['承运商', '网货平台'].includes(String(carrierType || '').trim());
return String(carrierType || '').trim() === '承运商';
},
getProjectCarrierOptions(project = {}) {
const rows = this.parseJsonArray(project.carrierJson);
@@ -7748,7 +7793,7 @@ export default {
],
];
if (this.isDispatchCarrierRequired(row.carrierType)) {
requiredFields.push(['carrierName', row.carrierType === '网货平台' ? '承运方' : '承运商']);
requiredFields.push(['carrierName', '承运商']);
}
if (row.carrierType === '承运商') {
requiredFields.push(['carrierContractId', '承运商合同']);
@@ -119,18 +119,22 @@
label="运单批次号"
min-width="140"
/><el-table-column prop="originalNo" label="原始单号" min-width="130" /><el-table-column
prop="loadingIdentifier"
label="配载标识号"
min-width="130"
/><el-table-column
prop="vehicleNo"
label="车牌号/航班号/船号/班列号"
min-width="190"
/><el-table-column prop="driverName" label="司机/船长" min-width="120" /><el-table-column
prop="transportType"
label="运输方式"
width="110"
/><el-table-column prop="cargoName" label="货物名称" width="130" /><el-table-column
prop="cargoType"
label="货物类型"
width="120"
/><el-table-column prop="quantity" label="重量" width="90" /><el-table-column
/><el-table-column prop="transportType" label="运输方式" width="110" /><el-table-column
prop="driverName"
label="司机/船长姓名"
min-width="120"
/><el-table-column
prop="driverPhone"
label="司机/船长手机号"
width="140"
/><el-table-column
prop="departureAddress"
label="发货地址"
min-width="220"
@@ -148,11 +152,22 @@
prop="arrivalPhone"
label="收货联系人电话"
width="140"
/><el-table-column prop="startDate" label="开始时间" width="160" sortable /><el-table-column
prop="endDate"
label="结束时间"
width="160"
sortable
/><el-table-column prop="cargoName" label="货物名称" width="130" /><el-table-column
prop="cargoType"
label="货物类型"
width="120"
/><el-table-column prop="packageType" label="包装" width="100" /><el-table-column
prop="quantity"
label="数量"
width="90"
/><el-table-column prop="quantityUnit" label="数量单位" width="100" /><el-table-column
prop="specification"
label="规格"
width="120"
/><el-table-column prop="model" label="型号" width="120" /><el-table-column
prop="mileage"
label="里程(km)"
width="100"
/><el-table-column prop="unitPrice" label="单价" width="90" /><el-table-column
prop="freight"
label="运费"
@@ -161,11 +176,31 @@
prop="freightTotal"
label="运费合计"
width="100"
/><el-table-column
prop="actualStartDate"
label="实际发货时间"
width="160"
sortable
/><el-table-column
prop="actualEndDate"
label="实际完成时间"
width="160"
sortable
/><el-table-column
prop="planStartDate"
label="预计发货时间"
width="160"
sortable
/><el-table-column
prop="planEndDate"
label="预计完成时间"
width="160"
sortable
/><el-table-column prop="remark" label="备注" min-width="160" /><el-table-column
label="操作"
width="80"
>-</el-table-column
></el-table
prop="waybillIdentifier"
label="同一运单标识号"
min-width="140"
/><el-table-column label="操作" width="80">-</el-table-column></el-table
>
<el-pagination
v-model:current-page="detailPage.current"
@@ -441,7 +476,6 @@
</section>
<div v-if="createPage" class="waybill-import-create__footer">
<el-button @click="closeCreate">取消</el-button>
<el-button @click="saveDraft">保存草稿</el-button>
<el-button type="primary" @click="confirmImport">确认导入</el-button>
</div>
</component>
@@ -496,7 +530,7 @@ const createDefaultForm = () => ({
const form = reactive(createDefaultForm());
// 批量导入状态仅保留草稿与导入完成两种,与后端 importStatus 取值一致。
const importStatusOptions = [
{ label: '草稿', value: 'draft' },
{ label: '进行中', value: 'draft' },
{ label: '导入完成', value: 'completed' },
];
// 编辑草稿时明细已入库,此时不再强制重新上传附件。
@@ -541,7 +575,7 @@ const previewRows = computed(() =>
);
const carrierContractsLoaded = ref(false),
hasCarrierContracts = ref(null);
const carrierTypes = ['承运商', '自运', '网货平台'];
const carrierTypes = ['承运商', '自运'];
const isSelfOperated = computed(() => form.carrierType === '自运');
// 项目下没有承运商合同时只允许自运,与运单管理的承运类型收窄规则一致。
const carrierTypeOptions = computed(() =>
@@ -570,19 +604,47 @@ const resetCreateForm = () => {
const detailColumns = [
{ prop: 'batchNo', label: '运单批次号', minWidth: 150 },
{ prop: 'originalNo', label: '原始单号', editor: 'input', minWidth: 150 },
{ prop: 'loadingIdentifier', label: '配载标识号', editor: 'input', minWidth: 150 },
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号', editor: 'input', minWidth: 220 },
{ prop: 'driverName', label: '司机/船长', editor: 'driver', minWidth: 170 },
{ prop: 'transportType', label: '运输类型', editor: 'transportType', minWidth: 150 },
{ prop: 'transportType', label: '运输方式', editor: 'transportType', minWidth: 150 },
{ prop: 'driverName', label: '司机/船长姓名', editor: 'driver', minWidth: 170 },
{ prop: 'driverPhone', label: '司机/船长手机号', editor: 'input', minWidth: 150 },
{ prop: 'departureAddress', label: '发货地址', editor: 'input', minWidth: 220 },
{ prop: 'departureContact', label: '发货联系人', editor: 'input', minWidth: 120 },
{ prop: 'departurePhone', label: '发货联系人电话', editor: 'input', minWidth: 150 },
{ prop: 'arrivalAddress', label: '到货地址', editor: 'input', minWidth: 220 },
{ prop: 'arrivalContact', label: '收货联系人', editor: 'input', minWidth: 120 },
{ prop: 'arrivalPhone', label: '收货联系人电话', editor: 'input', minWidth: 150 },
{ prop: 'cargoName', label: '货物名称', editor: 'cargoName', minWidth: 220 },
{ prop: 'cargoType', label: '货物类型', editor: 'cargoType', minWidth: 220 },
{ prop: 'quantity', label: '重量', editor: 'number', minWidth: 130 },
{ prop: 'packageType', label: '包装', editor: 'input', minWidth: 120 },
{ prop: 'quantity', label: '数量', editor: 'number', minWidth: 130 },
{ prop: 'quantityUnit', label: '数量单位', editor: 'input', minWidth: 120 },
{ prop: 'specification', label: '规格', editor: 'input', minWidth: 150 },
{ prop: 'model', label: '型号', editor: 'input', minWidth: 150 },
{ prop: 'mileage', label: '里程(km)', editor: 'number', minWidth: 120 },
{ prop: 'unitPrice', label: '单价', editor: 'number', minWidth: 120 },
{ prop: 'freight', label: '运费', editor: 'number', minWidth: 120 },
{ prop: 'otherFeeTotal', label: '其他费用合计', editor: 'number', minWidth: 140 },
{ prop: 'freightTotal', label: '运费合计', editor: 'number', minWidth: 120 },
{ prop: 'actualStartDate', label: '实际发货时间', editor: 'date', minWidth: 160 },
{ prop: 'actualEndDate', label: '实际完成时间', editor: 'date', minWidth: 160 },
{ prop: 'planStartDate', label: '预计发货时间', editor: 'date', minWidth: 160 },
{ prop: 'planEndDate', label: '预计完成时间', editor: 'date', minWidth: 160 },
{ prop: 'remark', label: '备注', editor: 'textarea', minWidth: 200 },
{ prop: 'waybillIdentifier', label: '同一运单标识号', editor: 'input', minWidth: 150 },
];
const requiredDetailFields = [
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' },
{ prop: 'driverName', label: '司机/船长' },
{ prop: 'transportType', label: '运输类型' },
{ prop: 'transportType', label: '运输方式' },
{ prop: 'departureAddress', label: '发货地址' },
{ prop: 'arrivalAddress', label: '到货地址' },
{ prop: 'cargoName', label: '货物名称' },
{ prop: 'cargoType', label: '货物类型' },
{ prop: 'quantity', label: '数量' },
{ prop: 'quantityUnit', label: '数量单位' },
{ prop: 'actualStartDate', label: '实际发货时间' },
{ prop: 'actualEndDate', label: '实际完成时间' },
];
const projectQueryParams = { approvalStatuses: 'approved,change_approved' };
const extractRecords = res => {
@@ -975,6 +1037,12 @@ const carrierChange = value => {
if (!isSelfOperated.value) form.carrierContractId = item.carrierContractId || '';
};
const fileChange = async (file, list) => {
if (file.raw?.size > 50 * 1024 * 1024) {
ElMessage.warning('单个文件大小不能超过50M');
files.value = [];
form.file = null;
return;
}
files.value = list.slice(-1);
form.file = file.raw;
const XLSX = await import('xlsx');
@@ -988,25 +1056,35 @@ const fileChange = async (file, list) => {
);
const keyMap = {
originalNo: ['原始单号'],
loadingIdentifier: ['配载标识号'],
vehicleNo: ['车牌号/航班号/船号/班列号'],
driverName: ['司机/船长'],
transportType: ['运输类型', '运输方式'],
cargoName: ['货物名称'],
cargoType: ['货物类型'],
quantity: ['重量'],
transportType: ['运输方式', '运输类型'],
driverName: ['司机/船长姓名', '司机/船长'],
driverPhone: ['司机/船长手机号'],
departureAddress: ['发货地址'],
departureContact: ['发货联系人'],
departurePhone: ['发货联系人电话'],
arrivalAddress: ['到货地址'],
arrivalContact: ['到货联系人', '收货联系人'],
arrivalContact: ['收货联系人', '到货联系人'],
arrivalPhone: ['收货联系人电话'],
startDate: ['开始时间'],
endDate: ['结束时间'],
cargoName: ['货物名称'],
cargoType: ['货物类型'],
packageType: ['包装'],
quantity: ['数量', '重量'],
quantityUnit: ['数量单位'],
specification: ['规格'],
model: ['型号'],
mileage: ['里程(km)', '里程'],
unitPrice: ['单价'],
freight: ['运费'],
otherFeeTotal: ['其他费用合计'],
freightTotal: ['运费合计'],
actualStartDate: ['实际发货时间', '开始时间'],
actualEndDate: ['实际完成时间', '结束时间'],
planStartDate: ['预计发货时间'],
planEndDate: ['预计完成时间'],
remark: ['备注'],
waybillIdentifier: ['同一运单标识号'],
};
const normalizeImportDate = value => {
if (value instanceof Date && !Number.isNaN(value.getTime())) {
@@ -1025,8 +1103,10 @@ const fileChange = async (file, list) => {
item[key] ??
'';
});
row.startDate = normalizeImportDate(row.startDate);
row.endDate = normalizeImportDate(row.endDate);
row.actualStartDate = normalizeImportDate(row.actualStartDate);
row.actualEndDate = normalizeImportDate(row.actualEndDate);
row.planStartDate = normalizeImportDate(row.planStartDate);
row.planEndDate = normalizeImportDate(row.planEndDate);
const duplicateKey = JSON.stringify(Object.keys(keyMap).map(key => row[key]));
count.set(duplicateKey, (count.get(duplicateKey) || 0) + 1);
row._duplicateKey = duplicateKey;
@@ -1143,6 +1223,9 @@ const normalizeImportRow = row => ({
otherFeeTotal: normalizeOptionalImportNumber(
firstNotEmpty(row.otherFeeTotal, row['其他费用合计'])
),
unitPrice: normalizeOptionalImportNumber(firstNotEmpty(row.unitPrice, row['单价'])),
freight: normalizeOptionalImportNumber(firstNotEmpty(row.freight, row['运费'])),
freightTotal: normalizeOptionalImportNumber(firstNotEmpty(row.freightTotal, row['运费合计'])),
transportType: firstNotEmpty(
row.transportType,
row['运输类型'],
@@ -4,10 +4,12 @@
'waybill-manage-page',
{
'waybill-manage-page--form-page': isStandaloneWaybillFormPage,
'waybill-manage-page--detail-page': isStandaloneWaybillDetailPage,
},
]"
>
<component
v-if="!isStandaloneWaybillDetailPage"
:is="crudContainer"
:option="pageFormOption"
:form-page-title="isStandaloneWaybillFormPage ? formPageTitle : undefined"
@@ -381,7 +383,7 @@
>
<span>货物信息</span>
<div v-if="!dialogReadonly" class="waybill-manage-page__section-actions">
<el-link type="primary" @click="cargoImportBox = true">导入货物</el-link>
<!-- <el-link type="primary" @click="cargoImportBox = true">导入货物</el-link> -->
<el-link type="primary" @click="openCommonCargoDialog">常用货物</el-link>
</div>
</div>
@@ -1514,7 +1516,7 @@
v-model="attachmentRows"
:readonly="dialogReadonly"
:file-types="attachmentFileTypes"
:max-size="500"
:max-size="50"
:show-file-list="false"
button-text="上传附件"
@change="handleAttachmentChange"
@@ -1653,24 +1655,29 @@
</el-dialog>
<empty-pagination
v-show="!isStandaloneWaybillFormPage"
v-show="!isStandaloneWaybillFormPage && !isStandaloneWaybillDetailPage"
:page="page"
@size-change="sizeChange"
@current-change="currentChange"
@load="onLoad(page, query)"
/>
<el-dialog
<component
:is="detailContainer"
v-model="detailBox"
:title="`${config.title}详情`"
append-to-body
:append-to-body="!isStandaloneWaybillDetailPage"
top="10px"
width="96%"
show-close
:class="['waybill-manage-page__detail-dialog', $attrs.option?.dialogCustomClass]"
:class="[
'waybill-manage-page__detail-dialog',
{ 'waybill-manage-page__detail-page': isStandaloneWaybillDetailPage },
$attrs.option?.dialogCustomClass,
]"
>
<div v-loading="detailLoading" class="waybill-manage-page__detail-content">
<template v-if="detailBox">
<template v-if="detailBox || isStandaloneWaybillDetailPage">
<section-card class="waybill-manage-page__waybill-detail-summary">
<div class="waybill-manage-page__waybill-heading">
<strong>运单详情</strong>
@@ -1775,7 +1782,6 @@
transportCargoRows.length ? transportCargoRows : parseJsonArray(detailRow.goodsJson)
"
border
height="220"
>
<el-table-column type="index" label="序号" width="70" />
<el-table-column prop="cargoName" label="货物名称" min-width="150" />
@@ -1982,10 +1988,13 @@
</div>
</template>
</div>
<template #footer>
<el-button type="primary" @click="detailBox = false">关闭</el-button>
<div v-if="isStandaloneWaybillDetailPage" class="waybill-manage-page__detail-footer">
<el-button type="primary" @click="closeDetail">关闭</el-button>
</div>
<template v-if="!isStandaloneWaybillDetailPage" #footer>
<el-button type="primary" @click="closeDetail">关闭</el-button>
</template>
</el-dialog>
</component>
<el-dialog
v-model="attachmentDocumentPreviewVisible"
@@ -2527,7 +2536,10 @@
搜索
</el-button>
</div>
<div ref="transportAmap" class="waybill-manage-page__map"></div>
<div class="map-picker-content">
<map-search-results :results="transportMapSearchResults" @select="selectTransportMapSearchResult" />
<div ref="transportAmap" class="waybill-manage-page__map"></div>
</div>
<div class="waybill-manage-page__map-info">
<span>{{ transportMapStatus }}</span>
<span v-if="transportMapSelected.regionName"
@@ -2815,6 +2827,7 @@ const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
let amapLoader;
const standaloneWaybillFormRoute = '/business/waybill-manage/form';
const standaloneWaybillFormPaths = ['/business/waybill-manage', standaloneWaybillFormRoute];
const standaloneWaybillDetailRoute = '/business/waybill-manage/detail';
const loadingCreateWaybillsStorageKey = 'loading-manage-create-waybills';
const attachmentViewerPlugins = [
@@ -2870,6 +2883,16 @@ const PageAvueForm = {
},
};
const PageDetail = {
inheritAttrs: false,
render() {
return h('div', { class: this.$attrs.class }, [
...(this.$slots.default?.() || []),
...(this.$slots.footer?.() || []),
]);
},
};
const estimateMenuButtonCount = config => {
const rowActions = (config.actions || []).filter(action => action !== 'batchComplete');
return 3 + rowActions.length + (config.operations || []).length;
@@ -2905,7 +2928,7 @@ const defaultTransportCargo = () => ({
remark: '',
});
const taskCarrierTypes = ['承运商', '自运', '网货平台'];
const taskCarrierTypes = ['承运商', '自运'];
// Avue 默认分组使用折叠容器。独立运单表单需要始终展示各业务分区,
// 因此仅替换本页面的分组渲染器,保留 avue-form 的字段渲染和校验能力。
@@ -2955,6 +2978,7 @@ const getPlainAvueForm = avueForm => {
export default {
components: {
PageAvueForm,
PageDetail,
InfoFilled,
Rank,
ElImageViewer,
@@ -2991,6 +3015,10 @@ export default {
type: Boolean,
default: false,
},
standaloneDetailPage: {
type: Boolean,
default: false,
},
},
data() {
return {
@@ -3144,6 +3172,7 @@ export default {
transportMapKeyword: '',
transportMapStatus: '可搜索地址或点击地图选点',
transportMapSelected: {},
transportMapSearchResults: [],
transportAmap: null,
transportAmapMarker: null,
transportAmapGeocoder: null,
@@ -3250,6 +3279,12 @@ export default {
isStandaloneWaybillFormPage() {
return this.isStandaloneWaybillPage && ['add', 'edit'].includes(this.$route.query.mode);
},
isStandaloneWaybillDetailPage() {
return this.standaloneDetailPage && this.$route.path === standaloneWaybillDetailRoute;
},
detailContainer() {
return this.isStandaloneWaybillDetailPage ? 'PageDetail' : 'el-dialog';
},
formPageTitle() {
if (this.isStandaloneWaybillFormPage) {
return (
@@ -3338,7 +3373,7 @@ export default {
return this.taskCarrierTypes;
},
taskCarrierLabel() {
return this.form.carrierType === '网货平台' ? '承运方' : '承运商';
return '承运商';
},
taskCarrierValue: {
get() {
@@ -3533,10 +3568,16 @@ export default {
if (!this.isStandaloneWaybillFormPage) return;
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
const id = this.$route.query.id || '';
const key = `${mode}:${id}`;
const copyId = this.$route.query.copyId || '';
const key = `${mode}:${id}:${copyId}`;
if (this.standaloneFormKey === key) return;
this.standaloneFormKey = key;
if (mode === 'edit') this.form = { id };
if (mode === 'edit') {
this.form = { id };
} else if (mode === 'add' && copyId) {
// 复制模式:加载原数据
this.form = { id: copyId };
}
this.beforeOpen(() => {}, mode);
},
buildFormGroupOption(option) {
@@ -4034,7 +4075,8 @@ export default {
waybillUnitPrice(row = {}) {
const price = row.unitPrice || row.price || '';
const unit = row.priceUnit || row.billingUnit || '';
return price === '' ? '' : unit ? `${price}(${unit})` : price;
if (price === '' || Number(price) === -1) return '';
return unit ? `${price}(${unit})` : price;
},
waybillCurrencyRemark(row = {}) {
const freight = this.parseJsonObject(row.freightJson);
@@ -4066,6 +4108,10 @@ export default {
return { value: `${this.waybillCurrencyRemark(row)}${value}` };
},
openDetail(row) {
if (!this.isStandaloneWaybillDetailPage) {
this.$router.push({ path: standaloneWaybillDetailRoute, query: { id: row.id } });
return;
}
this.detailBox = true;
this.detailLoading = true;
this.detailRow = { ...row };
@@ -4088,6 +4134,8 @@ export default {
this.detailRow = {
...detail,
contractNo: contract.contractNo || detail.contractNo || detail.contractName || '',
customerName:
contract.partyA || contract.customerName || detail.customerName || '',
};
this.applyFormDetail(this.detailRow);
return this.loadWaybillDetailProcessNodes(this.detailRow.projectId);
@@ -4108,6 +4156,14 @@ export default {
this.detailLoading = false;
});
},
closeDetail() {
if (this.isStandaloneWaybillDetailPage) {
this.$router.$avueRouter?.closeTag?.();
this.$router.push({ path: '/business/waybill-manage', query: {} });
return;
}
this.detailBox = false;
},
async openLoadingDetail(row = {}) {
let id = row.loadingId || row.loadingManageId || '';
if (!id && row.loadingNo) {
@@ -4744,6 +4800,30 @@ export default {
beforeOpen(done, type) {
this.crudDialogType = type;
this.dialogReadonly = type === 'view';
const copyId = this.$route.query.copyId || '';
// 复制模式:先加载原数据,然后清除 ID 相关字段
if (type === 'add' && copyId) {
this.api.getDetail(copyId).then(res => {
const sourceData = res.data.data || {};
// 清除不应该复制的字段
delete sourceData.id;
delete sourceData.code;
delete sourceData.waybillStatus;
delete sourceData.createTime;
delete sourceData.updateTime;
delete sourceData.createUser;
delete sourceData.updateUser;
delete sourceData.createDept;
this.applyFormDetail(sourceData);
// 标记为手工创建
this.form.dataSource = '手工创建';
done();
});
return;
}
if (type === 'add') {
this.form = {
...(this.config.defaultForm || {}),
@@ -4998,7 +5078,11 @@ export default {
this.form.settlementCurrency =
contract.settlementCurrency || contract.settlementCurrencyCode || contract.currency || '';
this.form.customerName =
contract.customerName || contract.customerNames || this.form.customerName || '';
contract.partyA ||
contract.customerName ||
contract.customerNames ||
this.form.customerName ||
'';
this.syncTaskFreightCurrencyFromContract(contract);
this.fillSelfOperatedCarrierFromContract(contract);
this.$refs.crud?.validateField?.('contractName');
@@ -5409,7 +5493,7 @@ export default {
this.form.carrierId = '';
},
isTaskCarrierRequired(carrierType) {
return ['承运商', '网货平台'].includes(String(carrierType || '').trim());
return String(carrierType || '').trim() === '承运商';
},
taskCarrierOptionValue(item = {}) {
if (this.form.carrierType !== '自运') {
@@ -5986,7 +6070,7 @@ export default {
['vehicleNo', this.transportVehicleNoLabel],
];
if (this.isTaskCarrierRequired(row.carrierType)) {
requiredFields.push(['carrierName', row.carrierType === '网货平台' ? '承运方' : '承运商']);
requiredFields.push(['carrierName', '承运商']);
}
if (row.carrierType === '承运商') {
requiredFields.push(['carrierContractId', '承运商合同']);
@@ -6805,6 +6889,12 @@ 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,
}));
const point = this.resolveMapPoint(result);
if (!point) {
this.transportMapStatus = '未找到匹配地址';
@@ -6825,6 +6915,11 @@ export default {
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
});
},
selectTransportMapSearchResult(item) {
if (item && item.location) {
this.pickTransportMapPoint(item.location, item.address || item.name || '');
}
},
pickTransportMapPoint(lnglat, keyword) {
const longitude = this.getPointLng(lnglat);
const latitude = this.getPointLat(lnglat);
@@ -7559,6 +7654,18 @@ export default {
openImportDialog(this, this.config.title, () => this.onLoad(this.page, this.query));
},
handleCopy(row) {
if (this.isStandaloneWaybillPage) {
this.$router.push({
path: standaloneWaybillFormRoute,
query: {
mode: 'add',
copyId: row.id,
name: `复制${this.config.title || ''}`,
},
});
return;
}
// 非独立页面模式,使用原有的弹窗复制逻辑
this.$confirm(`确定复制该${this.config.title}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
@@ -8629,6 +8736,17 @@ export default {
}
}
&__detail-page {
padding: 12px;
background: #f5f6fa;
}
&__detail-footer {
display: flex;
justify-content: flex-end;
padding-top: 12px;
}
&__waybill-detail-summary {
:deep(.el-card__body) {
padding: 18px 22px;
@@ -8835,10 +8953,6 @@ export default {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
.section-card {
min-height: 300px;
}
}
&__waybill-map-empty {