2026-10-03 12:43:25 +08:00
< script setup lang = "ts" >
2026-10-03 13:00:32 +08:00
import { computed , onBeforeUnmount , ref , watch } from 'vue'
2026-10-03 12:43:25 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
2026-10-03 13:00:32 +08:00
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
2026-10-03 12:43:25 +08:00
import FleetQuotePartySelect from './FleetQuotePartySelect.vue'
2026-10-03 13:00:32 +08:00
import { importFleetQuotes , saveFleetCsv , isFleetCommandRejected , type FleetQuoteDraft , type FleetQuoteKind } from '../api/fleetQuotes'
2026-10-03 12:43:25 +08:00
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 )
2026-10-03 13:00:32 +08:00
let fileSequence = 0
2026-10-03 12:43:25 +08:00
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校验失败' ) } }
2026-10-03 13:00:32 +08:00
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读取失败,请重新选择文件' ) }
}
2026-10-03 12:43:25 +08:00
function template () { saveFleetCsv ( new Blob ([ '\uFEFF' + fleetQuoteCsvHeader . join ( ',' ) + '\r\n' ], { type : 'text/csv;charset=utf-8' }), '车队报价模板.csv' ) }
2026-10-03 13:00:32 +08:00
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 ++ })
2026-10-03 12:43:25 +08:00
</ 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 >