168 lines
19 KiB
Vue
168 lines
19 KiB
Vue
<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>
|