57 lines
6.1 KiB
Vue
57 lines
6.1 KiB
Vue
<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>
|