✨ 完善运单导入编辑回填、分批校验与运费录入

导入编辑进入独立页面并回填数据,确认导入按每批100条校验后统一导出失败明细,完整录入运费信息按数量单位联动单价单位。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-27 22:54:33 +08:00
parent 98fbf123f5
commit f3a33b1a04
12 changed files with 668 additions and 133 deletions
+12
View File
@@ -22,6 +22,18 @@ export const getVoucherImages = (waybillId, 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 remove = api.remove;
export const copy = api.copy;
+1 -1
View File
@@ -31,7 +31,7 @@ export const loadingManageConfig = {
exportColumns: [
{ prop: 'loadingNo', label: '配载单号' },
{ prop: 'loadingSubNos', label: '配载子单号' },
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' },
{ prop: 'vehicleNo', label: '车牌号/船班号/船次号/班次号' },
{ prop: 'driverName', label: '司机' },
{ prop: 'driverPhone', label: '联系电话' },
{ prop: 'carrierType', label: '承运类型' },
+2 -2
View File
@@ -565,10 +565,10 @@ export const option = {
display: false,
},
{
label: '车牌号/航班号/船号/班列号',
label: '车牌号/船班号/船次号/班次号',
prop: 'vehicleNo',
search: true,
searchLabel: '车/船/航班/班列',
searchLabel: '车牌号/船班号/船次号/班次号',
searchOrder: 13,
minWidth: 210,
display: false,
@@ -104,7 +104,7 @@ export const generateWaybillColumns = [
{ label: '运单号', prop: 'waybillNo', minWidth: 160, link: true },
{ label: '项目名称', prop: 'projectName', minWidth: 160 },
{ label: '客户/委托方', prop: 'customerName', minWidth: 160 },
{ label: '车牌号/航班号/船号/班列号', prop: 'vehicleNo', minWidth: 190 },
{ label: '车牌号/船班号/船次号/班次号', prop: 'vehicleNo', minWidth: 190 },
{ label: '司机', prop: 'driverName', minWidth: 120 },
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
{ label: '运输类型', prop: 'transportType', minWidth: 130 },
@@ -2423,7 +2423,7 @@
<span v-else>-</span>
</template>
</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="carrierName" label="承运商" width="192" />
<el-table-column label="运输方式" width="192">
@@ -3514,7 +3514,7 @@
</el-table-column>
<el-table-column
prop="vehicleNo"
label="车牌号/航班号/船号/班列号"
label="车牌号/船班号/船次号/班次号"
min-width="200"
align="center"
show-overflow-tooltip
@@ -4169,7 +4169,7 @@
</el-form-item>
<el-form-item
v-if="dispatchItemForm.carrierType"
:label="dispatchIsNonRoadTransport ? '船/航/班列号' : '车牌号'"
:label="dispatchIsNonRoadTransport ? '船班号/船次号/班次号' : '车牌号'"
required
>
<el-input v-model="dispatchItemForm.vehicleNo" placeholder="请输入" />
@@ -6075,7 +6075,7 @@ export default {
return this.config.permission === 'waybill_manage' && !this.isRoadTransport;
},
transportVehicleNoLabel() {
return this.isRoadTransport ? '车牌号' : '船/航/班列号';
return this.isRoadTransport ? '车牌号' : '船班号/船次号/班次号';
},
transportPlanImportDuplicateCount() {
return this.transportPlanImportRows.filter(row => row._duplicate).length;
@@ -7121,7 +7121,7 @@ export default {
return String(row.carrierTypeName || row.carrierType || '').trim() === '承运商';
},
waybillVehicleNoLabel(row = {}) {
return this.waybillIsRoadTransport(row) ? '车牌号' : '船/航/班列号';
return this.waybillIsRoadTransport(row) ? '车牌号' : '船班号/船次号/班次号';
},
waybillUnitPrice(row = {}) {
const price = row.unitPrice || row.price || '';
@@ -9491,7 +9491,7 @@ export default {
}
} else {
const invalidLengthField = [
['vehicleNo', '船/航/班列号', 30],
['vehicleNo', '船班号/船次号/班次号', 30],
['captainName', '船长', 20],
['cabinNo', '舱位', 30],
['containerNo', '箱号', 30],
@@ -13278,7 +13278,7 @@ export default {
[
'vehicleNo',
['water', 'rail', 'air'].includes(this.resolveTransportMode(row.transportType))
? '船/航/班列号'
? '船班号/船次号/班次号'
: '车牌号',
],
];
@@ -13358,7 +13358,7 @@ export default {
['escortPhone', '押运人手机号', 20],
]
: [
['vehicleNo', '船/航/班列号', 30],
['vehicleNo', '船班号/船次号/班次号', 30],
['captainName', '船长', 20],
['cabinNo', '舱位', 30],
['containerNo', '箱号', 30],
@@ -248,7 +248,7 @@
</template>
<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><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.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>
@@ -756,7 +756,7 @@
<span v-else>-</span>
</template>
</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="carrierName" label="承运商" width="192" />
<el-table-column label="运输方式" width="192">
@@ -1401,7 +1401,7 @@
</el-table-column>
<el-table-column
prop="vehicleNo"
label="车牌号/航班号/船号/班列号"
label="车牌号/船班号/船次号/班次号"
min-width="200"
align="center"
show-overflow-tooltip
@@ -2097,7 +2097,7 @@
</el-form-item>
<el-form-item
v-if="dispatchItemForm.carrierType"
:label="dispatchIsNonRoadTransport ? '船/航/班列号' : '车牌号'"
:label="dispatchIsNonRoadTransport ? '船班号/船次号/班次号' : '车牌号'"
required
>
<el-input v-model="dispatchItemForm.vehicleNo" placeholder="请输入" />
@@ -8123,7 +8123,7 @@ export default {
[
'vehicleNo',
['water', 'rail', 'air'].includes(this.resolveTransportMode(row.transportType))
? '船/航/班列号'
? '船班号/船次号/班次号'
: '车牌号',
],
];
@@ -8204,7 +8204,7 @@ export default {
['escortPhone', '押运人手机号', 20],
]
: [
['vehicleNo', '船/航/班列号', 30],
['vehicleNo', '船班号/船次号/班次号', 30],
['captainName', '船长', 20],
['cabinNo', '舱位', 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">
<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-form-item label="司机/船长姓名"
><el-input v-model="detailQuery.driverName" clearable placeholder="请输入" /></el-form-item
@@ -219,12 +219,14 @@
prop="loadingIdentifier"
label="配载标识号"
min-width="130"
/><el-table-column
>
<template #default="{ row }">{{ row.loadingIdentifier || row.loadingNo || '' }}</template>
</el-table-column><el-table-column
prop="vehicleNo"
label="车牌号/航班号/船号/班列号"
label="车牌号/船班号/船次号/班次号"
min-width="190"
/><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
prop="driverName"
@@ -334,6 +336,7 @@
label-width="auto"
class="archive-form"
>
<div class="dialog-section-title">项目信息</div>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="项目" prop="projectId">
@@ -444,16 +447,21 @@
</el-form-item>
</el-col>
</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-upload
action="#"
accept=".xls,.xlsx"
:auto-upload="false"
:limit="1"
:disabled="!projectInfoReady"
:file-list="files"
:on-change="fileChange"
: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
><el-link type="primary" @click="downloadTemplate">下载模板</el-link></el-form-item
>
@@ -479,6 +487,7 @@
:data="previewRows"
border
max-height="440"
:row-class-name="({ row }) => (row._duplicate ? 'is-duplicate-row' : '')"
@selection-change="rowSelection = $event"
><el-table-column type="selection" width="55" /><el-table-column
type="index"
@@ -564,14 +573,18 @@
/><span v-else>{{ formatCell(row, column) }}</span></template
><span v-else>{{ formatCell(row, column) }}</span></template
></el-table-column
><el-table-column label="操作" width="180" fixed="right"
><el-table-column label="操作" width="220" fixed="right"
><template #default="{ row }"
><template v-if="row._editing"
><el-link type="primary" @click="saveRow(row)">保存</el-link
><el-link type="primary" @click="cancelRow(row)">取消</el-link></template
><template v-else
><el-link type="primary" @click="editRow(row)">编辑</el-link
><el-link type="danger" @click="deleteRow(row)">删除</el-link></template
><div class="waybill-import-create__row-actions"
><template v-if="row._editing"
><el-link type="primary" @click="saveRow(row)">保存</el-link
><el-link type="primary" @click="cancelRow(row)">取消</el-link></template
><template v-else
><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
></el-table-column
></el-table
@@ -579,6 +592,7 @@
</section>
<div v-if="createPage" class="waybill-import-create__footer">
<el-button @click="closeCreate">取消</el-button>
<el-button @click="saveDraft">保存草稿</el-button>
<el-button type="primary" @click="confirmImport">确认导入</el-button>
</div>
</component>
@@ -586,7 +600,7 @@
<script setup>
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 dayjs from 'dayjs';
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 emit = defineEmits(['update:modelValue', 'closed']);
const route = useRoute();
const router = useRouter();
const visible = computed({
get: () => props.modelValue,
@@ -656,10 +671,10 @@ const createDefaultForm = () => ({
file: null,
});
const form = reactive(createDefaultForm());
// 批量导入状态仅保留草稿与导入完成两种,与后端 importStatus 取值一致。
// 导入状态与运单业务状态一致:进行中、已完成。
const importStatusOptions = [
{ label: '进行中', value: 'draft' },
{ label: '导入完成', value: 'completed' },
{ label: '进行中', value: 'running' },
{ label: '已完成', value: 'completed' },
];
// 编辑草稿时明细已入库,此时不再强制重新上传附件。
const validateImportFile = (rule, value, callback) =>
@@ -705,6 +720,21 @@ const carrierContractsLoaded = ref(false),
hasCarrierContracts = ref(null);
const carrierTypes = ['承运商', '自运'];
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(() =>
carrierContractsLoaded.value && hasCarrierContracts.value === false ? ['自运'] : carrierTypes
@@ -733,7 +763,7 @@ const detailColumns = [
{ prop: 'batchNo', label: '运单批次号', minWidth: 150 },
{ prop: 'originalNo', label: '原始单号', editor: 'input', minWidth: 150 },
{ prop: 'loadingIdentifier', label: '配载标识号', editor: 'input', minWidth: 150 },
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号', editor: 'input', minWidth: 220 },
{ prop: 'vehicleNo', label: '车牌号/船班号/船次号/班次号', editor: 'input', minWidth: 220 },
{ prop: 'transportType', label: '运输方式', editor: 'transportType', minWidth: 150 },
{ prop: 'driverName', label: '司机/船长姓名', editor: 'driver', minWidth: 170 },
{ prop: 'driverPhone', label: '司机/船长手机号', editor: 'input', minWidth: 150 },
@@ -763,7 +793,7 @@ const detailColumns = [
{ prop: 'waybillIdentifier', label: '同一运单标识号', editor: 'input', minWidth: 150 },
];
const requiredDetailFields = [
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' },
{ prop: 'vehicleNo', label: '车牌号/船班号/船次号/班次号' },
{ prop: 'transportType', label: '运输方式' },
{ prop: 'departureAddress', label: '发货地址' },
{ prop: 'arrivalAddress', label: '到货地址' },
@@ -976,33 +1006,78 @@ const loadCommonCargoOptions = async row => {
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 [dictRes, cargoTypeRes, driverRes] = await Promise.all([
getDictionary({ code: 'transport_type' }),
getCargoTypeList(1, 9999),
getDriverList(1, 9999, {}),
]);
transportTypeOptions.value = extractRecords(dictRes).map(item => ({
label: item.dictValue || item.label,
value: item.dictKey || item.value,
}));
applyTransportTypeOptions(dictRes);
cargoTypeOptions.value = buildCargoTypeTree(extractRecords(cargoTypeRes));
cargoTypeFlatOptions.value = flattenCargoTypeOptions(cargoTypeOptions.value);
driverOptions.value = extractRecords(driverRes);
};
const ensureTransportTypeOptions = async () => {
if (transportTypeOptions.value.length) return;
const dictRes = await getDictionary({ code: 'transport_type' });
transportTypeOptions.value = extractRecords(dictRes).map(item => ({
label: item.dictValue || item.label,
value: item.dictKey || item.value,
}));
try {
applyTransportTypeOptions(await getDictionary({ code: 'transport_type' }));
} catch (error) {
transportTypeOptions.value = TRANSPORT_TYPE_DEFAULTS;
}
};
const transportTypeLabel = value => {
if (value === null || value === undefined || value === '') return '-';
return (
transportTypeOptions.value.find(item => String(item.value) === String(value))?.label || value
const source = value && typeof value === 'object' ? value : null;
const stored = source ? source.transportType : 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 res = await api.getImportBatches({ ...query, current: page.current, size: page.size });
@@ -1011,7 +1086,7 @@ const loadBatches = async () => {
};
onMounted(() => {
if (props.standalone) loadBatches();
if (props.createPage) openCreate();
if (props.createPage) openCreatePage();
});
watch(
() => props.modelValue,
@@ -1034,16 +1109,18 @@ const loadDetails = async () => {
details.value = res.data?.data?.records || [];
detailPage.total = res.data?.data?.total || 0;
};
const openCreate = async () => {
const openCreate = () => prepareCreateForm(true);
const prepareCreateForm = async generateCode => {
resetCreateForm();
createVisible.value = true;
const [projectRes, , optionRes, batchNo] = await Promise.all([
const requests = [
getProjectList(1, 9999, projectQueryParams),
loadEditorOptions(),
api.getImportOptions?.(),
generateBatchNo(),
]);
form.batchNo = batchNo;
];
if (generateCode) requests.push(generateBatchNo());
const [projectRes, , optionRes, batchNo] = await Promise.all(requests);
if (generateCode) form.batchNo = batchNo || '';
projects.value = projectRes.data?.data?.records || [];
const data = optionRes?.data?.data || {};
queryCarriers.value = data.carriers || [];
@@ -1082,27 +1159,74 @@ const loadBatchRows = async batchId => {
_key: `${id || 'row'}-${index}`,
batchNo: item.batchNo || form.batchNo,
}));
refreshDuplicateFlags();
rowSelection.value = [];
previewTab.value = 'all';
};
const editBatch = async row => {
await openCreate();
const editBatch = row => {
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 savedCarrierId = String(snapshot.carrierIds || snapshot.carrierId || '')
.split(',')
.map(item => item.trim())
.find(Boolean);
if (snapshot.projectId) await projectChange(snapshot.projectId);
const project = projects.value.find(item => String(item.id) === String(snapshot.projectId));
Object.assign(form, snapshot, {
projectId: snapshot.projectId || '',
id: snapshot.id || '',
batchNo: snapshot.batchNo || '',
projectId: project?.id || snapshot.projectId || '',
projectName: snapshot.projectName || form.projectName,
// 列表返回的是 importStatus,表单沿用 status 字段提交给后端。
status: snapshot.importStatus || 'completed',
status:
snapshot.importStatus === 'running' || snapshot.importStatus === 'completed'
? snapshot.importStatus
: 'completed',
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));
form.contractId = contract?.id || '';
form.carrierType = snapshot.carrierType || '承运商';
contractChange(form.contractId);
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 =
form.carrierType === '自运' ? snapshot.carrierName : snapshot.carrierContractId;
form.carrierType === '自运'
? snapshot.carrierName
: snapshot.carrierContractId || carrierOption?.value;
if (carrierValue) carrierChange(carrierValue);
await loadBatchRows(snapshot.id);
};
@@ -1170,6 +1294,12 @@ const carrierChange = value => {
if (!isSelfOperated.value) form.carrierContractId = item.carrierContractId || '';
};
const fileChange = async (file, list) => {
if (!projectInfoReady.value) {
ElMessage.warning('请先选择项目、客户合同和承运信息,再导入运单明细');
files.value = [];
form.file = null;
return;
}
if (file.raw?.size > 50 * 1024 * 1024) {
ElMessage.warning('单个文件大小不能超过50M');
files.value = [];
@@ -1190,7 +1320,7 @@ const fileChange = async (file, list) => {
const keyMap = {
originalNo: ['原始单号'],
loadingIdentifier: ['配载标识号'],
vehicleNo: ['车牌号/航班号/船号/班列号'],
vehicleNo: ['车牌号/船班号/船次号/班次号', '车牌号/航班号/船号/班列号'],
transportType: ['运输方式', '运输类型'],
driverName: ['司机/船长姓名', '司机/船长'],
driverPhone: ['司机/船长手机号'],
@@ -1227,7 +1357,6 @@ const fileChange = async (file, list) => {
const text = String(value ?? '').trim();
return text.slice(0, 10);
};
const count = new Map();
// 先解析数据,但不填充到 rows.value
const parsedRows = source.map((item, index) => {
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.planStartDate = normalizeImportDate(row.planStartDate);
row.planEndDate = normalizeImportDate(row.planEndDate);
const duplicateKey = JSON.stringify(Object.keys(keyMap).map(key => row[key]));
count.set(duplicateKey, (count.get(duplicateKey) || 0) + 1);
row._duplicateKey = duplicateKey;
return row;
});
parsedRows.forEach(row => {
row._duplicate = count.get(row._duplicateKey) > 1;
});
refreshDuplicateFlags(parsedRows);
// 调用后端校验接口,校验通过才填充表格
const isValid = await performValidation(parsedRows);
@@ -1271,8 +1395,39 @@ const fileRemove = () => {
/** 运单批量导入每次请求最多提交行数,避免网关 body 超限。 */
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 { _key, _duplicate, _duplicateKey, _editing, _editSnapshot, ...data } = row;
const { _key, _duplicate, _duplicateKey, _duplicateConfirmed, _duplicateSignature, _editing, _editSnapshot, ...data } = row;
return data;
};
@@ -1334,6 +1489,37 @@ const downloadImportFailures = async failureRows => {
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 => {
if (!parsedRows || !parsedRows.length) return false;
@@ -1361,15 +1547,7 @@ const performValidation = async parsedRows => {
}
if (errorMap.size) {
const failureRows = [];
errorMap.forEach((errorMessage, globalIndex) => {
failureRows.push({
...stripInternalRowFields(parsedRows[globalIndex]),
errorMessage,
});
});
await downloadImportFailures(failureRows);
ElMessage.warning('数据校验失败,已自动下载错误明细表,请修正后重新上传');
await exportCollectedImportFailures(parsedRows, errorMap);
return false;
}
@@ -1381,7 +1559,8 @@ const performValidation = async parsedRows => {
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) => {
if (column.editor === 'number') {
const [integer = '', decimal = ''] = String(value || '')
@@ -1456,6 +1635,7 @@ const editRow = row => {
const saveRow = row => {
row._editing = false;
delete row._editSnapshot;
refreshDuplicateFlags();
};
const cancelRow = row => {
if (row._editSnapshot) Object.assign(row, row._editSnapshot);
@@ -1464,11 +1644,13 @@ const cancelRow = row => {
};
const deleteRow = row => {
rows.value = rows.value.filter(item => item !== row);
refreshDuplicateFlags();
};
const removeSelectedRows = () => {
const keys = new Set(rowSelection.value.map(row => row._key));
rows.value = rows.value.filter(row => !keys.has(row._key));
rowSelection.value = [];
refreshDuplicateFlags();
};
const firstNotEmpty = (...values) => values.find(value => String(value ?? '').trim()) ?? '';
const normalizeOptionalImportNumber = (value, emptySentinels = []) => {
@@ -1534,17 +1716,27 @@ const saveDraft = async () => {
backToList();
};
const confirmImport = async () => {
// 导入状态选择草稿时不生成正式运单,等同于保存草稿。
if (form.status === 'draft') return saveDraft();
await formRef.value?.validate();
if (!validateImportRows()) return;
const count = rows.value.filter(row => row._duplicate).length;
if (count) await ElMessageBox.confirm(`当前导入有${count}条重复数据,是否确认导入?`, '提示');
if (form.importType === 'settlement')
if (count) await ElMessageBox.confirm(`当前导入有${count}条疑似重复数据,是否确认导入?`, '提示');
if (form.importType === 'settlement' && form.status === 'completed')
await ElMessageBox.confirm('确认后将同时生成运单、应收应付明细、计结算单,是否继续?', '提示');
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 || '';
let batchId = '';
try {
@@ -1660,6 +1852,15 @@ const confirmImport = async () => {
&__detail-panel {
margin-top: 12px;
}
&__import-title {
margin-top: 8px;
}
&__import-hint {
margin: 0 0 12px;
color: #909399;
font-size: 13px;
line-height: 20px;
}
&__form-actions {
display: flex;
justify-content: flex-end;
@@ -1672,6 +1873,14 @@ const confirmImport = async () => {
gap: 24px;
margin-bottom: 8px;
}
&__row-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
:deep(.is-duplicate-row > td.el-table__cell) {
background: #fff7e8;
}
&__file-tip {
margin: 0 24px;
color: #606266;
@@ -492,6 +492,7 @@
v-model="row.quantityUnit"
clearable
:disabled="dialogReadonly"
@change="handleTransportCargoQuantityUnitChange"
@visible-change="visible => visible && loadTaskQuantityUnitOptions()"
>
<el-option
@@ -1091,20 +1092,6 @@
</template>
</el-input>
</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-input
:model-value="taskFullFreightGroupAmount(group)"
@@ -1117,11 +1104,6 @@
</el-input>
</el-form-item>
</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-input
v-model="form.otherFeeTotal"
@@ -1133,6 +1115,11 @@
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
</el-input>
</el-form-item>
<el-form-item label="运费合计">
<el-input :model-value="taskFullFreightTotal" disabled>
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
</el-input>
</el-form-item>
</el-form>
</div>
</div>
@@ -1317,7 +1304,7 @@
@change="handleTaskCargoTypeChange"
/>
</el-form-item>
<el-form-item label="货物名称">
<el-form-item label="货物名称" required>
<el-autocomplete
v-model="form.cargoName"
placeholder="请输入货物名称"
@@ -1347,7 +1334,7 @@
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="型号">
<el-form-item label="型号" required>
<el-input
v-model="form.model"
placeholder="选择货物自动回填"
@@ -1355,7 +1342,7 @@
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="数量">
<el-form-item label="数量" required>
<el-input
v-model="form.quantity"
placeholder="请输入数量"
@@ -1962,7 +1949,7 @@
>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>航班号/船次号/班列号</span>
<span>船班号/船次号/班次号</span>
<strong>{{ detailRow.vehicleNo || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-detail-field">
@@ -2162,8 +2149,12 @@
label="批量补录"
name="batchSupplement"
>
<voucher-execution-detail
v-if="waybillIsCarrier(detailRow)"
:waybill-id="detailRow.id"
/>
<div
v-if="waybillVoucherFolderView"
v-else-if="waybillVoucherFolderView"
class="waybill-manage-page__voucher-folder-toolbar"
>
<el-button text type="primary" @click="backToWaybillVoucherFolders">
@@ -2172,6 +2163,7 @@
<span>{{ waybillVoucherFolderName(waybillVoucherFolder) }}</span>
</div>
<div
v-if="!waybillIsCarrier(detailRow)"
v-loading="waybillVoucherImagesLoading"
class="waybill-manage-page__voucher-image-grid"
>
@@ -3135,6 +3127,7 @@ import {
getVoucherImages as getProcessConfigVoucherImages,
} from '@/api/business/process-config';
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 { getList as getDriverList } from '@/api/transportCapacity/driver';
import {
@@ -3332,6 +3325,7 @@ export default {
WarnTriangleFilled,
ElImageViewer,
OpenFileViewer,
VoucherExecutionDetail,
},
name: 'WaybillManagePage',
props: {
@@ -3804,7 +3798,7 @@ export default {
return !this.isRoadTransport;
},
transportVehicleNoLabel() {
return this.isRoadTransport ? '车牌号' : '船/航/班列号';
return this.isRoadTransport ? '车牌号' : '船班号/船次号/班次号';
},
actionButtonLikeSearch() {
return this.config.actionButtonLikeSearch === true;
@@ -3873,9 +3867,6 @@ export default {
if (!total) return '';
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(3)));
},
fixedQuantityUnit() {
return WAYBILL_QUANTITY_UNIT;
},
taskFreightAmount() {
const quantity = Number(this.taskCargoQuantityTotal || this.form.quantity || 0);
const unitPrice = Number(this.form.unitPrice || 0);
@@ -4617,7 +4608,7 @@ export default {
return text.slice(0, 10);
},
waybillVehicleNoLabel(row = {}) {
return this.waybillIsRoadTransport(row) ? '车牌号' : '船/航/班列号';
return this.waybillIsRoadTransport(row) ? '车牌号' : '船班号/船次号/班次号';
},
waybillUnitPrice(row = {}) {
const price = row.unitPrice || row.price || '';
@@ -6408,14 +6399,11 @@ export default {
if (!rows.length) return false;
const quantityUnits = rows.map(row => String(row.quantityUnit || '').trim());
if (quantityUnits.some(unit => !unit) || new Set(quantityUnits).size !== 1) return false;
const quantityUnit = quantityUnits[0];
return rows.every(row => {
const priceUnit = String(row.priceUnit || '').trim();
const separatorIndex = Math.max(priceUnit.lastIndexOf('/'), priceUnit.lastIndexOf('/'));
const priceQuantityUnit =
separatorIndex >= 0 ? priceUnit.slice(separatorIndex + 1).trim() : priceUnit;
return !this.isBillingFieldEmpty(row.unitPrice) && priceQuantityUnit === quantityUnit;
});
const expectedPriceUnit = this.resolvePriceUnitByQuantityUnit(quantityUnits[0]);
if (!expectedPriceUnit) return false;
return rows.every(
row => !this.isBillingFieldEmpty(row.unitPrice) && String(row.priceUnit || '').trim() === expectedPriceUnit
);
},
taskFullFreightQuantity(index) {
return this.formatTaskFullFreightNumber(this.transportCargoRows[index]?.quantity || 0);
@@ -6431,10 +6419,6 @@ export default {
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 = {}) {
const prices = (group.rows || [])
.map(cargo => String(cargo.unitPrice ?? '').trim())
@@ -6443,7 +6427,11 @@ export default {
return prices[0];
},
taskFullFreightGroupPriceUnit(group = {}) {
return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : '');
return (
group.rows?.[0]?.priceUnit ||
this.resolvePriceUnitByQuantityUnit(group.rows?.[0]?.quantityUnit) ||
''
);
},
taskFullFreightGroupAmount(group = {}) {
const total = (group.rows || []).reduce(
@@ -7174,6 +7162,11 @@ export default {
requiredFields.push(['driverName', '司机']);
requiredFields.push(['driverPhone', '手机号']);
}
if (!this.isTaskFullMode) {
requiredFields.push(['cargoName', '货物名称']);
requiredFields.push(['model', '型号']);
requiredFields.push(['quantity', '数量']);
}
const emptyField = requiredFields.find(([prop]) => this.isBillingFieldEmpty(row[prop]));
if (emptyField) {
this.$message.warning(`请选择或输入${emptyField[1]}`);
@@ -7208,7 +7201,7 @@ export default {
}
} else {
const invalidLengthField = [
['vehicleNo', '船/航/班列号', 30],
['vehicleNo', '船班号/船次号/班次号', 30],
['captainName', '船长', 20],
['cabinNo', '舱位', 30],
['containerNo', '箱号', 30],
@@ -7466,6 +7459,24 @@ export default {
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 3)}` : parts[0];
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) {
const path = this.normalizeCargoTypePath(value);
const cargoType = this.findCargoTypeByPath(path);
@@ -9787,10 +9798,16 @@ export default {
}
&--road {
grid-template-columns: repeat(3, minmax(0, 1fr));
column-gap: 56px;
grid-template-columns: repeat(4, minmax(0, 1fr));
column-gap: 24px;
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) {
width: 112px;
}
+22 -4
View File
@@ -117,7 +117,7 @@
/>
</el-select>
</el-form-item>
<el-form-item label="车/船/航班/班列">
<el-form-item label="车牌号/船班号/船次号/班次号">
<el-input v-model="searchForm.vehicleNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="原始单号">
@@ -719,8 +719,13 @@
</div>
</el-tab-pane>
<el-tab-pane v-if="hasRelatedVoucher" label="批量补录" name="batchSupplement">
<voucher-execution-detail
v-if="isCarrierExecutionVoucher"
:key="processWaybillId"
:waybill-id="processWaybillId"
/>
<div
v-if="voucherFolderView"
v-else-if="voucherFolderView"
class="loading-detail__voucher-folder-toolbar"
>
<el-button text type="primary" @click="backToVoucherFolders">
@@ -728,7 +733,11 @@
</el-button>
<span>{{ voucherFolderName(voucherFolder) }}</span>
</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
v-for="(image, index) in voucherImages"
:key="
@@ -936,7 +945,7 @@
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="车/船/航班/班列">
<el-form-item label="车牌号/船班号/船次号/班次号">
<el-input
v-model="candidateQuery.vehicleNo"
clearable
@@ -1755,6 +1764,7 @@ import {
getDetail as getProcessConfigDetail,
getVoucherImages as getProcessConfigVoucherImages,
} from '@/api/business/process-config';
import VoucherExecutionDetail from './components/voucher-execution-detail.vue';
import { getList as getCommonAddressList } from '@/api/base/common-address';
import {
carrierTypeOptions,
@@ -1965,6 +1975,7 @@ export default {
components: {
Rank,
Search,
VoucherExecutionDetail,
},
data() {
return {
@@ -2118,6 +2129,13 @@ export default {
isSelfCarrierDetail() {
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() {
return normalizeLoadingMileage(this.dialogForm.mileage);
},
+6 -1
View File
@@ -1,12 +1,17 @@
<template>
<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 />
</basic-container>
</template>
<script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import WaybillImportDialog from './components/waybill-import-dialog.vue';
const route = useRoute();
const pageTitle = computed(() => (route.query.id ? '编辑导入' : '新建导入'));
</script>
<style scoped lang="scss">