Files
tms-erp-web/src/views/business/components/waybill-import-dialog.vue
T
2026-08-14 02:49:46 +08:00

387 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<el-dialog v-model="visible" title="导入运单" width="90%" append-to-body destroy-on-close>
<div class="waybill-import-toolbar">
<div class="waybill-import-toolbar__search">
<el-form :inline="true" :model="query" label-width="auto">
<el-form-item label="运单批次号"><el-input v-model="query.batchNo" clearable /></el-form-item>
<el-form-item label="承运商"><el-select v-model="query.carrierId" clearable filterable><el-option v-for="item in carriers" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-form-item label="创建时间"><el-date-picker v-model="query.createTimeRange" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" /></el-form-item>
<el-form-item label="创建人"><el-select v-model="query.createUser" clearable filterable><el-option v-for="item in creators" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
</el-form>
<div class="waybill-import-toolbar__search-actions"><el-button type="primary" @click="loadBatches">查询</el-button><el-button @click="resetQuery">重置</el-button></div>
</div>
<div class="waybill-import-toolbar__actions"><el-button type="primary" @click="openCreate">新建导入</el-button><el-button type="danger" plain :disabled="!selected.length" @click="removeBatches">批量删除</el-button></div>
</div>
<el-table :data="batches" border @selection-change="selected = $event">
<el-table-column type="selection" width="50" /><el-table-column type="index" label="序号" width="70" />
<el-table-column prop="batchNo" label="运单批次号" min-width="150" /><el-table-column prop="projectName" label="项目名称" min-width="150" />
<el-table-column prop="carrierName" label="承运商" min-width="140" /><el-table-column prop="carrierType" label="承运类型" min-width="120" />
<el-table-column prop="waybillCount" label="运单数" width="90" /><el-table-column prop="importTypeName" label="导入方式" width="100" />
<el-table-column prop="createTime" label="创建时间" min-width="170" sortable/><el-table-column prop="updateTime" label="更新时间" min-width="170" sortable/>
<el-table-column prop="statusName" label="状态" width="100" /><el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }"><el-link type="primary" @click="openDetail(row)">查看</el-link><el-link type="primary" @click="editBatch(row)">编辑</el-link><el-link type="danger" @click="removeBatch(row)">删除</el-link></template>
</el-table-column>
</el-table>
<el-pagination v-model:current-page="page.current" v-model:page-size="page.size" layout="total, prev, pager, next, sizes" :page-sizes="[10, 20, 50]" :total="page.total" @current-change="loadBatches" @size-change="loadBatches" />
</el-dialog>
<el-dialog v-model="detailVisible" title="导入运单详情" width="90%" append-to-body>
<el-form :inline="true" :model="detailQuery"><el-form-item label="车牌号/船号"><el-input v-model="detailQuery.vehicleNo" clearable /></el-form-item><el-form-item label="货物名称"><el-input v-model="detailQuery.cargoName" clearable /></el-form-item><el-button type="primary" @click="loadDetails">查询</el-button></el-form>
<el-table :data="details" border><el-table-column type="index" label="序号" width="65" /><el-table-column prop="batchNo" label="运单批次号" min-width="140" /><el-table-column prop="originalNo" label="原始单号" min-width="130" /><el-table-column prop="vehicleNo" label="车牌号/航班号/船号/班列号" min-width="190" /><el-table-column prop="driverName" label="司机/船长" min-width="120" /><el-table-column prop="transportType" label="运输方式" width="110" /><el-table-column prop="cargoName" label="货物名称" width="130" /><el-table-column prop="cargoType" label="货物类型" width="120" /><el-table-column prop="quantity" label="重量" width="90" /><el-table-column prop="departureAddress" label="发货地址" min-width="220" show-overflow-tooltip /><el-table-column prop="departureContact" label="发货联系人" width="110" /><el-table-column prop="departurePhone" label="发货联系人电话" width="140" /><el-table-column prop="arrivalAddress" label="到货地址" min-width="220" show-overflow-tooltip /><el-table-column prop="arrivalContact" label="收货联系人" width="110" /><el-table-column prop="arrivalPhone" label="收货联系人电话" width="140" /><el-table-column prop="startDate" label="开始时间" width="160" sortable/><el-table-column prop="endDate" label="结束时间" width="160" sortable/><el-table-column prop="unitPrice" label="单价" width="90" /><el-table-column prop="freight" label="运费" width="100" /><el-table-column prop="otherFeeTotal" label="其他费用合计" width="120" /><el-table-column prop="freightTotal" label="运费合计" width="100" /><el-table-column prop="remark" label="备注" min-width="160" /><el-table-column label="操作" width="80">-</el-table-column></el-table>
<el-pagination v-model:current-page="detailPage.current" v-model:page-size="detailPage.size" layout="total, prev, pager, next, sizes" :page-sizes="[10, 20, 50]" :total="detailPage.total" @current-change="loadDetails" @size-change="loadDetails" />
</el-dialog>
<el-dialog v-model="createVisible" title="新建导入" width="85%" append-to-body destroy-on-close class="waybill-import-create">
<section class="waybill-import-create__form-panel">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="auto" class="archive-form">
<el-row :gutter="20"><el-col :span="6"><el-form-item label="项目" prop="projectId"><el-select v-model="form.projectId" filterable clearable placeholder="模糊查询后选择" @change="projectChange"><el-option v-for="item in projects" :key="item.id" :label="item.projectName" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="客户" prop="customerName"><el-input v-model="form.customerName" disabled placeholder="自动带出" /></el-form-item></el-col><el-col :span="6"><el-form-item label="客户合同" prop="contractId"><el-select v-model="form.contractId" filterable clearable placeholder="自动带出" @change="contractChange"><el-option v-for="item in contracts" :key="item.id" :label="item.contractName" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="承运类型" prop="carrierType"><el-select v-model="form.carrierType"><el-option label="承运商" value="承运商" /><el-option label="自运" value="自运" /><el-option label="网货平台" value="网货平台" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="承运商"><el-select v-model="form.carrierIds" multiple filterable placeholder="自动带出,多个需选择"><el-option v-for="item in carriers" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="导入状态" prop="status"><el-select v-model="form.status"><el-option label="完成" value="completed" /><el-option label="进行中" value="processing" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="导入类型" prop="importType"><el-select v-model="form.importType"><el-option label="运单" value="waybill" /><el-option label="结算单" value="settlement" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="计划名称"><el-select v-model="form.planId" clearable filterable placeholder="请选择"><el-option v-for="item in plans" :key="item.id" :label="item.planName" :value="item.id" /></el-select></el-form-item></el-col></el-row>
<el-form-item label="运单明细表" prop="file"><el-upload action="#" accept=".xls,.xlsx" :auto-upload="false" :limit="1" :file-list="files" :on-change="fileChange" :on-remove="fileRemove"><el-button type="primary">添加附件</el-button></el-upload><span class="waybill-import-create__file-tip">请上传运单明细表仅支持 excel 格式</span><el-link type="primary" @click="downloadTemplate">下载模板</el-link></el-form-item>
</el-form>
<div class="waybill-import-create__form-actions"><el-button @click="createVisible = false">关闭</el-button><el-button @click="saveDraft">保存草稿</el-button><el-button type="primary" @click="confirmImport">确认导入</el-button></div>
</section>
<section class="waybill-import-create__detail-panel" v-if="rows.length"><div class="waybill-import-create__detail-head"><div class="dialog-section-title">导入数据明细</div><el-button type="danger" plain :disabled="!rowSelection.length" @click="removeSelectedRows">批量删除</el-button></div><el-tabs v-model="previewTab"><el-tab-pane :label="`运单数(${rows.length}`" name="all" /><el-tab-pane :label="`疑似重复(${duplicateRows.length}`" name="duplicate" /></el-tabs><el-table :data="previewRows" border max-height="440" @selection-change="rowSelection = $event"><el-table-column type="selection" width="55" /><el-table-column type="index" label="序号" width="65" /><el-table-column v-for="column in detailColumns" :key="column.prop" :prop="column.prop" :label="column.label" :min-width="column.minWidth || 150" show-overflow-tooltip><template #default="{ row }"><template v-if="row._editing"><el-select v-if="column.editor === 'transportType'" v-model="row.transportType" clearable filterable placeholder="请选择"><el-option v-for="item in transportTypeOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select><el-select v-else-if="column.editor === 'driver'" v-model="row.driverName" clearable filterable placeholder="请选择" @change="value => handleEditorChange(column, row, value)"><el-option v-for="item in driverOptions" :key="item.id || item.driverName || item.name" :label="item.driverName || item.name" :value="item.driverName || item.name" /></el-select><el-cascader v-else-if="column.editor === 'cargoType'" :model-value="resolveCargoTypePath(row)" :options="cargoTypeOptions" :props="{ label: 'cargoName', value: 'id', children: 'children', emitPath: true }" clearable filterable placeholder="请选择" @change="value => handleEditorChange(column, row, value)" /><el-select v-else-if="column.editor === 'cargoName'" v-model="row.cargoName" clearable filterable placeholder="请选择" @visible-change="visible => visible && loadCommonCargoOptions(row)" @change="value => handleEditorChange(column, row, value)"><el-option v-for="item in getCommonCargoOptions(row)" :key="item.id || item.cargoName || item.name" :label="formatCargoTypeLabel(item)" :value="formatCargoTypeLabel(item)" /></el-select><el-date-picker v-else-if="column.editor === 'date'" v-model="row[column.prop]" type="date" format="YYYY-MM-DD" value-format="YYYY-MM-DD" placeholder="请选择" /><el-input v-else-if="column.editor === 'textarea'" v-model="row[column.prop]" type="textarea" :autosize="{ minRows: 1, maxRows: 3 }" :placeholder="`请输入${column.label}`" /><el-input v-else-if="column.editor === 'number'" :model-value="row[column.prop]" inputmode="decimal" :placeholder="`请输入${column.label}`" @input="value => updateEditorValue(column, row, value)" /><el-input v-else v-model="row[column.prop]" :placeholder="`请输入${column.label}`" /></template><span v-else>{{ formatCell(row, column) }}</span></template></el-table-column><el-table-column label="操作" width="180" 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></template></el-table-column></el-table></section>
</el-dialog>
</template>
<script setup>
import { computed, reactive, ref } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
import { getList as getContractList } from '@/api/business/contract-manage';
import { getList as getProjectList } from '@/api/business/project-apply';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { getDictionary } from '@/api/system/dictbiz';
import * as api from '@/api/business/waybill-manage';
import { downloadXls } from '@/utils/util';
const props = defineProps({ modelValue: Boolean });
const emit = defineEmits(['update:modelValue']);
const visible = computed({ get: () => props.modelValue, set: value => emit('update:modelValue', value) });
const createVisible = ref(false), detailVisible = ref(false), formRef = ref();
const query = reactive({ batchNo: '', carrierId: '', createUser: '', createTimeRange: [] });
const detailQuery = reactive({ vehicleNo: '', cargoName: '' });
const form = reactive({ projectId: '', customerName: '', contractId: '', contractName: '', carrierType: '承运商', carrierIds: [], status: 'completed', importType: 'waybill', planId: '', file: null });
const rules = { projectId: [{ required: true, message: '请选择项目' }], customerName: [{ required: true, message: '客户不能为空' }], contractId: [{ required: true, message: '请选择客户合同' }], carrierType: [{ required: true, message: '请选择承运类型' }], status: [{ required: true, message: '请选择导入状态' }], importType: [{ required: true, message: '请选择导入类型' }], file: [{ required: true, message: '请上传明细表' }] };
const batches = ref([]), details = ref([]), rows = ref([]), selected = ref([]), files = ref([]), projects = ref([]), contracts = ref([]), carriers = ref([]), creators = ref([]), plans = ref([]);
const transportTypeOptions = ref([]), cargoTypeOptions = ref([]), cargoTypeFlatOptions = ref([]), commonCargoOptionsMap = ref({}), commonCargoLoadingMap = ref({}), driverOptions = ref([]);
const rowSelection = ref([]), previewTab = ref('all');
const duplicateRows = computed(() => rows.value.filter(row => row._duplicate));
const previewRows = computed(() => previewTab.value === 'duplicate' ? duplicateRows.value : rows.value);
const page = reactive({ current: 1, size: 10, total: 0 }), detailPage = reactive({ current: 1, size: 10, total: 0 });
const detailColumns = [
{ prop: 'batchNo', label: '运单批次号', editor: 'input', minWidth: 150 },
{ prop: 'originalNo', label: '原始单号', editor: 'input', minWidth: 150 },
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号', editor: 'input', minWidth: 220 },
{ prop: 'driverName', label: '司机/船长', editor: 'driver', minWidth: 170 },
{ prop: 'transportType', label: '运输方式', editor: 'transportType', minWidth: 150 },
{ prop: 'cargoType', label: '货物类型', editor: 'cargoType', minWidth: 220 },
{ prop: 'cargoName', label: '货物名称', editor: 'cargoName', minWidth: 220 },
{ prop: 'quantity', label: '重量', editor: 'number', minWidth: 130 },
{ prop: 'departureAddress', label: '发货地址', editor: 'textarea', minWidth: 260 },
{ prop: 'departureContact', label: '发货联系人', editor: 'input', minWidth: 140 },
{ prop: 'departurePhone', label: '发货联系人电话', editor: 'input', minWidth: 160 },
{ prop: 'arrivalAddress', label: '到货地址', editor: 'textarea', minWidth: 260 },
{ prop: 'arrivalContact', label: '收货联系人', editor: 'input', minWidth: 140 },
{ prop: 'arrivalPhone', label: '收货联系人电话', editor: 'input', minWidth: 160 },
{ prop: 'startDate', label: '开始时间', editor: 'date', minWidth: 150 },
{ prop: 'endDate', label: '结束时间', editor: 'date', minWidth: 150 },
{ prop: 'unitPrice', label: '单价', editor: 'number', minWidth: 130 },
{ prop: 'freight', label: '运费', editor: 'number', minWidth: 130 },
{ prop: 'otherFeeTotal', label: '其他费用合计', editor: 'number', minWidth: 150 },
{ prop: 'freightTotal', label: '运费合计', editor: 'number', minWidth: 130 },
{ prop: 'remark', label: '备注', editor: 'textarea', minWidth: 180 },
];
const requiredDetailFields = [{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' }, { prop: 'driverName', label: '司机/船长' }, { prop: 'transportType', label: '运输方式' }, { prop: 'cargoName', label: '货物名称' }, { prop: 'cargoType', label: '货物类型' }, { prop: 'departureAddress', label: '发货地址' }, { prop: 'departureContact', label: '发货联系人' }, { prop: 'departurePhone', label: '发货联系人电话' }, { prop: 'arrivalAddress', label: '到货地址' }, { prop: 'arrivalContact', label: '到货联系人' }, { prop: 'arrivalPhone', label: '收货联系人电话' }, { prop: 'startDate', label: '开始时间' }, { prop: 'endDate', label: '结束时间' }, { prop: 'unitPrice', label: '单价' }, { prop: 'freight', label: '运费' }];
const projectQueryParams = { approvalStatuses: 'approved,change_approved' };
const extractRecords = res => {
const data = res?.data || res;
if (Array.isArray(data)) return data;
if (Array.isArray(data?.records)) return data.records;
if (Array.isArray(data?.data)) return data.data;
if (Array.isArray(data?.data?.records)) return data.data.records;
return [];
};
const formatCargoTypeLabel = item => item?.cargoName || item?.name || item?.typeName || item?.label || '';
const buildCargoTypeTree = cargoTypes => {
const flatCargoTypes = [];
const collectCargoTypes = list => {
(list || []).forEach(item => {
flatCargoTypes.push({ ...item, children: undefined });
if (item.children?.length) collectCargoTypes(item.children);
});
};
collectCargoTypes(cargoTypes);
const nodeMap = new Map();
const codeMap = new Map();
flatCargoTypes.forEach(item => {
const id = item.id ?? item.cargoCode ?? item.code;
if (id === undefined || id === null) return;
const node = {
...item,
id: String(id),
cargoName: formatCargoTypeLabel(item),
cargoCode: item.cargoCode || item.code || '',
parentId: item.parentId === undefined || item.parentId === null ? '' : String(item.parentId),
parentCargoCode: item.parentCargoCode || item.parentCode || '',
children: [],
};
nodeMap.set(node.id, node);
if (node.cargoCode) codeMap.set(String(node.cargoCode), node);
});
const rootNodes = [];
nodeMap.forEach(node => {
const parent = nodeMap.get(node.parentId) || codeMap.get(String(node.parentCargoCode || '')) || codeMap.get(String(node.parentId || ''));
if (parent && parent !== node && Number(node.typeLevel) !== 1) parent.children.push(node);
else rootNodes.push(node);
});
const normalize = (cargoType, level = 1, parentPath = []) => {
if (level > 2) return null;
const id = String(cargoType.id);
const path = [...parentPath, id];
const children = level === 1 ? (cargoType.children || []).map(item => normalize(item, level + 1, path)).filter(Boolean) : [];
return { ...cargoType, id, cargoName: formatCargoTypeLabel(cargoType), path, leaf: level === 2, children: children.length ? children : undefined };
};
return rootNodes.map(item => normalize(item, 1)).filter(Boolean);
};
const flattenCargoTypeOptions = options => {
const result = [];
const collect = list => (list || []).forEach(item => {
result.push(item);
if (item.children?.length) collect(item.children);
});
collect(options);
return result;
};
const getCargoTypePathLabels = path => {
let options = cargoTypeOptions.value;
const labels = [];
(path || []).forEach(value => {
const option = (options || []).find(item => String(item.id) === String(value));
if (!option) return;
labels.push(option.cargoName || '');
options = option.children || [];
});
return labels.filter(Boolean);
};
const findCargoTypeByPath = path => {
const id = Array.isArray(path) ? path[path.length - 1] : path;
return cargoTypeFlatOptions.value.find(item => String(item.id) === String(id));
};
const resolveCargoTypePath = row => {
if (Array.isArray(row.cargoTypePath) && row.cargoTypePath.length) return row.cargoTypePath;
const cargoType = cargoTypeFlatOptions.value.find(item => [item.cargoName, item.name, item.typeName, item.cargoCode].some(value => String(value || '') === String(row.cargoType || row.cargoTypeCode || '')));
return cargoType?.path || [];
};
const commonCargoKey = row => {
const path = resolveCargoTypePath(row);
const cargoType = findCargoTypeByPath(path);
return String(cargoType?.cargoCode || cargoType?.code || path.join('/'));
};
const getCommonCargoOptions = row => commonCargoOptionsMap.value[commonCargoKey(row)] || [];
const loadCommonCargoOptions = async row => {
const path = resolveCargoTypePath(row);
const cargoType = findCargoTypeByPath(path);
const key = commonCargoKey(row);
if (!key || commonCargoLoadingMap.value[key]) return;
commonCargoLoadingMap.value = { ...commonCargoLoadingMap.value, [key]: true };
try {
const labels = getCargoTypePathLabels(path);
const res = await getCommonCargoList(1, 500, {
secondCargoTypeName: labels[1] || row.cargoType || '',
secondCargoTypeCode: cargoType?.cargoCode || cargoType?.code || '',
allDept: 0,
});
commonCargoOptionsMap.value = { ...commonCargoOptionsMap.value, [key]: extractRecords(res) };
} finally {
commonCargoLoadingMap.value = { ...commonCargoLoadingMap.value, [key]: false };
}
};
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,
}));
cargoTypeOptions.value = buildCargoTypeTree(extractRecords(cargoTypeRes));
cargoTypeFlatOptions.value = flattenCargoTypeOptions(cargoTypeOptions.value);
driverOptions.value = extractRecords(driverRes);
};
const loadBatches = async () => { const res = await api.getImportBatches({ ...query, current: page.current, size: page.size }); batches.value = res.data?.data?.records || []; page.total = res.data?.data?.total || 0; };
const resetQuery = () => { Object.assign(query, { batchNo: '', carrierId: '', createUser: '', createTimeRange: [] }); page.current = 1; loadBatches(); };
const loadDetails = async () => { const res = await api.getImportDetails({ batchId: detailQuery.batchId, ...detailQuery, current: detailPage.current, size: detailPage.size }); details.value = res.data?.data?.records || []; detailPage.total = res.data?.data?.total || 0; };
const openCreate = async () => {
createVisible.value = true;
const [projectRes] = await Promise.all([getProjectList(1, 9999, projectQueryParams), loadEditorOptions()]);
projects.value = projectRes.data?.data?.records || [];
api.getImportOptions?.().then(res => {
const data = res.data?.data || {};
contracts.value = data.contracts || [];
carriers.value = data.carriers || [];
creators.value = data.creators || [];
plans.value = data.plans || [];
});
};
const openDetail = row => { detailQuery.batchId = row.id; detailVisible.value = true; loadDetails(); };
const editBatch = row => { openCreate(); Object.assign(form, row); };
const removeBatches = () => api.removeImportBatches(selected.value.map(item => item.id).join(',')).then(loadBatches);
const removeBatch = row => api.removeImportBatches(row.id).then(loadBatches);
const projectChange = async id => {
const item = projects.value.find(row => row.id === id);
form.customerName = item?.customerName || item?.customerNames || item?.customer || '';
form.contractId = '';
form.contractName = '';
contracts.value = [];
if (!id) return;
const res = await getContractList(1, 9999, {
projectId: id,
projectName: item?.projectName,
});
contracts.value = res.data?.data?.records || [];
if (contracts.value.length) {
form.contractId = contracts.value[0].id;
form.contractName = contracts.value[0].contractName || '';
}
};
const contractChange = id => {
const item = contracts.value.find(row => row.id === id);
form.contractName = item?.contractName || '';
};
const fileChange = async (file, list) => {
files.value = list.slice(-1); form.file = file.raw;
const XLSX = await import('xlsx');
const workbook = XLSX.read(await file.raw.arrayBuffer(), { type: 'array', cellDates: true });
const source = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], { defval: '' }).map(item => Object.fromEntries(Object.entries(item).map(([key, value]) => [key.replace(/^\*/, ''), value])));
const keyMap = { originalNo: ['原始单号'], vehicleNo: ['车牌号/航班号/船号/班列号'], driverName: ['司机/船长'], transportType: ['运输类型'], cargoName: ['货物名称'], cargoType: ['货物类型'], quantity: ['重量'], departureAddress: ['发货地址'], departureContact: ['发货联系人'], departurePhone: ['发货联系人电话'], arrivalAddress: ['到货地址'], arrivalContact: ['到货联系人', '收货联系人'], arrivalPhone: ['收货联系人电话'], startDate: ['开始时间'], endDate: ['结束时间'], unitPrice: ['单价'], freight: ['运费'], otherFeeTotal: ['其他费用合计'], freightTotal: ['运费合计'], remark: ['备注'] };
const normalizeImportDate = value => {
if (value instanceof Date && !Number.isNaN(value.getTime())) {
const pad = number => String(number).padStart(2, '0');
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
}
const text = String(value ?? '').trim();
return text.slice(0, 10);
};
const count = new Map();
rows.value = source.map((item, index) => { const row = { _key: `${Date.now()}-${index}`, batchNo: '', ...item }; Object.entries(keyMap).forEach(([key, labels]) => { row[key] = labels.map(label => item[label]).find(value => String(value ?? '').trim()) ?? item[key] ?? ''; }); row.startDate = normalizeImportDate(row.startDate); row.endDate = normalizeImportDate(row.endDate); const duplicateKey = JSON.stringify(Object.keys(keyMap).map(key => row[key])); count.set(duplicateKey, (count.get(duplicateKey) || 0) + 1); row._duplicateKey = duplicateKey; return row; });
rows.value.forEach(row => { row._duplicate = count.get(row._duplicateKey) > 1; });
};
const fileRemove = () => { files.value = []; form.file = null; };
const formatCell = (row, column) => row[column.prop] || '';
const updateEditorValue = (column, row, value) => {
if (column.editor === 'number') {
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
row[column.prop] = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
return;
}
row[column.prop] = value;
};
const handleEditorChange = (column, row, value) => {
if (column.editor === 'driver') {
const driver = driverOptions.value.find(item => [item.driverName, item.name].some(name => String(name || '') === String(value || '')));
row.driverName = value || '';
if (driver) row.driverPhone = driver.mobile || driver.phone || driver.driverPhone || row.driverPhone || '';
}
if (column.editor === 'cargoType') {
const path = Array.isArray(value) ? value : [];
const cargoType = findCargoTypeByPath(path);
const labels = getCargoTypePathLabels(path);
row.cargoTypePath = path;
row.cargoType = labels.at(-1) || '';
row.cargoTypeCode = cargoType?.cargoCode || cargoType?.code || cargoType?.id || '';
row.cargoName = '';
row.specification = '';
row.model = '';
loadCommonCargoOptions(row);
}
if (column.editor === 'cargoName') {
const cargo = getCommonCargoOptions(row).find(item => formatCargoTypeLabel(item) === value);
row.cargoName = value || '';
if (cargo) {
row.specification = cargo.specification || cargo.spec || row.specification || '';
row.model = cargo.model || cargo.modelName || row.model || '';
row.packageType = cargo.packageType || row.packageType || '';
row.brand = cargo.brand || row.brand || '';
}
}
};
const downloadTemplate = async () => {
try {
const res = await api.exportImportTemplate();
downloadXls(res.data, '运单批量导入模板.xlsx');
ElMessage.success('模板下载成功');
} catch (error) {
ElMessage.error('模板下载失败');
}
};
const validateImportRows = () => {
if (!rows.value.length) {
ElMessage.warning('请上传至少一条运单明细');
return false;
}
for (const [index, row] of rows.value.entries()) {
const missingField = requiredDetailFields.find(field => !String(row[field.prop] ?? '').trim());
if (missingField) {
ElMessage.warning(`第${index + 1}${missingField.label}不能为空`);
return false;
}
}
return true;
};
const editRow = row => { row._editSnapshot = JSON.parse(JSON.stringify(row)); row._editing = true; };
const saveRow = row => { row._editing = false; delete row._editSnapshot; };
const cancelRow = row => { if (row._editSnapshot) Object.assign(row, row._editSnapshot); row._editing = false; delete row._editSnapshot; };
const deleteRow = row => { rows.value = rows.value.filter(item => item !== row); };
const removeSelectedRows = () => { const keys = new Set(rowSelection.value.map(row => row._key)); rows.value = rows.value.filter(row => !keys.has(row._key)); rowSelection.value = []; };
const saveDraft = () => api.saveImportDraft({ ...form, rows: rows.value });
const confirmImport = async () => { 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') await ElMessageBox.confirm('确认后将同时生成运单、应收应付明细、计结算单,是否继续?', '提示'); await api.confirmImport({ ...form, rows: rows.value }); ElMessage.success('导入成功'); createVisible.value = false; loadBatches(); };
</script>
<style scoped lang="scss">
.waybill-import-toolbar {
margin-bottom: 12px;
&__search {
display: flex;
align-items: flex-start;
gap: 12px;
.el-form {
flex: 1;
margin-bottom: 0;
}
}
&__search-actions {
display: flex;
flex: none;
gap: 8px;
}
&__actions { margin-top: 8px; }
}
.waybill-import-create {
&__form-panel,
&__detail-panel {
border: 1px solid #eff1f7;
padding: 20px 24px;
}
&__detail-panel { margin-top: 12px; }
&__form-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
&__detail-head { display: flex; align-items: center; gap: 24px; margin-bottom: 8px; }
&__file-tip { margin: 0 24px; color: #606266; }
:deep(.el-table .el-input),
:deep(.el-table .el-select),
:deep(.el-table .el-cascader),
:deep(.el-table .el-date-editor) {
width: 100%;
}
}
</style>