Files
admin-web/src/modules/overseas-master-data/components/FleetQuoteImport.vue
T
2026-10-20 23:34:11 +08:00

57 lines
6.1 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.
<script setup lang="ts">
import '@/styles/adminWorkbench.css'
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import FleetQuotePartySelect from './FleetQuotePartySelect.vue'
import { importFleetQuotes, saveFleetCsv, isFleetCommandRejected, type FleetQuoteDraft, type FleetQuoteKind } from '../api/fleetQuotes'
import { fleetQuoteCsvHeader, parseFleetQuoteCsv } from '../fleetQuoteCsv'
const open = defineModel<boolean>({ default: false }), props = defineProps<{ kind: FleetQuoteKind }>(), emit = defineEmits<{ saved: [] }>()
const partyId = ref(''), rows = ref<FleetQuoteDraft[]>([]), source = ref(''), saving = ref(false), error = ref<ApiErrorDetails | null>(null)
const pending = ref<{ key: string; rows: FleetQuoteDraft[] } | null>(null)
const activeKind = computed(() => pending.value?.rows[0]?.kind || props.kind)
let fileSequence = 0
watch([partyId, () => props.kind], () => { if (!pending.value) preview() })
function preview() { try { rows.value = source.value ? parseFleetQuoteCsv(source.value, props.kind, partyId.value) : []; error.value = null } catch (e) { rows.value = []; error.value = getApiErrorDetails(e, 'CSV校验失败') } }
async function pick(event: Event) {
if (saving.value || pending.value) return
const file = (event.target as HTMLInputElement).files?.[0]; if (!file) return
const current = ++fileSequence; rows.value = []; source.value = ''; error.value = null
if (file.size > 500000) { error.value = getApiErrorDetails(new Error('CSV不能超过500KB')); return }
try { const text = await file.text(); if (current !== fileSequence) return; source.value = text; preview() }
catch (e) { if (current === fileSequence) error.value = getApiErrorDetails(e, 'CSV读取失败,请重新选择文件') }
}
function template() { saveFleetCsv(new Blob(['\uFEFF' + fleetQuoteCsvHeader.join(',') + '\r\n'], { type: 'text/csv;charset=utf-8' }), '车队报价模板.csv') }
async function save() { if (saving.value || (!pending.value && (!rows.value.length || !partyId.value))) return; fileSequence++; saving.value = true; error.value = null; try { pending.value ??= { key: crypto.randomUUID(), rows: rows.value.map(r => ({ ...r })) }; await importFleetQuotes(pending.value.key, pending.value.rows); pending.value = null; rows.value = []; source.value = ''; open.value = false; emit('saved') } catch (e) { error.value = getApiErrorDetails(e, '整批导入失败,请核对报价库与目录后重试'); if (isFleetCommandRejected(e)) pending.value = null } finally { saving.value = false } }
onBeforeUnmount(() => { fileSequence++ })
</script>
<template><AppDialog v-model="open" title="批量导入车队报价" width="1040px" class="admin-density-dialog admin-workbench is-dialog" fullscreenable :confirm-loading="saving" :show-confirm="false" :close-on-click-modal="false">
<p class="density-dialog-note">本批{{ activeKind === 'PURCHASE' ? '采购' : '销售' }}报价使用同一往来单位;每批最多200条,服务端整批校验,任一失败全部不写入。</p>
<el-alert v-if="error" type="error" :title="error.message" :closable="false"><ApiErrorMeta :details="error" /></el-alert>
<p v-if="pending" role="status" class="density-dialog-note">已保留原批次和原内容。结果未确认时请按原请求重试,避免重复导入。</p>
<div class="density-dialog-sections">
<section class="density-dialog-section" data-tone="work" aria-label="导入报价文件与归属">
<h3 class="density-dialog-section__title">导入资料</h3>
<el-form label-position="left" label-width="130px" class="density-form" :disabled="saving || !!pending">
<table class="density-table density-table--editable"><tbody>
<tr><td><el-form-item label="报价用途"><span>{{activeKind === 'PURCHASE' ? '车队采购报价' : '外接销售报价'}}</span></el-form-item></td></tr>
<tr><td><el-form-item :label="activeKind === 'PURCHASE' ? '海外车队供应商' : '委托客户'"><FleetQuotePartySelect v-model="partyId" :kind="activeKind" :disabled="saving || !!pending" /></el-form-item></td></tr>
<tr><td><el-form-item label="CSV文件"><input type="file" accept=".csv" aria-label="选择报价CSV" :disabled="saving || !!pending" @change="pick" /></el-form-item></td></tr>
</tbody></table>
</el-form>
<el-button :disabled="saving" @click="template">下载CSV模板</el-button>
</section>
<section class="density-dialog-section" data-tone="source" aria-label="导入报价预览">
<h3 class="density-dialog-section__title">报价预览</h3>
<p aria-live="polite" class="density-dialog-note">共 {{ rows.length }} 条,预览前10条;目录有效性与重复组合由服务端核对。</p>
<div class="fleet-import-scroll"><table class="density-table fleet-import-table"><thead><tr><th scope="col">发货城市</th><th scope="col">目的仓码</th><th scope="col">距离(mile)</th><th scope="col">币种</th><th scope="col">整车自提</th><th scope="col">整车派送</th><th scope="col">散板派送</th><th scope="col">有效期</th></tr></thead><tbody><tr v-for="(row, index) in rows.slice(0,10)" :key="index"><td>{{ row.originCity }}</td><td>{{ row.destinationCode }}</td><td>{{ row.distanceMiles }}</td><td>{{ row.currency }}</td><td>{{ row.selfPickupPrice ?? '—' }}</td><td>{{ row.fullDeliveryPrice ?? '—' }}</td><td>{{ row.looseDeliveryPrice ?? '—' }}</td><td>{{ row.validFrom }} — {{ row.validTo }}</td></tr><tr v-if="!rows.length"><td colspan="8">请选择往来单位并上传有效CSV文件</td></tr></tbody></table></div>
</section>
</div>
<template #footer="{close}"><div class="density-actions density-actions--footer"><el-button :disabled="saving" @click="close">关闭</el-button><el-button type="primary" :disabled="!rows.length || !partyId" :loading="saving" @click="save">{{ pending ? '按原请求重试' : '确认导入' }}</el-button></div></template>
</AppDialog></template>
<style scoped>
.fleet-import-scroll { overflow-x: auto; }
.fleet-import-table { min-width: 900px; }
</style>