29 lines
4.1 KiB
Vue
29 lines
4.1 KiB
Vue
<script setup lang="ts">
|
||||
|
|
import { computed, ref, watch } from 'vue'
|
|||
|
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
|||
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
|||
|
|
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
|||
|
|
import FleetQuotePartySelect from './FleetQuotePartySelect.vue'
|
|||
|
|
import { importFleetQuotes, saveFleetCsv, 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)
|
|||
|
|
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) { const file = (event.target as HTMLInputElement).files?.[0]; if (!file) return; if (file.size > 500000) { error.value = getApiErrorDetails(new Error('CSV不能超过500KB')); return }; source.value = await file.text(); preview() }
|
|||
|
|
function template() { saveFleetCsv(new Blob(['\uFEFF' + fleetQuoteCsvHeader.join(',') + '\r\n'], { type: 'text/csv;charset=utf-8' }), '车队报价模板.csv') }
|
|||
|
|
async function save() { 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 (e instanceof ApiResponseError && ([400, 409, 422].includes(e.status || 0) || ['400', '409', '422', 'VALIDATION_FAILED', 'CONFLICT'].includes(e.code))) pending.value = null } finally { saving.value = false } }
|
|||
|
|
</script>
|
|||
|
|
<template><AppDialog v-model="open" title="批量导入车队报价" width="1040px" class="admin-density-dialog" :close-on-click-modal="false">
|
|||
|
|
<p>本批{{ activeKind === 'PURCHASE' ? '采购' : '销售' }}报价使用同一往来单位;每批最多200条,服务端整批校验,任一失败全部不写入。</p>
|
|||
|
|
<FleetQuotePartySelect v-model="partyId" :kind="activeKind" :disabled="saving || !!pending" />
|
|||
|
|
<el-button @click="template">下载CSV模板</el-button><input type="file" accept=".csv" aria-label="选择报价CSV" :disabled="saving || !!pending" @change="pick" />
|
|||
|
|
<el-alert v-if="error" type="error" :title="error.message" :closable="false"><ApiErrorMeta :details="error" /></el-alert>
|
|||
|
|
<p v-if="pending">已保留原批次和原内容。结果未确认时请按原请求重试,避免重复导入。</p>
|
|||
|
|
<p aria-live="polite">共 {{ rows.length }} 条,预览前10条;目录有效性与重复组合由服务端核对。</p>
|
|||
|
|
<table class="density-table"><thead><tr><th>发货城市</th><th>目的仓码</th><th>距离(mile)</th><th>币种</th><th>整车自提</th><th>整车派送</th><th>散板派送</th><th>有效期</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></tbody></table>
|
|||
|
|
<template #footer><el-button @click="open = false">关闭</el-button><el-button type="primary" :disabled="!rows.length || !partyId" :loading="saving" @click="save">{{ pending ? '按原请求重试' : '确认导入' }}</el-button></template>
|
|||
|
|
</AppDialog></template>
|