2026-08-19 16:45:51 +08:00
< script setup lang = "ts" >
2026-08-20 18:02:52 +08:00
import { onMounted , reactive , ref , watch } from 'vue'
import { Download , Eye , Pencil , Plus , Power } from '@lucide/vue'
import { ElMessage , ElMessageBox } from 'element-plus'
2026-08-19 16:45:51 +08:00
import AdminPage from '@/shared/components/AdminPage.vue'
2026-08-20 18:02:52 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
2026-08-19 16:45:51 +08:00
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
2026-08-20 18:02:52 +08:00
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { changeQuoteStatus , createQuote , exportQuotes , getQuote , listQuotes , listQuoteWarehouseOptions , updateQuote } from '../api/overseasMasterData'
import { overseasMasterDataPermissions as permission } from '../permissions'
import { saveCsv } from '../downloadCsv'
import { masterDataErrorMessage } from '../apiError'
import type { MasterDataStatus , OverseasTransitPrice , OverseasWarehouseFee , OverseasWarehouseQuote , OverseasWarehouseQuoteDetail , PartnerWarehouseOption } from '../types'
2026-08-19 16:45:51 +08:00
2026-08-20 18:02:52 +08:00
const access = useAccessPreviewStore ()
const rows = ref < OverseasWarehouseQuote [] >([]), total = ref ( 0 ), page = ref ( 1 ), pageSize = ref ( 20 ), loading = ref ( false ), saving = ref ( false ), exporting = ref ( false )
const error = ref < ApiErrorDetails | null >( null ), options = ref < PartnerWarehouseOption [] >([]), detail = ref < OverseasWarehouseQuoteDetail | null >( null )
const detailOpen = ref ( false ), editorOpen = ref ( false ), editingId = ref ( '' )
const filters = ref < QueryModel >({ keyword : '' , warehouseId : '' , providerName : '' , currency : '' , status : '' })
const fields = ref < QueryField [] >([])
function syncFields () { fields . value = [
2026-08-19 16:45:51 +08:00
{ key : 'keyword' , type : 'input' , label : '报价名称 / 版本' , placeholder : '报价名称 / 版本' , width : 180 },
2026-08-20 18:02:52 +08:00
{ key : 'warehouseId' , type : 'select' , label : '海外仓' , placeholder : '全部海外仓' , options : options . value . map ( v => ({ label : ` ${ v . warehouseName } · ${ v . warehouseCode } ` , value : v . warehouseId })), width : 220 },
{ key : 'providerName' , type : 'input' , label : '服务商' , placeholder : '服务商' , width : 170 },
{ key : 'currency' , type : 'select' , label : '币种' , placeholder : '全部币种' , options : [ 'USD' , 'CNY' ]. map ( v => ({ label : v , value : v })), width : 120 },
{ key : 'status' , type : 'select' , label : '状态' , placeholder : '全部状态' , options : [{ label : '启用' , value : 'ACTIVE' },{ label : '停用' , value : 'INACTIVE' }], width : 120 },
] }
syncFields ()
2026-08-19 16:45:51 +08:00
const columns : DataTableColumn [] = [
2026-08-20 18:02:52 +08:00
{ key : 'actions' , label : '操作' , width : 270 , fixed : 'left' , slotName : 'actions' }, { key : 'name' , prop : 'name' , label : '报价名称' , minWidth : 190 },
{ key : 'warehouseName' , prop : 'warehouseName' , label : '海外仓' , minWidth : 170 }, { key : 'providerName' , prop : 'providerName' , label : '服务商' , minWidth : 180 },
{ key : 'currency' , prop : 'currency' , label : '币种' , width : 74 }, { key : 'effectiveStart' , prop : 'effectiveStart' , label : '生效日期' , width : 112 },
{ key : 'effectiveEnd' , prop : 'effectiveEnd' , label : '失效日期' , width : 112 }, { key : 'versionCode' , prop : 'versionCode' , label : '报价版本' , width : 92 },
{ key : 'status' , label : '状态' , width : 76 , slotName : 'status' }, { key : 'updatedAt' , prop : 'updatedAt' , label : '更新时间' , minWidth : 150 }, { key : 'remark' , prop : 'remark' , label : '备注' , minWidth : 180 },
2026-08-19 16:45:51 +08:00
]
2026-08-20 18:02:52 +08:00
const form = reactive ({ name : '' , warehouseId : '' , providerName : '' , currency : 'USD' , effectiveStart : '' , effectiveEnd : '' , versionCode : 'V1.0' , remark : '' , recordVersion : 0 , warehouseFees : [] as OverseasWarehouseFee [], transitPrices : [] as OverseasTransitPrice [] })
function resetForm (){ Object . assign ( form ,{ name : '' , warehouseId : '' , providerName : '' , currency : 'USD' , effectiveStart : '' , effectiveEnd : '' , versionCode : 'V1.0' , remark : '' , recordVersion : 0 , warehouseFees : [{ item : '入库卸货' , billingUnit : '托' , unitPrice : 0 , minimumCharge : null , remark : null }], transitPrices : []}) }
function params (){ return { page : page . value , size : pageSize . value , keyword : String ( filters . value . keyword || '' ) || undefined , warehouseId : String ( filters . value . warehouseId || '' ) || undefined , providerName : String ( filters . value . providerName || '' ) || undefined , currency : String ( filters . value . currency || '' ) || undefined , status : ( filters . value . status || undefined ) as MasterDataStatus | undefined } }
async function load (){ loading . value = true ; error . value = null ; try { const r = await listQuotes ( params ()); rows . value = r . records ; total . value = r . total } catch ( e ){ rows . value = []; total . value = 0 ; error . value = getApiErrorDetails ( e , '海外仓报价加载失败' )} finally { loading . value = false } }
async function loadOptions (){ try { options . value = await listQuoteWarehouseOptions (); syncFields ()} catch ( e ){ ElMessage . error ( masterDataErrorMessage ( e , '合作仓候选加载失败' ))} }
function search (){ if ( page . value === 1 ) void load (); else page . value = 1 }
async function openDetail ( row : OverseasWarehouseQuote ){ try { detail . value = await getQuote ( row . id ); detailOpen . value = true } catch ( e ){ ElMessage . error ( masterDataErrorMessage ( e , '报价详情加载失败' ))} }
function fill ( d : OverseasWarehouseQuoteDetail ){ const s = d . summary ; Object . assign ( form ,{ name : s . name , warehouseId : s . warehouseId , providerName : s . providerName , currency : s . currency , effectiveStart : s . effectiveStart , effectiveEnd : s . effectiveEnd , versionCode : s . versionCode , remark : s . remark || '' , recordVersion : s . recordVersion , warehouseFees : d . warehouseFees . map ( v =>({... v })), transitPrices : d . transitPrices . map ( v =>({... v }))})}
async function openCreate (){ editingId . value = '' ; resetForm (); if ( ! options . value . length ) await loadOptions (); editorOpen . value = true }
async function openEdit ( row : OverseasWarehouseQuote ){ try { const d = await getQuote ( row . id ); editingId . value = row . id ; fill ( d ); editorOpen . value = true } catch ( e ){ ElMessage . error ( masterDataErrorMessage ( e , '报价详情加载失败' ))}}
async function save (){ if ( ! form . name ||! form . warehouseId ||! form . providerName ||! form . effectiveStart ||! form . effectiveEnd ||! form . warehouseFees . length ){ ElMessage . warning ( '请完整填写报价必填信息' ); return } if ( form . effectiveEnd < form .effectiveStart ) { ElMessage.warning (' 失效日期不能早于生效日期 '); return } saving.value = true ; try { const common = { name : form . name , providerName : form . providerName , currency : form . currency as 'USD' | 'CNY' , effectiveStart : form . effectiveStart , effectiveEnd : form . effectiveEnd , warehouseFees : form . warehouseFees , transitPrices : form . transitPrices , remark : form . remark || null } ; if ( editingId.value ) await updateQuote ( editingId.value , { ...common , recordVersion : form.recordVersion } ); else await createQuote ( { ...common , warehouseId : form.warehouseId , versionCode : form.versionCode } ); ElMessage.success ( editingId.value ?' 报价已更新 ' : ' 报价已创建 '); editorOpen.value = false ; await load () } catch ( e ) { ElMessage.error ( masterDataErrorMessage ( e ,' 报价保存失败 ')) } finally { saving.value = false }}
async function toggle ( row : OverseasWarehouseQuote ) { const next : MasterDataStatus = row.status ===' ACTIVE '?' INACTIVE ' : ' ACTIVE '; try { await ElMessageBox.confirm (` 确认 $ { next = ==' ACTIVE '?' 启用 ' : ' 停用 ' } “$ { row.name } ” 吗 ? `,'报价状态 ', { type : ' warning ' } ); await changeQuoteStatus ( row.id , next , row.recordVersion ); ElMessage.success (' 报价状态已更新 '); await load () } catch ( e ) { if ( e !==' cancel '&& e !==' close ') ElMessage.error ( masterDataErrorMessage ( e ,' 报价状态更新失败 ')) }}
async function exportCsv () { exporting.value = true ; try { const { page : _ , size : __ , ...q } = params (); saveCsv ( await exportQuotes ( q ),' overseas -quotes .csv ') } catch ( e ) { ElMessage.error ( masterDataErrorMessage ( e ,' 报价导出失败 ')) } finally { exporting.value = false }}
watch ([ page , pageSize ], load ); onMounted (()= >{ void Promise . all ([ load (), loadOptions ()])})
2026-08-19 16:45:51 +08:00
</ script >
2026-08-20 18:02:52 +08:00
< template >< AdminPage >< ListPageLayout aria -label =" 海外仓报价列表 " >
< template # toolbar >< QueryToolbar v-model = "filters" class="surface-panel" :fields="fields" ariaLabel="海外仓报价筛选" preference-key="overseas-quotes-query" :loading="loading" @search="search" /></ template >
< template # state >< LoadErrorState v-if = "error" title="海外仓报价加载失败" :message="error.message" @retry="load" /></ template >
< section class = "surface-panel table-panel" >< DataTable v -model:page = "page" v -model:page-size = "pageSize" :data = "rows" :columns = "columns" :total = "total" :loading = "loading" preference -key =" overseas -quotes-table " row -key =" id " empty -text =" 暂无海外仓报价 " >
< template # toolbar -actions >< el-button v-if = "access.can(permission.quoteCreate)" type="primary" @click="openCreate"><Plus :size="16"/>新增报价</el-button><el-button v-if="access.can(permission.quoteExport)" :loading="exporting" @click="exportCsv"><Download :size="16" /> 导出Excel </ el -button ></ template >
< template # actions = "{row}" >< el-button link type = "primary" @click ="openDetail(row as OverseasWarehouseQuote)" >< Eye :size = "15" /> 详情 </ el-button >< el-button v-if = "access.can(permission.quoteEdit)" link type="primary" @click="openEdit(row as OverseasWarehouseQuote)"><Pencil :size="15"/>编辑</el-button><el-button v-if="access.can(permission.quoteStatus)" link :type="(row as OverseasWarehouseQuote).status==='ACTIVE'?'danger':'success'" @click="toggle(row as OverseasWarehouseQuote)"><Power :size="15" />{{ ( row as OverseasWarehouseQuote ) .status ===' ACTIVE '?' 停用 ' : ' 启用 ' }} </ el -button ></ template >
< template # status = "{row}" >< el-tag : type = "(row as OverseasWarehouseQuote).status==='ACTIVE'?'success':'info'" >{{( row as OverseasWarehouseQuote ). status === 'ACTIVE' ? '启用' : '停用' }}</ el-tag ></ template >
</ DataTable ></ section >
< AppDialog v-model = "detailOpen" title="海外仓报价详情" width="min(980px, calc(100vw - 28px))" :show-confirm="false"><div v-if="detail" class="detail-grid"><div v-for="item in [['报价名称',detail.summary.name],['海外仓',detail.summary.warehouseName],['服务商',detail.summary.providerName],['币种',detail.summary.currency],['生效日期',detail.summary.effectiveStart],['失效日期',detail.summary.effectiveEnd],['版本',detail.summary.versionCode],['状态',detail.summary.status]]" :key="item[0]"><span>{{item[0]}}</span><strong>{{item[1]}}</strong></div></div><h3>仓储作业费用</h3><el-table :data="detail?.warehouseFees||[]" border><el-table-column prop="item" label="费用项目"/><el-table-column prop="billingUnit" label="计费单位"/><el-table-column prop="unitPrice" label="单价"/><el-table-column prop="minimumCharge" label="最低收费"/><el-table-column prop="remark" label="备注"/></el-table><h3>中转派送价格</h3><el-table :data="detail?.transitPrices||[]" border><el-table-column prop="fbaWarehouseCode" label="目的FBA仓"/><el-table-column prop="deliveryMethod" label="车型/提货方式"/><el-table-column prop="estimatedMiles" label="预估迈数"/><el-table-column prop="price" label="报价"/><el-table-column prop="minimumCharge" label="最低收费" /> </ el -table ></ AppDialog >
< AppDialog v-model = "editorOpen" :title="editingId?'编辑海外仓报价':'新增海外仓报价'" width="min(1180px, calc(100vw - 28px))" :confirm-loading="saving" confirm-text="保存报价" @confirm="save"><el-form label-position="top"><div class="form-grid"><el-form-item label="报价名称"><el-input v-model="form.name"/></el-form-item><el-form-item label="海外仓"><el-select v-model="form.warehouseId" :disabled="!!editingId"><el-option v-for="o in options" :key="o.warehouseId" :label="`${o.warehouseName} · ${o.warehouseCode}`" :value="o.warehouseId"/></el-select></el-form-item><el-form-item label="服务商"><el-input v-model="form.providerName"/></el-form-item><el-form-item label="币种"><el-select v-model="form.currency"><el-option label="USD" value="USD"/><el-option label="CNY" value="CNY"/></el-select></el-form-item><el-form-item label="生效日期"><el-date-picker v-model="form.effectiveStart" type="date" value-format="YYYY-MM-DD"/></el-form-item><el-form-item label="失效日期"><el-date-picker v-model="form.effectiveEnd" type="date" value-format="YYYY-MM-DD"/></el-form-item><el-form-item label="报价版本"><el-input v-model="form.versionCode" :disabled="!!editingId"/></el-form-item><el-form-item label="备注"><el-input v-model="form.remark"/></el-form-item></div><h3>仓储作业费用 <el-button @click="form.warehouseFees.push({item:'',billingUnit:'',unitPrice:0,minimumCharge:null,remark:null})">+ 添加费用</el-button></h3><div v-for="(f,i) in form.warehouseFees" :key="i" class="line-grid"><el-input v-model="f.item" placeholder="费用项目"/><el-input v-model="f.billingUnit" placeholder="计费单位"/><el-input-number v-model="f.unitPrice" :min="0"/><el-input-number v-model="f.minimumCharge" :min="0"/><el-button type="danger" plain @click="form.warehouseFees.splice(i,1)">删除</el-button></div><h3>中转派送价格 <el-button @click="form.transitPrices.push({fbaWarehouseCode:'',deliveryMethod:'',estimatedMiles:null,price:0,minimumCharge:null,remark:null})">+ 添加派送价格</el-button></h3><div v-for="(t,i) in form.transitPrices" :key="i" class="line-grid"><el-input v-model="t.fbaWarehouseCode" placeholder="FBA仓"/><el-input v-model="t.deliveryMethod" placeholder="车型/提货方式"/><el-input-number v-model="t.price" :min="0"/><el-input-number v-model="t.minimumCharge" :min="0"/><el-button type="danger" plain @click="form.transitPrices.splice(i,1)" > 删除 </ el -button ></ div ></ el-form ></ AppDialog >
</ ListPageLayout ></ AdminPage ></ template >
< style scoped >. table - panel { min - width : 0 ; overflow : hidden }. form - grid { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr )); gap : 12 px 16 px }. line - grid { display : grid ; grid - template - columns : 2 fr 1.2 fr 1 fr 1 fr auto ; gap : 8 px ; margin : 8 px 0 }. detail - grid { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr )); gap : 1 px ; background : var ( -- cc - border ); border : 1 px solid var ( -- cc - border )}. detail - grid > div { padding : 12 px ; background : # fff }. detail - grid span { display : block ; color : var ( -- cc - muted ); font - size : 12 px }. detail - grid strong { display : block ; margin - top : 4 px } h3 { margin : 18 px 0 10 px ; font - size : 14 px }</ style >