Files
tms-erp-web/src/views/business/components/waybill-import-dialog.vue
b2894lxlx 433c611195 1、完善发货模板
2、完善运输计划
3、完善运单管理
2026-08-05 15:52:24 +08:00

168 lines
19 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" /><el-table-column prop="updateTime" label="更新时间" min-width="170" />
<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" /><el-table-column prop="endDate" label="结束时间" width="160" /><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">
<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="自动带出"><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="140" show-overflow-tooltip /><el-table-column label="操作" width="180" fixed="right"><template #default="{ row }"><el-link type="primary" @click="cancelRow(row)">取消</el-link><el-link type="primary" @click="saveRow(row)">保存</el-link><el-link type="primary" @click="editRow(row)">编辑</el-link><el-link type="danger" @click="deleteRow(row)">删除</el-link></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 getProjectList } from '@/api/business/project-apply';
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: '', 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 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: '运单批次号' }, { prop: 'originalNo', label: '原始单号' }, { prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' }, { prop: 'driverName', label: '司机/船长' }, { prop: 'transportType', label: '运输类型' }, { prop: 'cargoName', label: '货物名称' }, { prop: 'cargoType', label: '货物类型' }, { prop: 'quantity', 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: '运费' }, { prop: 'otherFeeTotal', label: '其他费用合计' }, { prop: 'freightTotal', label: '运费合计' }, { prop: 'remark', label: '备注' }];
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 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 getProjectList(1, 9999, {});
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 = id => { const item = projects.value.find(row => row.id === id); form.customerName = item?.customerName || ''; api.getImportContracts?.(id).then(res => { contracts.value = res.data?.data || []; }); };
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 count = new Map();
rows.value = source.map((item, index) => { const row = { _key: `${Date.now()}-${index}`, batchNo: '', ...item }; Object.entries(keyMap).forEach(([label, key]) => { row[key] = item[label] ?? item[key] ?? ''; }); const duplicateKey = JSON.stringify(Object.values(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 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._editing = true; };
const saveRow = row => { row._editing = false; };
const cancelRow = row => { row._cancelled = true; };
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; }
}
</style>