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
@@ -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;