✨ 完善运单导入编辑回填、分批校验与运费录入
导入编辑进入独立页面并回填数据,确认导入按每批100条校验后统一导出失败明细,完整录入运费信息按数量单位联动单价单位。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -22,6 +22,18 @@ export const getVoucherImages = (waybillId, params = {}) =>
|
|||||||
...params,
|
...params,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
export const getVoucherFolders = waybillId =>
|
||||||
|
request({
|
||||||
|
url: '/blade-transport/process-config/voucher-folders',
|
||||||
|
method: 'get',
|
||||||
|
params: { waybillId },
|
||||||
|
});
|
||||||
|
export const getVoucherFolderDetail = (voucherId, plateNo) =>
|
||||||
|
request({
|
||||||
|
url: '/blade-transport/process-config/voucher-folder-detail',
|
||||||
|
method: 'get',
|
||||||
|
params: { voucherId, plateNo },
|
||||||
|
});
|
||||||
export const submit = api.submit;
|
export const submit = api.submit;
|
||||||
export const remove = api.remove;
|
export const remove = api.remove;
|
||||||
export const copy = api.copy;
|
export const copy = api.copy;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const loadingManageConfig = {
|
|||||||
exportColumns: [
|
exportColumns: [
|
||||||
{ prop: 'loadingNo', label: '配载单号' },
|
{ prop: 'loadingNo', label: '配载单号' },
|
||||||
{ prop: 'loadingSubNos', label: '配载子单号' },
|
{ prop: 'loadingSubNos', label: '配载子单号' },
|
||||||
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' },
|
{ prop: 'vehicleNo', label: '车牌号/船班号/船次号/班次号' },
|
||||||
{ prop: 'driverName', label: '司机' },
|
{ prop: 'driverName', label: '司机' },
|
||||||
{ prop: 'driverPhone', label: '联系电话' },
|
{ prop: 'driverPhone', label: '联系电话' },
|
||||||
{ prop: 'carrierType', label: '承运类型' },
|
{ prop: 'carrierType', label: '承运类型' },
|
||||||
|
|||||||
@@ -565,10 +565,10 @@ export const option = {
|
|||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '车牌号/航班号/船号/班列号',
|
label: '车牌号/船班号/船次号/班次号',
|
||||||
prop: 'vehicleNo',
|
prop: 'vehicleNo',
|
||||||
search: true,
|
search: true,
|
||||||
searchLabel: '车/船/航班/班列',
|
searchLabel: '车牌号/船班号/船次号/班次号',
|
||||||
searchOrder: 13,
|
searchOrder: 13,
|
||||||
minWidth: 210,
|
minWidth: 210,
|
||||||
display: false,
|
display: false,
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export const generateWaybillColumns = [
|
|||||||
{ label: '运单号', prop: 'waybillNo', minWidth: 160, link: true },
|
{ label: '运单号', prop: 'waybillNo', minWidth: 160, link: true },
|
||||||
{ label: '项目名称', prop: 'projectName', minWidth: 160 },
|
{ label: '项目名称', prop: 'projectName', minWidth: 160 },
|
||||||
{ label: '客户/委托方', prop: 'customerName', minWidth: 160 },
|
{ label: '客户/委托方', prop: 'customerName', minWidth: 160 },
|
||||||
{ label: '车牌号/航班号/船号/班列号', prop: 'vehicleNo', minWidth: 190 },
|
{ label: '车牌号/船班号/船次号/班次号', prop: 'vehicleNo', minWidth: 190 },
|
||||||
{ label: '司机', prop: 'driverName', minWidth: 120 },
|
{ label: '司机', prop: 'driverName', minWidth: 120 },
|
||||||
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
|
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
|
||||||
{ label: '运输类型', prop: 'transportType', minWidth: 130 },
|
{ label: '运输类型', prop: 'transportType', minWidth: 130 },
|
||||||
|
|||||||
@@ -2423,7 +2423,7 @@
|
|||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="vehicleNo" label="车牌号/航班号/船号/班列号" width="200" />
|
<el-table-column prop="vehicleNo" label="车牌号/船班号/船次号/班次号" width="200" />
|
||||||
<el-table-column prop="driverName" label="司机" width="128" />
|
<el-table-column prop="driverName" label="司机" width="128" />
|
||||||
<el-table-column prop="carrierName" label="承运商" width="192" />
|
<el-table-column prop="carrierName" label="承运商" width="192" />
|
||||||
<el-table-column label="运输方式" width="192">
|
<el-table-column label="运输方式" width="192">
|
||||||
@@ -3514,7 +3514,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
prop="vehicleNo"
|
prop="vehicleNo"
|
||||||
label="车牌号/航班号/船号/班列号"
|
label="车牌号/船班号/船次号/班次号"
|
||||||
min-width="200"
|
min-width="200"
|
||||||
align="center"
|
align="center"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
@@ -4169,7 +4169,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item
|
<el-form-item
|
||||||
v-if="dispatchItemForm.carrierType"
|
v-if="dispatchItemForm.carrierType"
|
||||||
:label="dispatchIsNonRoadTransport ? '船/航/班列号' : '车牌号'"
|
:label="dispatchIsNonRoadTransport ? '船班号/船次号/班次号' : '车牌号'"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<el-input v-model="dispatchItemForm.vehicleNo" placeholder="请输入" />
|
<el-input v-model="dispatchItemForm.vehicleNo" placeholder="请输入" />
|
||||||
@@ -6075,7 +6075,7 @@ export default {
|
|||||||
return this.config.permission === 'waybill_manage' && !this.isRoadTransport;
|
return this.config.permission === 'waybill_manage' && !this.isRoadTransport;
|
||||||
},
|
},
|
||||||
transportVehicleNoLabel() {
|
transportVehicleNoLabel() {
|
||||||
return this.isRoadTransport ? '车牌号' : '船/航/班列号';
|
return this.isRoadTransport ? '车牌号' : '船班号/船次号/班次号';
|
||||||
},
|
},
|
||||||
transportPlanImportDuplicateCount() {
|
transportPlanImportDuplicateCount() {
|
||||||
return this.transportPlanImportRows.filter(row => row._duplicate).length;
|
return this.transportPlanImportRows.filter(row => row._duplicate).length;
|
||||||
@@ -7121,7 +7121,7 @@ export default {
|
|||||||
return String(row.carrierTypeName || row.carrierType || '').trim() === '承运商';
|
return String(row.carrierTypeName || row.carrierType || '').trim() === '承运商';
|
||||||
},
|
},
|
||||||
waybillVehicleNoLabel(row = {}) {
|
waybillVehicleNoLabel(row = {}) {
|
||||||
return this.waybillIsRoadTransport(row) ? '车牌号' : '船/航/班列号';
|
return this.waybillIsRoadTransport(row) ? '车牌号' : '船班号/船次号/班次号';
|
||||||
},
|
},
|
||||||
waybillUnitPrice(row = {}) {
|
waybillUnitPrice(row = {}) {
|
||||||
const price = row.unitPrice || row.price || '';
|
const price = row.unitPrice || row.price || '';
|
||||||
@@ -9491,7 +9491,7 @@ export default {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const invalidLengthField = [
|
const invalidLengthField = [
|
||||||
['vehicleNo', '船/航/班列号', 30],
|
['vehicleNo', '船班号/船次号/班次号', 30],
|
||||||
['captainName', '船长', 20],
|
['captainName', '船长', 20],
|
||||||
['cabinNo', '舱位', 30],
|
['cabinNo', '舱位', 30],
|
||||||
['containerNo', '箱号', 30],
|
['containerNo', '箱号', 30],
|
||||||
@@ -13278,7 +13278,7 @@ export default {
|
|||||||
[
|
[
|
||||||
'vehicleNo',
|
'vehicleNo',
|
||||||
['water', 'rail', 'air'].includes(this.resolveTransportMode(row.transportType))
|
['water', 'rail', 'air'].includes(this.resolveTransportMode(row.transportType))
|
||||||
? '船/航/班列号'
|
? '船班号/船次号/班次号'
|
||||||
: '车牌号',
|
: '车牌号',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
@@ -13358,7 +13358,7 @@ export default {
|
|||||||
['escortPhone', '押运人手机号', 20],
|
['escortPhone', '押运人手机号', 20],
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
['vehicleNo', '船/航/班列号', 30],
|
['vehicleNo', '船班号/船次号/班次号', 30],
|
||||||
['captainName', '船长', 20],
|
['captainName', '船长', 20],
|
||||||
['cabinNo', '舱位', 30],
|
['cabinNo', '舱位', 30],
|
||||||
['containerNo', '箱号', 30],
|
['containerNo', '箱号', 30],
|
||||||
|
|||||||
@@ -248,7 +248,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<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-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><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="船班号/船次号/班次号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="船长"><el-input v-model="route.captainName" placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="船长"><el-input v-model="route.captainName" placeholder="请输入" /></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="联系电话"><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="联系电话"><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="箱号"><el-input v-model="route.containerNo" placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="箱号"><el-input v-model="route.containerNo" placeholder="请输入" /></el-form-item></el-col>
|
||||||
|
|||||||
@@ -756,7 +756,7 @@
|
|||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="vehicleNo" label="车牌号/航班号/船号/班列号" width="200" />
|
<el-table-column prop="vehicleNo" label="车牌号/船班号/船次号/班次号" width="200" />
|
||||||
<el-table-column prop="driverName" label="司机" width="128" />
|
<el-table-column prop="driverName" label="司机" width="128" />
|
||||||
<el-table-column prop="carrierName" label="承运商" width="192" />
|
<el-table-column prop="carrierName" label="承运商" width="192" />
|
||||||
<el-table-column label="运输方式" width="192">
|
<el-table-column label="运输方式" width="192">
|
||||||
@@ -1401,7 +1401,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
prop="vehicleNo"
|
prop="vehicleNo"
|
||||||
label="车牌号/航班号/船号/班列号"
|
label="车牌号/船班号/船次号/班次号"
|
||||||
min-width="200"
|
min-width="200"
|
||||||
align="center"
|
align="center"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
@@ -2097,7 +2097,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item
|
<el-form-item
|
||||||
v-if="dispatchItemForm.carrierType"
|
v-if="dispatchItemForm.carrierType"
|
||||||
:label="dispatchIsNonRoadTransport ? '船/航/班列号' : '车牌号'"
|
:label="dispatchIsNonRoadTransport ? '船班号/船次号/班次号' : '车牌号'"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<el-input v-model="dispatchItemForm.vehicleNo" placeholder="请输入" />
|
<el-input v-model="dispatchItemForm.vehicleNo" placeholder="请输入" />
|
||||||
@@ -8123,7 +8123,7 @@ export default {
|
|||||||
[
|
[
|
||||||
'vehicleNo',
|
'vehicleNo',
|
||||||
['water', 'rail', 'air'].includes(this.resolveTransportMode(row.transportType))
|
['water', 'rail', 'air'].includes(this.resolveTransportMode(row.transportType))
|
||||||
? '船/航/班列号'
|
? '船班号/船次号/班次号'
|
||||||
: '车牌号',
|
: '车牌号',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
@@ -8204,7 +8204,7 @@ export default {
|
|||||||
['escortPhone', '押运人手机号', 20],
|
['escortPhone', '押运人手机号', 20],
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
['vehicleNo', '船/航/班列号', 30],
|
['vehicleNo', '船班号/船次号/班次号', 30],
|
||||||
['captainName', '船长', 20],
|
['captainName', '船长', 20],
|
||||||
['cabinNo', '舱位', 30],
|
['cabinNo', '舱位', 30],
|
||||||
['containerNo', '箱号', 30],
|
['containerNo', '箱号', 30],
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
<template>
|
||||||
|
<div class="voucher-folder-page voucher-folder-page--embedded">
|
||||||
|
<section class="voucher-folder-page__search">
|
||||||
|
<el-form :model="query" label-position="right" label-width="160px">
|
||||||
|
<div class="voucher-folder-page__search-grid">
|
||||||
|
<el-form-item label="车牌号">
|
||||||
|
<el-input v-model="query.plateNo" clearable placeholder="请输入" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否关联运单">
|
||||||
|
<el-select v-model="query.matched" clearable placeholder="全部">
|
||||||
|
<el-option label="是" :value="1" />
|
||||||
|
<el-option label="否" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<div class="voucher-folder-page__search-actions">
|
||||||
|
<el-button type="primary" @click="search">查询</el-button>
|
||||||
|
<el-button @click="reset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</section>
|
||||||
|
<section class="voucher-folder-page__table-wrap">
|
||||||
|
<div class="voucher-folder-page__title">{{ batchTitle }}</div>
|
||||||
|
<el-table v-loading="loading" :data="pageRows" border class="voucher-folder-page__table">
|
||||||
|
<el-table-column type="index" label="序号" width="70" :index="rowIndex" />
|
||||||
|
<el-table-column prop="voucherNo" label="执行凭证号" min-width="220">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link type="primary" @click="openViewer(row)">{{ row.voucherNo }}</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="plateNo" label="车牌号" min-width="150" />
|
||||||
|
<el-table-column prop="folderName" label="文件夹名称" min-width="160" />
|
||||||
|
<el-table-column prop="matched" label="是否关联运单" width="130">
|
||||||
|
<template #default="{ row }">{{ row.matched === 1 ? '是' : '否' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="processStatus" label="处理状态" width="130" />
|
||||||
|
<el-table-column prop="createTime" label="创建时间" min-width="180" />
|
||||||
|
<el-table-column prop="updateTime" label="更新时间" min-width="180" />
|
||||||
|
<el-table-column label="操作" width="120" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="voucher-folder-page__actions">
|
||||||
|
<el-link type="primary" @click="openViewer(row)">查看</el-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="voucher-folder-page__pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="page.current"
|
||||||
|
v-model:page-size="page.size"
|
||||||
|
:total="filteredRows.length"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, prev, pager, next, sizes"
|
||||||
|
@size-change="page.current = 1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<el-dialog v-model="viewerVisible" title="执行凭证详情" width="960px" destroy-on-close>
|
||||||
|
<div v-loading="viewerLoading" class="voucher-folder-page__viewer">
|
||||||
|
<div class="voucher-folder-page__viewer-info">
|
||||||
|
<span>执行凭证号:{{ viewer.voucherNo || '-' }}</span>
|
||||||
|
<span>文件夹名称:{{ viewer.folderName || '-' }}</span>
|
||||||
|
<span>是否关联运单:{{ viewer.matched === 1 ? '是' : '否' }}</span>
|
||||||
|
<span>
|
||||||
|
配载单号:
|
||||||
|
<el-link v-if="viewer.loadingNo" type="primary" @click="goToLoading(viewer.loadingNo)">
|
||||||
|
{{ viewer.loadingNo }}
|
||||||
|
</el-link>
|
||||||
|
<template v-else>-</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="viewerFiles.length" class="voucher-folder-page__images">
|
||||||
|
<el-image
|
||||||
|
v-for="(file, index) in viewerFiles"
|
||||||
|
:key="file.id || file.url || index"
|
||||||
|
:src="file.url"
|
||||||
|
:preview-src-list="viewerFiles.map(item => item.url).filter(Boolean)"
|
||||||
|
:initial-index="index"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else description="暂无凭证文件" />
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { getVoucherFolderDetail, getVoucherFolders } from '@/api/business/process-config';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
waybillId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const loading = ref(false);
|
||||||
|
const rows = ref([]);
|
||||||
|
const page = reactive({ current: 1, size: 10 });
|
||||||
|
const query = reactive({ plateNo: '', matched: undefined });
|
||||||
|
const appliedQuery = reactive({ plateNo: '', matched: undefined });
|
||||||
|
const viewerVisible = ref(false);
|
||||||
|
const viewerLoading = ref(false);
|
||||||
|
const viewer = ref({});
|
||||||
|
const viewerFiles = ref([]);
|
||||||
|
|
||||||
|
const batchTitle = computed(() => {
|
||||||
|
const batchNos = [...new Set(rows.value.map(item => item.voucherBatchNo).filter(Boolean))];
|
||||||
|
return batchNos.length === 1 ? batchNos[0] : '执行凭证详情';
|
||||||
|
});
|
||||||
|
const filteredRows = computed(() => {
|
||||||
|
const plateNo = String(appliedQuery.plateNo || '').trim();
|
||||||
|
return rows.value.filter(item => {
|
||||||
|
const plateMatched = !plateNo || String(item.plateNo || '').includes(plateNo);
|
||||||
|
const relatedMatched =
|
||||||
|
appliedQuery.matched === undefined || appliedQuery.matched === null || appliedQuery.matched === ''
|
||||||
|
? true
|
||||||
|
: item.matched === appliedQuery.matched;
|
||||||
|
return plateMatched && relatedMatched;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const pageRows = computed(() => {
|
||||||
|
const start = (page.current - 1) * page.size;
|
||||||
|
return filteredRows.value.slice(start, start + page.size);
|
||||||
|
});
|
||||||
|
const rowIndex = index => (page.current - 1) * page.size + index + 1;
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
if (!props.waybillId) {
|
||||||
|
rows.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getVoucherFolders(props.waybillId);
|
||||||
|
rows.value = res.data?.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
rows.value = [];
|
||||||
|
ElMessage.error(error.message || '获取执行凭证失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const search = () => {
|
||||||
|
appliedQuery.plateNo = query.plateNo;
|
||||||
|
appliedQuery.matched = query.matched;
|
||||||
|
page.current = 1;
|
||||||
|
};
|
||||||
|
const reset = () => {
|
||||||
|
query.plateNo = '';
|
||||||
|
query.matched = undefined;
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
const openViewer = async row => {
|
||||||
|
viewerVisible.value = true;
|
||||||
|
viewerLoading.value = true;
|
||||||
|
viewer.value = row;
|
||||||
|
viewerFiles.value = [];
|
||||||
|
try {
|
||||||
|
const res = await getVoucherFolderDetail(row.voucherId, row.plateNo);
|
||||||
|
viewer.value = res.data?.data || row;
|
||||||
|
viewerFiles.value = viewer.value.files || [];
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.message || '获取凭证详情失败');
|
||||||
|
} finally {
|
||||||
|
viewerLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const goToLoading = loadingNo => {
|
||||||
|
router.push({ path: '/business/loading-manage', query: { loadingNo } });
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.waybillId,
|
||||||
|
() => {
|
||||||
|
page.current = 1;
|
||||||
|
load();
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.voucher-folder-page {
|
||||||
|
&__search {
|
||||||
|
padding: 12px 12px 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
&__search-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 360px 360px 1fr;
|
||||||
|
gap: 8px 24px;
|
||||||
|
}
|
||||||
|
&__search-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
&__table-wrap {
|
||||||
|
padding: 12px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
&__title {
|
||||||
|
padding-bottom: 12px;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
&__pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
&__viewer {
|
||||||
|
min-height: 360px;
|
||||||
|
}
|
||||||
|
&__viewer-info {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 28px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
&__images {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 140px);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
&__images :deep(.el-image) {
|
||||||
|
width: 140px;
|
||||||
|
height: 100px;
|
||||||
|
border: 6px solid #cfcfcf;
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
:deep(.el-table) {
|
||||||
|
--el-table-border-color: #eff1f7;
|
||||||
|
}
|
||||||
|
:deep(.el-table__body tr:nth-child(even) > td) {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
:deep(.el-input),
|
||||||
|
:deep(.el-select) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.voucher-folder-page__search-grid {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
.voucher-folder-page__search-actions {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
<section-card class="waybill-search">
|
<section-card class="waybill-search">
|
||||||
<el-form :inline="true" :model="detailQuery"
|
<el-form :inline="true" :model="detailQuery"
|
||||||
>
|
>
|
||||||
<el-form-item label="车牌号/船号"
|
<el-form-item label="车牌号/船班号/船次号/班次号"
|
||||||
><el-input v-model="detailQuery.vehicleNo" clearable /></el-form-item
|
><el-input v-model="detailQuery.vehicleNo" clearable /></el-form-item
|
||||||
><el-form-item label="司机/船长姓名"
|
><el-form-item label="司机/船长姓名"
|
||||||
><el-input v-model="detailQuery.driverName" clearable placeholder="请输入" /></el-form-item
|
><el-input v-model="detailQuery.driverName" clearable placeholder="请输入" /></el-form-item
|
||||||
@@ -219,12 +219,14 @@
|
|||||||
prop="loadingIdentifier"
|
prop="loadingIdentifier"
|
||||||
label="配载标识号"
|
label="配载标识号"
|
||||||
min-width="130"
|
min-width="130"
|
||||||
/><el-table-column
|
>
|
||||||
|
<template #default="{ row }">{{ row.loadingIdentifier || row.loadingNo || '' }}</template>
|
||||||
|
</el-table-column><el-table-column
|
||||||
prop="vehicleNo"
|
prop="vehicleNo"
|
||||||
label="车牌号/航班号/船号/班列号"
|
label="车牌号/船班号/船次号/班次号"
|
||||||
min-width="190"
|
min-width="190"
|
||||||
/><el-table-column prop="transportType" label="运输方式" width="110">
|
/><el-table-column prop="transportType" label="运输方式" width="110">
|
||||||
<template #default="{ row }">{{ transportTypeLabel(row.transportType) }}</template>
|
<template #default="{ row }">{{ transportTypeLabel(row) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
prop="driverName"
|
prop="driverName"
|
||||||
@@ -334,6 +336,7 @@
|
|||||||
label-width="auto"
|
label-width="auto"
|
||||||
class="archive-form"
|
class="archive-form"
|
||||||
>
|
>
|
||||||
|
<div class="dialog-section-title">项目信息</div>
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="项目" prop="projectId">
|
<el-form-item label="项目" prop="projectId">
|
||||||
@@ -444,16 +447,21 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
<div class="dialog-section-title waybill-import-create__import-title">导入运单明细</div>
|
||||||
|
<p v-if="!projectInfoReady" class="waybill-import-create__import-hint">
|
||||||
|
请先选择项目、客户合同和承运信息,再导入运单明细
|
||||||
|
</p>
|
||||||
<el-form-item label="运单明细表" prop="file"
|
<el-form-item label="运单明细表" prop="file"
|
||||||
><el-upload
|
><el-upload
|
||||||
action="#"
|
action="#"
|
||||||
accept=".xls,.xlsx"
|
accept=".xls,.xlsx"
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
:limit="1"
|
:limit="1"
|
||||||
|
:disabled="!projectInfoReady"
|
||||||
:file-list="files"
|
:file-list="files"
|
||||||
:on-change="fileChange"
|
:on-change="fileChange"
|
||||||
:on-remove="fileRemove"
|
:on-remove="fileRemove"
|
||||||
><el-button type="primary">添加附件</el-button></el-upload
|
><el-button type="primary" :disabled="!projectInfoReady">导入</el-button></el-upload
|
||||||
><span class="waybill-import-create__file-tip">请上传运单明细表,仅支持 excel 格式;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法</span
|
><span class="waybill-import-create__file-tip">请上传运单明细表,仅支持 excel 格式;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法</span
|
||||||
><el-link type="primary" @click="downloadTemplate">下载模板</el-link></el-form-item
|
><el-link type="primary" @click="downloadTemplate">下载模板</el-link></el-form-item
|
||||||
>
|
>
|
||||||
@@ -479,6 +487,7 @@
|
|||||||
:data="previewRows"
|
:data="previewRows"
|
||||||
border
|
border
|
||||||
max-height="440"
|
max-height="440"
|
||||||
|
:row-class-name="({ row }) => (row._duplicate ? 'is-duplicate-row' : '')"
|
||||||
@selection-change="rowSelection = $event"
|
@selection-change="rowSelection = $event"
|
||||||
><el-table-column type="selection" width="55" /><el-table-column
|
><el-table-column type="selection" width="55" /><el-table-column
|
||||||
type="index"
|
type="index"
|
||||||
@@ -564,14 +573,18 @@
|
|||||||
/><span v-else>{{ formatCell(row, column) }}</span></template
|
/><span v-else>{{ formatCell(row, column) }}</span></template
|
||||||
><span v-else>{{ formatCell(row, column) }}</span></template
|
><span v-else>{{ formatCell(row, column) }}</span></template
|
||||||
></el-table-column
|
></el-table-column
|
||||||
><el-table-column label="操作" width="180" fixed="right"
|
><el-table-column label="操作" width="220" fixed="right"
|
||||||
><template #default="{ row }"
|
><template #default="{ row }"
|
||||||
><template v-if="row._editing"
|
><div class="waybill-import-create__row-actions"
|
||||||
><el-link type="primary" @click="saveRow(row)">保存</el-link
|
><template v-if="row._editing"
|
||||||
><el-link type="primary" @click="cancelRow(row)">取消</el-link></template
|
><el-link type="primary" @click="saveRow(row)">保存</el-link
|
||||||
><template v-else
|
><el-link type="primary" @click="cancelRow(row)">取消</el-link></template
|
||||||
><el-link type="primary" @click="editRow(row)">编辑</el-link
|
><template v-else
|
||||||
><el-link type="danger" @click="deleteRow(row)">删除</el-link></template
|
><el-link v-if="row._duplicate" type="primary" @click="confirmDuplicateRow(row)"
|
||||||
|
>确认</el-link
|
||||||
|
><el-link type="primary" @click="editRow(row)">编辑</el-link
|
||||||
|
><el-link type="danger" @click="deleteRow(row)">删除</el-link></template
|
||||||
|
></div
|
||||||
></template
|
></template
|
||||||
></el-table-column
|
></el-table-column
|
||||||
></el-table
|
></el-table
|
||||||
@@ -579,6 +592,7 @@
|
|||||||
</section>
|
</section>
|
||||||
<div v-if="createPage" class="waybill-import-create__footer">
|
<div v-if="createPage" class="waybill-import-create__footer">
|
||||||
<el-button @click="closeCreate">取消</el-button>
|
<el-button @click="closeCreate">取消</el-button>
|
||||||
|
<el-button @click="saveDraft">保存草稿</el-button>
|
||||||
<el-button type="primary" @click="confirmImport">确认导入</el-button>
|
<el-button type="primary" @click="confirmImport">确认导入</el-button>
|
||||||
</div>
|
</div>
|
||||||
</component>
|
</component>
|
||||||
@@ -586,7 +600,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||||
@@ -601,6 +615,7 @@ import SectionCard from '@/components/section-card/main.vue';
|
|||||||
|
|
||||||
const props = defineProps({ modelValue: Boolean, standalone: Boolean, createPage: Boolean });
|
const props = defineProps({ modelValue: Boolean, standalone: Boolean, createPage: Boolean });
|
||||||
const emit = defineEmits(['update:modelValue', 'closed']);
|
const emit = defineEmits(['update:modelValue', 'closed']);
|
||||||
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const visible = computed({
|
const visible = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
@@ -656,10 +671,10 @@ const createDefaultForm = () => ({
|
|||||||
file: null,
|
file: null,
|
||||||
});
|
});
|
||||||
const form = reactive(createDefaultForm());
|
const form = reactive(createDefaultForm());
|
||||||
// 批量导入状态仅保留草稿与导入完成两种,与后端 importStatus 取值一致。
|
// 导入状态与运单业务状态一致:进行中、已完成。
|
||||||
const importStatusOptions = [
|
const importStatusOptions = [
|
||||||
{ label: '进行中', value: 'draft' },
|
{ label: '进行中', value: 'running' },
|
||||||
{ label: '导入完成', value: 'completed' },
|
{ label: '已完成', value: 'completed' },
|
||||||
];
|
];
|
||||||
// 编辑草稿时明细已入库,此时不再强制重新上传附件。
|
// 编辑草稿时明细已入库,此时不再强制重新上传附件。
|
||||||
const validateImportFile = (rule, value, callback) =>
|
const validateImportFile = (rule, value, callback) =>
|
||||||
@@ -705,6 +720,21 @@ const carrierContractsLoaded = ref(false),
|
|||||||
hasCarrierContracts = ref(null);
|
hasCarrierContracts = ref(null);
|
||||||
const carrierTypes = ['承运商', '自运'];
|
const carrierTypes = ['承运商', '自运'];
|
||||||
const isSelfOperated = computed(() => form.carrierType === '自运');
|
const isSelfOperated = computed(() => form.carrierType === '自运');
|
||||||
|
const projectInfoReady = computed(() => {
|
||||||
|
if (
|
||||||
|
!form.projectId ||
|
||||||
|
!String(form.customerName || '').trim() ||
|
||||||
|
!form.contractId ||
|
||||||
|
!form.carrierType ||
|
||||||
|
!form.status ||
|
||||||
|
!form.importType
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isSelfOperated.value
|
||||||
|
? Boolean(String(form.carrierName || '').trim())
|
||||||
|
: Boolean(form.carrierContractId);
|
||||||
|
});
|
||||||
// 项目下没有承运商合同时只允许自运,与运单管理的承运类型收窄规则一致。
|
// 项目下没有承运商合同时只允许自运,与运单管理的承运类型收窄规则一致。
|
||||||
const carrierTypeOptions = computed(() =>
|
const carrierTypeOptions = computed(() =>
|
||||||
carrierContractsLoaded.value && hasCarrierContracts.value === false ? ['自运'] : carrierTypes
|
carrierContractsLoaded.value && hasCarrierContracts.value === false ? ['自运'] : carrierTypes
|
||||||
@@ -733,7 +763,7 @@ const detailColumns = [
|
|||||||
{ prop: 'batchNo', label: '运单批次号', minWidth: 150 },
|
{ prop: 'batchNo', label: '运单批次号', minWidth: 150 },
|
||||||
{ prop: 'originalNo', label: '原始单号', editor: 'input', minWidth: 150 },
|
{ prop: 'originalNo', label: '原始单号', editor: 'input', minWidth: 150 },
|
||||||
{ prop: 'loadingIdentifier', label: '配载标识号', editor: 'input', minWidth: 150 },
|
{ prop: 'loadingIdentifier', label: '配载标识号', editor: 'input', minWidth: 150 },
|
||||||
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号', editor: 'input', minWidth: 220 },
|
{ prop: 'vehicleNo', label: '车牌号/船班号/船次号/班次号', editor: 'input', minWidth: 220 },
|
||||||
{ prop: 'transportType', label: '运输方式', editor: 'transportType', minWidth: 150 },
|
{ prop: 'transportType', label: '运输方式', editor: 'transportType', minWidth: 150 },
|
||||||
{ prop: 'driverName', label: '司机/船长姓名', editor: 'driver', minWidth: 170 },
|
{ prop: 'driverName', label: '司机/船长姓名', editor: 'driver', minWidth: 170 },
|
||||||
{ prop: 'driverPhone', label: '司机/船长手机号', editor: 'input', minWidth: 150 },
|
{ prop: 'driverPhone', label: '司机/船长手机号', editor: 'input', minWidth: 150 },
|
||||||
@@ -763,7 +793,7 @@ const detailColumns = [
|
|||||||
{ prop: 'waybillIdentifier', label: '同一运单标识号', editor: 'input', minWidth: 150 },
|
{ prop: 'waybillIdentifier', label: '同一运单标识号', editor: 'input', minWidth: 150 },
|
||||||
];
|
];
|
||||||
const requiredDetailFields = [
|
const requiredDetailFields = [
|
||||||
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' },
|
{ prop: 'vehicleNo', label: '车牌号/船班号/船次号/班次号' },
|
||||||
{ prop: 'transportType', label: '运输方式' },
|
{ prop: 'transportType', label: '运输方式' },
|
||||||
{ prop: 'departureAddress', label: '发货地址' },
|
{ prop: 'departureAddress', label: '发货地址' },
|
||||||
{ prop: 'arrivalAddress', label: '到货地址' },
|
{ prop: 'arrivalAddress', label: '到货地址' },
|
||||||
@@ -976,33 +1006,78 @@ const loadCommonCargoOptions = async row => {
|
|||||||
commonCargoLoadingMap.value = { ...commonCargoLoadingMap.value, [key]: false };
|
commonCargoLoadingMap.value = { ...commonCargoLoadingMap.value, [key]: false };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const TRANSPORT_TYPE_DEFAULTS = [
|
||||||
|
{ label: '公路运输', value: 'road', dictKey: 'road', dictValue: '公路运输' },
|
||||||
|
{ label: '铁路运输', value: 'railway', dictKey: 'railway', dictValue: '铁路运输' },
|
||||||
|
{ label: '水路运输', value: 'river', dictKey: 'river', dictValue: '水路运输' },
|
||||||
|
{ label: '航空运输', value: 'air', dictKey: 'air', dictValue: '航空运输' },
|
||||||
|
];
|
||||||
|
const readDictionaryList = res => {
|
||||||
|
const payload = res?.data?.data ?? res?.data ?? res;
|
||||||
|
if (Array.isArray(payload)) return payload;
|
||||||
|
if (Array.isArray(payload?.records)) return payload.records;
|
||||||
|
if (Array.isArray(payload?.data)) return payload.data;
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
const normalizeTransportTypeOptions = list => {
|
||||||
|
const options = [];
|
||||||
|
const append = item => {
|
||||||
|
if (!item || typeof item !== 'object') return;
|
||||||
|
const dictKey = String(item.dictKey ?? item.value ?? '').trim();
|
||||||
|
const dictValue = String(item.dictValue ?? item.label ?? '').trim();
|
||||||
|
if (dictKey === '-1') {
|
||||||
|
(item.children || []).forEach(append);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dictKey || dictValue) {
|
||||||
|
options.push({
|
||||||
|
label: dictValue || dictKey,
|
||||||
|
value: dictKey || dictValue,
|
||||||
|
dictKey,
|
||||||
|
dictValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
(item.children || []).forEach(append);
|
||||||
|
};
|
||||||
|
(list || []).forEach(append);
|
||||||
|
return options.length ? options : TRANSPORT_TYPE_DEFAULTS;
|
||||||
|
};
|
||||||
|
const applyTransportTypeOptions = res => {
|
||||||
|
transportTypeOptions.value = normalizeTransportTypeOptions(readDictionaryList(res));
|
||||||
|
};
|
||||||
const loadEditorOptions = async () => {
|
const loadEditorOptions = async () => {
|
||||||
const [dictRes, cargoTypeRes, driverRes] = await Promise.all([
|
const [dictRes, cargoTypeRes, driverRes] = await Promise.all([
|
||||||
getDictionary({ code: 'transport_type' }),
|
getDictionary({ code: 'transport_type' }),
|
||||||
getCargoTypeList(1, 9999),
|
getCargoTypeList(1, 9999),
|
||||||
getDriverList(1, 9999, {}),
|
getDriverList(1, 9999, {}),
|
||||||
]);
|
]);
|
||||||
transportTypeOptions.value = extractRecords(dictRes).map(item => ({
|
applyTransportTypeOptions(dictRes);
|
||||||
label: item.dictValue || item.label,
|
|
||||||
value: item.dictKey || item.value,
|
|
||||||
}));
|
|
||||||
cargoTypeOptions.value = buildCargoTypeTree(extractRecords(cargoTypeRes));
|
cargoTypeOptions.value = buildCargoTypeTree(extractRecords(cargoTypeRes));
|
||||||
cargoTypeFlatOptions.value = flattenCargoTypeOptions(cargoTypeOptions.value);
|
cargoTypeFlatOptions.value = flattenCargoTypeOptions(cargoTypeOptions.value);
|
||||||
driverOptions.value = extractRecords(driverRes);
|
driverOptions.value = extractRecords(driverRes);
|
||||||
};
|
};
|
||||||
const ensureTransportTypeOptions = async () => {
|
const ensureTransportTypeOptions = async () => {
|
||||||
if (transportTypeOptions.value.length) return;
|
if (transportTypeOptions.value.length) return;
|
||||||
const dictRes = await getDictionary({ code: 'transport_type' });
|
try {
|
||||||
transportTypeOptions.value = extractRecords(dictRes).map(item => ({
|
applyTransportTypeOptions(await getDictionary({ code: 'transport_type' }));
|
||||||
label: item.dictValue || item.label,
|
} catch (error) {
|
||||||
value: item.dictKey || item.value,
|
transportTypeOptions.value = TRANSPORT_TYPE_DEFAULTS;
|
||||||
}));
|
}
|
||||||
};
|
};
|
||||||
const transportTypeLabel = value => {
|
const transportTypeLabel = value => {
|
||||||
if (value === null || value === undefined || value === '') return '-';
|
const source = value && typeof value === 'object' ? value : null;
|
||||||
return (
|
const stored = source ? source.transportType : value;
|
||||||
transportTypeOptions.value.find(item => String(item.value) === String(value))?.label || value
|
const named = String(source?.transportTypeName || '').trim();
|
||||||
|
const text = String(stored ?? '').trim();
|
||||||
|
if (named && named !== text) return named;
|
||||||
|
if (!text && !named) return '-';
|
||||||
|
const target = (named || text).toLowerCase();
|
||||||
|
const matched = transportTypeOptions.value.find(item =>
|
||||||
|
[item.value, item.dictKey, item.label, item.dictValue].some(
|
||||||
|
candidate => String(candidate ?? '').trim().toLowerCase() === target
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
return matched?.dictValue || matched?.label || named || text || '-';
|
||||||
};
|
};
|
||||||
const loadBatches = async () => {
|
const loadBatches = async () => {
|
||||||
const res = await api.getImportBatches({ ...query, current: page.current, size: page.size });
|
const res = await api.getImportBatches({ ...query, current: page.current, size: page.size });
|
||||||
@@ -1011,7 +1086,7 @@ const loadBatches = async () => {
|
|||||||
};
|
};
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.standalone) loadBatches();
|
if (props.standalone) loadBatches();
|
||||||
if (props.createPage) openCreate();
|
if (props.createPage) openCreatePage();
|
||||||
});
|
});
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
@@ -1034,16 +1109,18 @@ const loadDetails = async () => {
|
|||||||
details.value = res.data?.data?.records || [];
|
details.value = res.data?.data?.records || [];
|
||||||
detailPage.total = res.data?.data?.total || 0;
|
detailPage.total = res.data?.data?.total || 0;
|
||||||
};
|
};
|
||||||
const openCreate = async () => {
|
const openCreate = () => prepareCreateForm(true);
|
||||||
|
const prepareCreateForm = async generateCode => {
|
||||||
resetCreateForm();
|
resetCreateForm();
|
||||||
createVisible.value = true;
|
createVisible.value = true;
|
||||||
const [projectRes, , optionRes, batchNo] = await Promise.all([
|
const requests = [
|
||||||
getProjectList(1, 9999, projectQueryParams),
|
getProjectList(1, 9999, projectQueryParams),
|
||||||
loadEditorOptions(),
|
loadEditorOptions(),
|
||||||
api.getImportOptions?.(),
|
api.getImportOptions?.(),
|
||||||
generateBatchNo(),
|
];
|
||||||
]);
|
if (generateCode) requests.push(generateBatchNo());
|
||||||
form.batchNo = batchNo;
|
const [projectRes, , optionRes, batchNo] = await Promise.all(requests);
|
||||||
|
if (generateCode) form.batchNo = batchNo || '';
|
||||||
projects.value = projectRes.data?.data?.records || [];
|
projects.value = projectRes.data?.data?.records || [];
|
||||||
const data = optionRes?.data?.data || {};
|
const data = optionRes?.data?.data || {};
|
||||||
queryCarriers.value = data.carriers || [];
|
queryCarriers.value = data.carriers || [];
|
||||||
@@ -1082,27 +1159,74 @@ const loadBatchRows = async batchId => {
|
|||||||
_key: `${id || 'row'}-${index}`,
|
_key: `${id || 'row'}-${index}`,
|
||||||
batchNo: item.batchNo || form.batchNo,
|
batchNo: item.batchNo || form.batchNo,
|
||||||
}));
|
}));
|
||||||
|
refreshDuplicateFlags();
|
||||||
rowSelection.value = [];
|
rowSelection.value = [];
|
||||||
previewTab.value = 'all';
|
previewTab.value = 'all';
|
||||||
};
|
};
|
||||||
const editBatch = async row => {
|
const editBatch = row => {
|
||||||
await openCreate();
|
router.push({
|
||||||
|
path: '/business/waybill-import/form',
|
||||||
|
query: { id: row.id, batchNo: row.batchNo || '' },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const openCreatePage = async () => {
|
||||||
|
const batchId = route.query.id;
|
||||||
|
if (!batchId) {
|
||||||
|
await openCreate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await api.getImportBatches({
|
||||||
|
id: batchId,
|
||||||
|
batchNo: route.query.batchNo || '',
|
||||||
|
current: 1,
|
||||||
|
size: 20,
|
||||||
|
});
|
||||||
|
const batch = (res.data?.data?.records || []).find(item => String(item.id) === String(batchId));
|
||||||
|
if (!batch) {
|
||||||
|
ElMessage.warning('未找到导入批次');
|
||||||
|
router.push({ path: '/business/waybill-import' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await fillEditForm(batch);
|
||||||
|
};
|
||||||
|
const fillEditForm = async row => {
|
||||||
|
await prepareCreateForm(false);
|
||||||
const snapshot = { ...row };
|
const snapshot = { ...row };
|
||||||
|
const savedCarrierId = String(snapshot.carrierIds || snapshot.carrierId || '')
|
||||||
|
.split(',')
|
||||||
|
.map(item => item.trim())
|
||||||
|
.find(Boolean);
|
||||||
if (snapshot.projectId) await projectChange(snapshot.projectId);
|
if (snapshot.projectId) await projectChange(snapshot.projectId);
|
||||||
|
const project = projects.value.find(item => String(item.id) === String(snapshot.projectId));
|
||||||
Object.assign(form, snapshot, {
|
Object.assign(form, snapshot, {
|
||||||
projectId: snapshot.projectId || '',
|
id: snapshot.id || '',
|
||||||
|
batchNo: snapshot.batchNo || '',
|
||||||
|
projectId: project?.id || snapshot.projectId || '',
|
||||||
projectName: snapshot.projectName || form.projectName,
|
projectName: snapshot.projectName || form.projectName,
|
||||||
// 列表返回的是 importStatus,表单沿用 status 字段提交给后端。
|
// 列表返回的是 importStatus,表单沿用 status 字段提交给后端。
|
||||||
status: snapshot.importStatus || 'completed',
|
status:
|
||||||
|
snapshot.importStatus === 'running' || snapshot.importStatus === 'completed'
|
||||||
|
? snapshot.importStatus
|
||||||
|
: 'completed',
|
||||||
file: null,
|
file: null,
|
||||||
|
carrierIds: [],
|
||||||
});
|
});
|
||||||
|
const plan = plans.value.find(item => String(item.id) === String(snapshot.planId));
|
||||||
|
if (plan) form.planId = plan.id;
|
||||||
const contract = contracts.value.find(item => String(item.id) === String(snapshot.contractId));
|
const contract = contracts.value.find(item => String(item.id) === String(snapshot.contractId));
|
||||||
form.contractId = contract?.id || '';
|
form.contractId = contract?.id || '';
|
||||||
form.carrierType = snapshot.carrierType || '承运商';
|
form.carrierType = snapshot.carrierType || '承运商';
|
||||||
contractChange(form.contractId);
|
contractChange(form.contractId);
|
||||||
syncCarrierOptions();
|
syncCarrierOptions();
|
||||||
|
const carrierOption =
|
||||||
|
form.carrierType === '自运'
|
||||||
|
? null
|
||||||
|
: carrierOptions.value.find(item => String(item.id) === String(savedCarrierId || '')) ||
|
||||||
|
carrierOptions.value.find(item => item.carrierName === snapshot.carrierName);
|
||||||
const carrierValue =
|
const carrierValue =
|
||||||
form.carrierType === '自运' ? snapshot.carrierName : snapshot.carrierContractId;
|
form.carrierType === '自运'
|
||||||
|
? snapshot.carrierName
|
||||||
|
: snapshot.carrierContractId || carrierOption?.value;
|
||||||
if (carrierValue) carrierChange(carrierValue);
|
if (carrierValue) carrierChange(carrierValue);
|
||||||
await loadBatchRows(snapshot.id);
|
await loadBatchRows(snapshot.id);
|
||||||
};
|
};
|
||||||
@@ -1170,6 +1294,12 @@ const carrierChange = value => {
|
|||||||
if (!isSelfOperated.value) form.carrierContractId = item.carrierContractId || '';
|
if (!isSelfOperated.value) form.carrierContractId = item.carrierContractId || '';
|
||||||
};
|
};
|
||||||
const fileChange = async (file, list) => {
|
const fileChange = async (file, list) => {
|
||||||
|
if (!projectInfoReady.value) {
|
||||||
|
ElMessage.warning('请先选择项目、客户合同和承运信息,再导入运单明细');
|
||||||
|
files.value = [];
|
||||||
|
form.file = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (file.raw?.size > 50 * 1024 * 1024) {
|
if (file.raw?.size > 50 * 1024 * 1024) {
|
||||||
ElMessage.warning('单个文件大小不能超过50M');
|
ElMessage.warning('单个文件大小不能超过50M');
|
||||||
files.value = [];
|
files.value = [];
|
||||||
@@ -1190,7 +1320,7 @@ const fileChange = async (file, list) => {
|
|||||||
const keyMap = {
|
const keyMap = {
|
||||||
originalNo: ['原始单号'],
|
originalNo: ['原始单号'],
|
||||||
loadingIdentifier: ['配载标识号'],
|
loadingIdentifier: ['配载标识号'],
|
||||||
vehicleNo: ['车牌号/航班号/船号/班列号'],
|
vehicleNo: ['车牌号/船班号/船次号/班次号', '车牌号/航班号/船号/班列号'],
|
||||||
transportType: ['运输方式', '运输类型'],
|
transportType: ['运输方式', '运输类型'],
|
||||||
driverName: ['司机/船长姓名', '司机/船长'],
|
driverName: ['司机/船长姓名', '司机/船长'],
|
||||||
driverPhone: ['司机/船长手机号'],
|
driverPhone: ['司机/船长手机号'],
|
||||||
@@ -1227,7 +1357,6 @@ const fileChange = async (file, list) => {
|
|||||||
const text = String(value ?? '').trim();
|
const text = String(value ?? '').trim();
|
||||||
return text.slice(0, 10);
|
return text.slice(0, 10);
|
||||||
};
|
};
|
||||||
const count = new Map();
|
|
||||||
// 先解析数据,但不填充到 rows.value
|
// 先解析数据,但不填充到 rows.value
|
||||||
const parsedRows = source.map((item, index) => {
|
const parsedRows = source.map((item, index) => {
|
||||||
const row = { _key: `${Date.now()}-${index}`, ...item, batchNo: form.batchNo };
|
const row = { _key: `${Date.now()}-${index}`, ...item, batchNo: form.batchNo };
|
||||||
@@ -1241,14 +1370,9 @@ const fileChange = async (file, list) => {
|
|||||||
row.actualEndDate = normalizeImportDate(row.actualEndDate);
|
row.actualEndDate = normalizeImportDate(row.actualEndDate);
|
||||||
row.planStartDate = normalizeImportDate(row.planStartDate);
|
row.planStartDate = normalizeImportDate(row.planStartDate);
|
||||||
row.planEndDate = normalizeImportDate(row.planEndDate);
|
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;
|
|
||||||
return row;
|
return row;
|
||||||
});
|
});
|
||||||
parsedRows.forEach(row => {
|
refreshDuplicateFlags(parsedRows);
|
||||||
row._duplicate = count.get(row._duplicateKey) > 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 调用后端校验接口,校验通过才填充表格
|
// 调用后端校验接口,校验通过才填充表格
|
||||||
const isValid = await performValidation(parsedRows);
|
const isValid = await performValidation(parsedRows);
|
||||||
@@ -1271,8 +1395,39 @@ const fileRemove = () => {
|
|||||||
/** 运单批量导入每次请求最多提交行数,避免网关 body 超限。 */
|
/** 运单批量导入每次请求最多提交行数,避免网关 body 超限。 */
|
||||||
const IMPORT_BATCH_SIZE = 100;
|
const IMPORT_BATCH_SIZE = 100;
|
||||||
|
|
||||||
|
const importFieldText = value => String(value ?? '').trim();
|
||||||
|
|
||||||
|
/** 同一运单批次号下,全部导入字段一致时标记为疑似重复。已在线确认的行不再标为重复。 */
|
||||||
|
const refreshDuplicateFlags = (list = rows.value) => {
|
||||||
|
const groups = new Map();
|
||||||
|
list.forEach(row => {
|
||||||
|
const signature = detailColumns.map(column => importFieldText(row[column.prop])).join('\u0001');
|
||||||
|
const batchNo = importFieldText(row.batchNo || form.batchNo);
|
||||||
|
if (row._duplicateConfirmed && row._duplicateSignature !== signature) {
|
||||||
|
row._duplicateConfirmed = false;
|
||||||
|
}
|
||||||
|
row._duplicateSignature = signature;
|
||||||
|
const groupKey = `${batchNo}\u0002${signature}`;
|
||||||
|
const group = groups.get(groupKey) || [];
|
||||||
|
group.push(row);
|
||||||
|
groups.set(groupKey, group);
|
||||||
|
});
|
||||||
|
groups.forEach(group => {
|
||||||
|
const duplicated = group.length > 1;
|
||||||
|
group.forEach(row => {
|
||||||
|
row._duplicate = duplicated && !row._duplicateConfirmed;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDuplicateRow = row => {
|
||||||
|
row._duplicateConfirmed = true;
|
||||||
|
refreshDuplicateFlags();
|
||||||
|
ElMessage.success('已确认,该行将按正常数据导入');
|
||||||
|
};
|
||||||
|
|
||||||
const stripInternalRowFields = row => {
|
const stripInternalRowFields = row => {
|
||||||
const { _key, _duplicate, _duplicateKey, _editing, _editSnapshot, ...data } = row;
|
const { _key, _duplicate, _duplicateKey, _duplicateConfirmed, _duplicateSignature, _editing, _editSnapshot, ...data } = row;
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1334,6 +1489,37 @@ const downloadImportFailures = async failureRows => {
|
|||||||
downloadXls(blob, `运单导入失败明细_${dayjs().format('YYYYMMDDHHmmss')}.xlsx`);
|
downloadXls(blob, `运单导入失败明细_${dayjs().format('YYYYMMDDHHmmss')}.xlsx`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 按每批 100 条请求校验,全部批次结束后再汇总失败行。 */
|
||||||
|
const collectImportValidationErrors = async rowList => {
|
||||||
|
const errorMap = new Map();
|
||||||
|
for (const chunk of chunkImportRows(rowList)) {
|
||||||
|
const response = await api.validateImport(buildImportPayload(form.status, chunk.rows));
|
||||||
|
const result = response?.data?.data ?? response?.data ?? response;
|
||||||
|
if (result?.success === false || (Array.isArray(result?.errors) && result.errors.length)) {
|
||||||
|
(result.errors || []).forEach(error => {
|
||||||
|
const localIndex = Number(error.rowIndex);
|
||||||
|
if (Number.isNaN(localIndex) || localIndex < 0 || localIndex >= chunk.globalIndexes.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
errorMap.set(chunk.globalIndexes[localIndex], error.errorMessage || '校验失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errorMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportCollectedImportFailures = async (rowList, errorMap, exportAll = false) => {
|
||||||
|
const indexedRows = exportAll
|
||||||
|
? rowList.map((row, globalIndex) => [globalIndex, errorMap.get(globalIndex) || ''])
|
||||||
|
: [...errorMap.entries()].sort((left, right) => left[0] - right[0]);
|
||||||
|
const failureRows = indexedRows.map(([globalIndex, errorMessage]) => ({
|
||||||
|
...stripInternalRowFields(rowList[globalIndex]),
|
||||||
|
errorMessage,
|
||||||
|
}));
|
||||||
|
await downloadImportFailures(failureRows);
|
||||||
|
ElMessage.warning('数据校验失败,已自动下载错误明细表,请修正后重新上传');
|
||||||
|
};
|
||||||
|
|
||||||
const performValidation = async parsedRows => {
|
const performValidation = async parsedRows => {
|
||||||
if (!parsedRows || !parsedRows.length) return false;
|
if (!parsedRows || !parsedRows.length) return false;
|
||||||
|
|
||||||
@@ -1361,15 +1547,7 @@ const performValidation = async parsedRows => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (errorMap.size) {
|
if (errorMap.size) {
|
||||||
const failureRows = [];
|
await exportCollectedImportFailures(parsedRows, errorMap);
|
||||||
errorMap.forEach((errorMessage, globalIndex) => {
|
|
||||||
failureRows.push({
|
|
||||||
...stripInternalRowFields(parsedRows[globalIndex]),
|
|
||||||
errorMessage,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await downloadImportFailures(failureRows);
|
|
||||||
ElMessage.warning('数据校验失败,已自动下载错误明细表,请修正后重新上传');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1381,7 +1559,8 @@ const performValidation = async parsedRows => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const formatCell = (row, column) => row[column.prop] || '';
|
const formatCell = (row, column) =>
|
||||||
|
column.prop === 'transportType' ? transportTypeLabel(row) : row[column.prop] || '';
|
||||||
const updateEditorValue = (column, row, value) => {
|
const updateEditorValue = (column, row, value) => {
|
||||||
if (column.editor === 'number') {
|
if (column.editor === 'number') {
|
||||||
const [integer = '', decimal = ''] = String(value || '')
|
const [integer = '', decimal = ''] = String(value || '')
|
||||||
@@ -1456,6 +1635,7 @@ const editRow = row => {
|
|||||||
const saveRow = row => {
|
const saveRow = row => {
|
||||||
row._editing = false;
|
row._editing = false;
|
||||||
delete row._editSnapshot;
|
delete row._editSnapshot;
|
||||||
|
refreshDuplicateFlags();
|
||||||
};
|
};
|
||||||
const cancelRow = row => {
|
const cancelRow = row => {
|
||||||
if (row._editSnapshot) Object.assign(row, row._editSnapshot);
|
if (row._editSnapshot) Object.assign(row, row._editSnapshot);
|
||||||
@@ -1464,11 +1644,13 @@ const cancelRow = row => {
|
|||||||
};
|
};
|
||||||
const deleteRow = row => {
|
const deleteRow = row => {
|
||||||
rows.value = rows.value.filter(item => item !== row);
|
rows.value = rows.value.filter(item => item !== row);
|
||||||
|
refreshDuplicateFlags();
|
||||||
};
|
};
|
||||||
const removeSelectedRows = () => {
|
const removeSelectedRows = () => {
|
||||||
const keys = new Set(rowSelection.value.map(row => row._key));
|
const keys = new Set(rowSelection.value.map(row => row._key));
|
||||||
rows.value = rows.value.filter(row => !keys.has(row._key));
|
rows.value = rows.value.filter(row => !keys.has(row._key));
|
||||||
rowSelection.value = [];
|
rowSelection.value = [];
|
||||||
|
refreshDuplicateFlags();
|
||||||
};
|
};
|
||||||
const firstNotEmpty = (...values) => values.find(value => String(value ?? '').trim()) ?? '';
|
const firstNotEmpty = (...values) => values.find(value => String(value ?? '').trim()) ?? '';
|
||||||
const normalizeOptionalImportNumber = (value, emptySentinels = []) => {
|
const normalizeOptionalImportNumber = (value, emptySentinels = []) => {
|
||||||
@@ -1534,17 +1716,27 @@ const saveDraft = async () => {
|
|||||||
backToList();
|
backToList();
|
||||||
};
|
};
|
||||||
const confirmImport = async () => {
|
const confirmImport = async () => {
|
||||||
// 导入状态选择草稿时不生成正式运单,等同于保存草稿。
|
|
||||||
if (form.status === 'draft') return saveDraft();
|
|
||||||
await formRef.value?.validate();
|
await formRef.value?.validate();
|
||||||
if (!validateImportRows()) return;
|
if (!validateImportRows()) return;
|
||||||
const count = rows.value.filter(row => row._duplicate).length;
|
const count = rows.value.filter(row => row._duplicate).length;
|
||||||
if (count) await ElMessageBox.confirm(`当前导入有${count}条重复数据,是否确认导入?`, '提示');
|
if (count) await ElMessageBox.confirm(`当前导入有${count}条疑似重复数据,是否确认导入?`, '提示');
|
||||||
if (form.importType === 'settlement')
|
if (form.importType === 'settlement' && form.status === 'completed')
|
||||||
await ElMessageBox.confirm('确认后将同时生成运单、应收应付明细、计结算单,是否继续?', '提示');
|
await ElMessageBox.confirm('确认后将同时生成运单、应收应付明细、计结算单,是否继续?', '提示');
|
||||||
|
|
||||||
const chunks = chunkImportRows(rows.value);
|
const chunks = chunkImportRows(rows.value);
|
||||||
// 仅草稿转确认时复用原批次 id;过程中不回写 form.id,避免失败重试误覆盖已完成批次。
|
let errorMap;
|
||||||
|
try {
|
||||||
|
errorMap = await collectImportValidationErrors(rows.value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('校验失败:', error);
|
||||||
|
ElMessage.error(error?.message || '校验接口调用失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (errorMap.size) {
|
||||||
|
await exportCollectedImportFailures(rows.value, errorMap, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 校验全部通过后才分批落库,避免前面批次已入库、后面批次校验失败。
|
||||||
const draftBatchId = form.id || '';
|
const draftBatchId = form.id || '';
|
||||||
let batchId = '';
|
let batchId = '';
|
||||||
try {
|
try {
|
||||||
@@ -1660,6 +1852,15 @@ const confirmImport = async () => {
|
|||||||
&__detail-panel {
|
&__detail-panel {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
&__import-title {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
&__import-hint {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
&__form-actions {
|
&__form-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -1672,6 +1873,14 @@ const confirmImport = async () => {
|
|||||||
gap: 24px;
|
gap: 24px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
&__row-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
:deep(.is-duplicate-row > td.el-table__cell) {
|
||||||
|
background: #fff7e8;
|
||||||
|
}
|
||||||
&__file-tip {
|
&__file-tip {
|
||||||
margin: 0 24px;
|
margin: 0 24px;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
|
|||||||
@@ -492,6 +492,7 @@
|
|||||||
v-model="row.quantityUnit"
|
v-model="row.quantityUnit"
|
||||||
clearable
|
clearable
|
||||||
:disabled="dialogReadonly"
|
:disabled="dialogReadonly"
|
||||||
|
@change="handleTransportCargoQuantityUnitChange"
|
||||||
@visible-change="visible => visible && loadTaskQuantityUnitOptions()"
|
@visible-change="visible => visible && loadTaskQuantityUnitOptions()"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
@@ -1091,20 +1092,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="数量合计">
|
|
||||||
<el-input :model-value="taskFullFreightGroupQuantity(group)" disabled>
|
|
||||||
<template #append>
|
|
||||||
<el-select
|
|
||||||
:model-value="group.quantityUnit"
|
|
||||||
class="waybill-manage-page__append-select waybill-manage-page__append-select--wide"
|
|
||||||
placeholder="单位"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
<el-option :label="fixedQuantityUnit" :value="fixedQuantityUnit" />
|
|
||||||
</el-select>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="运费">
|
<el-form-item label="运费">
|
||||||
<el-input
|
<el-input
|
||||||
:model-value="taskFullFreightGroupAmount(group)"
|
:model-value="taskFullFreightGroupAmount(group)"
|
||||||
@@ -1117,11 +1104,6 @@
|
|||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
<el-form-item label="运费合计">
|
|
||||||
<el-input :model-value="taskFullFreightTotal" disabled>
|
|
||||||
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="其他运费合计">
|
<el-form-item label="其他运费合计">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.otherFeeTotal"
|
v-model="form.otherFeeTotal"
|
||||||
@@ -1133,6 +1115,11 @@
|
|||||||
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
|
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="运费合计">
|
||||||
|
<el-input :model-value="taskFullFreightTotal" disabled>
|
||||||
|
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1317,7 +1304,7 @@
|
|||||||
@change="handleTaskCargoTypeChange"
|
@change="handleTaskCargoTypeChange"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="货物名称">
|
<el-form-item label="货物名称" required>
|
||||||
<el-autocomplete
|
<el-autocomplete
|
||||||
v-model="form.cargoName"
|
v-model="form.cargoName"
|
||||||
placeholder="请输入货物名称"
|
placeholder="请输入货物名称"
|
||||||
@@ -1347,7 +1334,7 @@
|
|||||||
:disabled="dialogReadonly"
|
:disabled="dialogReadonly"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="型号">
|
<el-form-item label="型号" required>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.model"
|
v-model="form.model"
|
||||||
placeholder="选择货物自动回填"
|
placeholder="选择货物自动回填"
|
||||||
@@ -1355,7 +1342,7 @@
|
|||||||
:disabled="dialogReadonly"
|
:disabled="dialogReadonly"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="数量">
|
<el-form-item label="数量" required>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.quantity"
|
v-model="form.quantity"
|
||||||
placeholder="请输入数量"
|
placeholder="请输入数量"
|
||||||
@@ -1962,7 +1949,7 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="waybill-manage-page__waybill-detail-field">
|
<div class="waybill-manage-page__waybill-detail-field">
|
||||||
<span>航班号/船次号/班列号</span>
|
<span>船班号/船次号/班次号</span>
|
||||||
<strong>{{ detailRow.vehicleNo || '-' }}</strong>
|
<strong>{{ detailRow.vehicleNo || '-' }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="waybill-manage-page__waybill-detail-field">
|
<div class="waybill-manage-page__waybill-detail-field">
|
||||||
@@ -2162,8 +2149,12 @@
|
|||||||
label="批量补录"
|
label="批量补录"
|
||||||
name="batchSupplement"
|
name="batchSupplement"
|
||||||
>
|
>
|
||||||
|
<voucher-execution-detail
|
||||||
|
v-if="waybillIsCarrier(detailRow)"
|
||||||
|
:waybill-id="detailRow.id"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="waybillVoucherFolderView"
|
v-else-if="waybillVoucherFolderView"
|
||||||
class="waybill-manage-page__voucher-folder-toolbar"
|
class="waybill-manage-page__voucher-folder-toolbar"
|
||||||
>
|
>
|
||||||
<el-button text type="primary" @click="backToWaybillVoucherFolders">
|
<el-button text type="primary" @click="backToWaybillVoucherFolders">
|
||||||
@@ -2172,6 +2163,7 @@
|
|||||||
<span>{{ waybillVoucherFolderName(waybillVoucherFolder) }}</span>
|
<span>{{ waybillVoucherFolderName(waybillVoucherFolder) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
v-if="!waybillIsCarrier(detailRow)"
|
||||||
v-loading="waybillVoucherImagesLoading"
|
v-loading="waybillVoucherImagesLoading"
|
||||||
class="waybill-manage-page__voucher-image-grid"
|
class="waybill-manage-page__voucher-image-grid"
|
||||||
>
|
>
|
||||||
@@ -3135,6 +3127,7 @@ import {
|
|||||||
getVoucherImages as getProcessConfigVoucherImages,
|
getVoucherImages as getProcessConfigVoucherImages,
|
||||||
} from '@/api/business/process-config';
|
} from '@/api/business/process-config';
|
||||||
import { getPunchRecords as getWaybillPunchRecords, locateVehicle, trackVehicle } from '@/api/business/waybill-manage';
|
import { getPunchRecords as getWaybillPunchRecords, locateVehicle, trackVehicle } from '@/api/business/waybill-manage';
|
||||||
|
import VoucherExecutionDetail from './voucher-execution-detail.vue';
|
||||||
import { getMkPublicDetail } from '@/api/mk-process';
|
import { getMkPublicDetail } from '@/api/mk-process';
|
||||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||||
import {
|
import {
|
||||||
@@ -3332,6 +3325,7 @@ export default {
|
|||||||
WarnTriangleFilled,
|
WarnTriangleFilled,
|
||||||
ElImageViewer,
|
ElImageViewer,
|
||||||
OpenFileViewer,
|
OpenFileViewer,
|
||||||
|
VoucherExecutionDetail,
|
||||||
},
|
},
|
||||||
name: 'WaybillManagePage',
|
name: 'WaybillManagePage',
|
||||||
props: {
|
props: {
|
||||||
@@ -3804,7 +3798,7 @@ export default {
|
|||||||
return !this.isRoadTransport;
|
return !this.isRoadTransport;
|
||||||
},
|
},
|
||||||
transportVehicleNoLabel() {
|
transportVehicleNoLabel() {
|
||||||
return this.isRoadTransport ? '车牌号' : '船/航/班列号';
|
return this.isRoadTransport ? '车牌号' : '船班号/船次号/班次号';
|
||||||
},
|
},
|
||||||
actionButtonLikeSearch() {
|
actionButtonLikeSearch() {
|
||||||
return this.config.actionButtonLikeSearch === true;
|
return this.config.actionButtonLikeSearch === true;
|
||||||
@@ -3873,9 +3867,6 @@ export default {
|
|||||||
if (!total) return '';
|
if (!total) return '';
|
||||||
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(3)));
|
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(3)));
|
||||||
},
|
},
|
||||||
fixedQuantityUnit() {
|
|
||||||
return WAYBILL_QUANTITY_UNIT;
|
|
||||||
},
|
|
||||||
taskFreightAmount() {
|
taskFreightAmount() {
|
||||||
const quantity = Number(this.taskCargoQuantityTotal || this.form.quantity || 0);
|
const quantity = Number(this.taskCargoQuantityTotal || this.form.quantity || 0);
|
||||||
const unitPrice = Number(this.form.unitPrice || 0);
|
const unitPrice = Number(this.form.unitPrice || 0);
|
||||||
@@ -4617,7 +4608,7 @@ export default {
|
|||||||
return text.slice(0, 10);
|
return text.slice(0, 10);
|
||||||
},
|
},
|
||||||
waybillVehicleNoLabel(row = {}) {
|
waybillVehicleNoLabel(row = {}) {
|
||||||
return this.waybillIsRoadTransport(row) ? '车牌号' : '船/航/班列号';
|
return this.waybillIsRoadTransport(row) ? '车牌号' : '船班号/船次号/班次号';
|
||||||
},
|
},
|
||||||
waybillUnitPrice(row = {}) {
|
waybillUnitPrice(row = {}) {
|
||||||
const price = row.unitPrice || row.price || '';
|
const price = row.unitPrice || row.price || '';
|
||||||
@@ -6408,14 +6399,11 @@ export default {
|
|||||||
if (!rows.length) return false;
|
if (!rows.length) return false;
|
||||||
const quantityUnits = rows.map(row => String(row.quantityUnit || '').trim());
|
const quantityUnits = rows.map(row => String(row.quantityUnit || '').trim());
|
||||||
if (quantityUnits.some(unit => !unit) || new Set(quantityUnits).size !== 1) return false;
|
if (quantityUnits.some(unit => !unit) || new Set(quantityUnits).size !== 1) return false;
|
||||||
const quantityUnit = quantityUnits[0];
|
const expectedPriceUnit = this.resolvePriceUnitByQuantityUnit(quantityUnits[0]);
|
||||||
return rows.every(row => {
|
if (!expectedPriceUnit) return false;
|
||||||
const priceUnit = String(row.priceUnit || '').trim();
|
return rows.every(
|
||||||
const separatorIndex = Math.max(priceUnit.lastIndexOf('/'), priceUnit.lastIndexOf('/'));
|
row => !this.isBillingFieldEmpty(row.unitPrice) && String(row.priceUnit || '').trim() === expectedPriceUnit
|
||||||
const priceQuantityUnit =
|
);
|
||||||
separatorIndex >= 0 ? priceUnit.slice(separatorIndex + 1).trim() : priceUnit;
|
|
||||||
return !this.isBillingFieldEmpty(row.unitPrice) && priceQuantityUnit === quantityUnit;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
taskFullFreightQuantity(index) {
|
taskFullFreightQuantity(index) {
|
||||||
return this.formatTaskFullFreightNumber(this.transportCargoRows[index]?.quantity || 0);
|
return this.formatTaskFullFreightNumber(this.transportCargoRows[index]?.quantity || 0);
|
||||||
@@ -6431,10 +6419,6 @@ export default {
|
|||||||
Number(cargo.unitPrice || 0) * Number(cargo.quantity || 0)
|
Number(cargo.unitPrice || 0) * Number(cargo.quantity || 0)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
taskFullFreightGroupQuantity(group = {}) {
|
|
||||||
const total = (group.rows || []).reduce((sum, cargo) => sum + Number(cargo.quantity || 0), 0);
|
|
||||||
return this.formatTaskFullFreightNumber(total);
|
|
||||||
},
|
|
||||||
taskFullFreightGroupUnitPrice(group = {}) {
|
taskFullFreightGroupUnitPrice(group = {}) {
|
||||||
const prices = (group.rows || [])
|
const prices = (group.rows || [])
|
||||||
.map(cargo => String(cargo.unitPrice ?? '').trim())
|
.map(cargo => String(cargo.unitPrice ?? '').trim())
|
||||||
@@ -6443,7 +6427,11 @@ export default {
|
|||||||
return prices[0];
|
return prices[0];
|
||||||
},
|
},
|
||||||
taskFullFreightGroupPriceUnit(group = {}) {
|
taskFullFreightGroupPriceUnit(group = {}) {
|
||||||
return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : '');
|
return (
|
||||||
|
group.rows?.[0]?.priceUnit ||
|
||||||
|
this.resolvePriceUnitByQuantityUnit(group.rows?.[0]?.quantityUnit) ||
|
||||||
|
''
|
||||||
|
);
|
||||||
},
|
},
|
||||||
taskFullFreightGroupAmount(group = {}) {
|
taskFullFreightGroupAmount(group = {}) {
|
||||||
const total = (group.rows || []).reduce(
|
const total = (group.rows || []).reduce(
|
||||||
@@ -7174,6 +7162,11 @@ export default {
|
|||||||
requiredFields.push(['driverName', '司机']);
|
requiredFields.push(['driverName', '司机']);
|
||||||
requiredFields.push(['driverPhone', '手机号']);
|
requiredFields.push(['driverPhone', '手机号']);
|
||||||
}
|
}
|
||||||
|
if (!this.isTaskFullMode) {
|
||||||
|
requiredFields.push(['cargoName', '货物名称']);
|
||||||
|
requiredFields.push(['model', '型号']);
|
||||||
|
requiredFields.push(['quantity', '数量']);
|
||||||
|
}
|
||||||
const emptyField = requiredFields.find(([prop]) => this.isBillingFieldEmpty(row[prop]));
|
const emptyField = requiredFields.find(([prop]) => this.isBillingFieldEmpty(row[prop]));
|
||||||
if (emptyField) {
|
if (emptyField) {
|
||||||
this.$message.warning(`请选择或输入${emptyField[1]}`);
|
this.$message.warning(`请选择或输入${emptyField[1]}`);
|
||||||
@@ -7208,7 +7201,7 @@ export default {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const invalidLengthField = [
|
const invalidLengthField = [
|
||||||
['vehicleNo', '船/航/班列号', 30],
|
['vehicleNo', '船班号/船次号/班次号', 30],
|
||||||
['captainName', '船长', 20],
|
['captainName', '船长', 20],
|
||||||
['cabinNo', '舱位', 30],
|
['cabinNo', '舱位', 30],
|
||||||
['containerNo', '箱号', 30],
|
['containerNo', '箱号', 30],
|
||||||
@@ -7466,6 +7459,24 @@ export default {
|
|||||||
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 3)}` : parts[0];
|
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 3)}` : parts[0];
|
||||||
this.syncTransportCargoJson();
|
this.syncTransportCargoJson();
|
||||||
},
|
},
|
||||||
|
resolvePriceUnitByQuantityUnit(quantityUnit) {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
吨: '元/吨',
|
||||||
|
方: '元/立方',
|
||||||
|
件: '元/件',
|
||||||
|
}[String(quantityUnit || '').trim()] || ''
|
||||||
|
);
|
||||||
|
},
|
||||||
|
handleTransportCargoQuantityUnitChange(quantityUnit) {
|
||||||
|
const priceUnit = this.resolvePriceUnitByQuantityUnit(quantityUnit);
|
||||||
|
if (!priceUnit) return;
|
||||||
|
(this.transportCargoRows || []).forEach(cargo => {
|
||||||
|
cargo.priceUnit = priceUnit;
|
||||||
|
});
|
||||||
|
this.form.priceUnit = priceUnit;
|
||||||
|
this.syncTransportCargoJson();
|
||||||
|
},
|
||||||
handleTransportCargoTypeChange(row, value) {
|
handleTransportCargoTypeChange(row, value) {
|
||||||
const path = this.normalizeCargoTypePath(value);
|
const path = this.normalizeCargoTypePath(value);
|
||||||
const cargoType = this.findCargoTypeByPath(path);
|
const cargoType = this.findCargoTypeByPath(path);
|
||||||
@@ -9787,10 +9798,16 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--road {
|
&--road {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
column-gap: 56px;
|
column-gap: 24px;
|
||||||
row-gap: 16px;
|
row-gap: 16px;
|
||||||
|
|
||||||
|
:deep(.el-form-item__content > .el-input),
|
||||||
|
:deep(.el-form-item__content > .el-select) {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.waybill-manage-page__append-select) {
|
:deep(.waybill-manage-page__append-select) {
|
||||||
width: 112px;
|
width: 112px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="车/船/航班/班列">
|
<el-form-item label="车牌号/船班号/船次号/班次号">
|
||||||
<el-input v-model="searchForm.vehicleNo" clearable placeholder="请输入" />
|
<el-input v-model="searchForm.vehicleNo" clearable placeholder="请输入" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="原始单号">
|
<el-form-item label="原始单号">
|
||||||
@@ -719,8 +719,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane v-if="hasRelatedVoucher" label="批量补录" name="batchSupplement">
|
<el-tab-pane v-if="hasRelatedVoucher" label="批量补录" name="batchSupplement">
|
||||||
|
<voucher-execution-detail
|
||||||
|
v-if="isCarrierExecutionVoucher"
|
||||||
|
:key="processWaybillId"
|
||||||
|
:waybill-id="processWaybillId"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="voucherFolderView"
|
v-else-if="voucherFolderView"
|
||||||
class="loading-detail__voucher-folder-toolbar"
|
class="loading-detail__voucher-folder-toolbar"
|
||||||
>
|
>
|
||||||
<el-button text type="primary" @click="backToVoucherFolders">
|
<el-button text type="primary" @click="backToVoucherFolders">
|
||||||
@@ -728,7 +733,11 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
<span>{{ voucherFolderName(voucherFolder) }}</span>
|
<span>{{ voucherFolderName(voucherFolder) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-loading="voucherImagesLoading" class="loading-detail__voucher-image-grid">
|
<div
|
||||||
|
v-if="!isCarrierExecutionVoucher"
|
||||||
|
v-loading="voucherImagesLoading"
|
||||||
|
class="loading-detail__voucher-image-grid"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(image, index) in voucherImages"
|
v-for="(image, index) in voucherImages"
|
||||||
:key="
|
:key="
|
||||||
@@ -936,7 +945,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="车/船/航班/班列">
|
<el-form-item label="车牌号/船班号/船次号/班次号">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="candidateQuery.vehicleNo"
|
v-model="candidateQuery.vehicleNo"
|
||||||
clearable
|
clearable
|
||||||
@@ -1755,6 +1764,7 @@ import {
|
|||||||
getDetail as getProcessConfigDetail,
|
getDetail as getProcessConfigDetail,
|
||||||
getVoucherImages as getProcessConfigVoucherImages,
|
getVoucherImages as getProcessConfigVoucherImages,
|
||||||
} from '@/api/business/process-config';
|
} from '@/api/business/process-config';
|
||||||
|
import VoucherExecutionDetail from './components/voucher-execution-detail.vue';
|
||||||
import { getList as getCommonAddressList } from '@/api/base/common-address';
|
import { getList as getCommonAddressList } from '@/api/base/common-address';
|
||||||
import {
|
import {
|
||||||
carrierTypeOptions,
|
carrierTypeOptions,
|
||||||
@@ -1965,6 +1975,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Rank,
|
Rank,
|
||||||
Search,
|
Search,
|
||||||
|
VoucherExecutionDetail,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -2118,6 +2129,13 @@ export default {
|
|||||||
isSelfCarrierDetail() {
|
isSelfCarrierDetail() {
|
||||||
return String(this.dialogForm.carrierType || '').trim() === '自运';
|
return String(this.dialogForm.carrierType || '').trim() === '自运';
|
||||||
},
|
},
|
||||||
|
isCarrierExecutionVoucher() {
|
||||||
|
if (String(this.dialogForm.carrierType || '').trim() === '承运商') return true;
|
||||||
|
const waybill = (this.selectedWaybillRows || []).find(
|
||||||
|
item => String(item.id) === String(this.processWaybillId)
|
||||||
|
);
|
||||||
|
return String(waybill?.carrierTypeName || waybill?.carrierType || '').trim() === '承运商';
|
||||||
|
},
|
||||||
detailTaskMileage() {
|
detailTaskMileage() {
|
||||||
return normalizeLoadingMileage(this.dialogForm.mileage);
|
return normalizeLoadingMileage(this.dialogForm.mileage);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<basic-container class="waybill-import-form-page-container">
|
<basic-container class="waybill-import-form-page-container">
|
||||||
<div class="waybill-import-form-page-title">新建导入</div>
|
<div class="waybill-import-form-page-title">{{ pageTitle }}</div>
|
||||||
<waybill-import-dialog create-page />
|
<waybill-import-dialog create-page />
|
||||||
</basic-container>
|
</basic-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
import WaybillImportDialog from './components/waybill-import-dialog.vue';
|
import WaybillImportDialog from './components/waybill-import-dialog.vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const pageTitle = computed(() => (route.query.id ? '编辑导入' : '新建导入'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
Reference in New Issue
Block a user