Files
admin-web/src/modules/overseas-master-data/pages/OverseasQuotesView.vue
T
2026-10-21 13:22:42 +08:00

126 lines
18 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 { useExportTask } from '@/modules/downloads'
import '@/styles/adminDensity.css'
import { onMounted, reactive, ref, watch } from 'vue'
import { Download, Plus } from '@lucide/vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import AdminPage from '@/shared/components/AdminPage.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
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'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { changeQuoteStatus, createQuote, getQuote, listQuotes, listQuoteWarehouseOptions, updateQuote } from '../api/overseasMasterData'
import { overseasMasterDataPermissions as permission } from '../permissions'
import { masterDataErrorMessage } from '../apiError'
import type { MasterDataStatus, OverseasTransitPrice, OverseasWarehouseFee, OverseasWarehouseQuote, OverseasWarehouseQuoteDetail, PartnerWarehouseOption } from '../types'
const submitExport = useExportTask()
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(''), editingWarehouse = ref('')
const filters = ref<QueryModel>({ keyword: '', warehouseId: '', providerName: '', currency: '', status: '' })
const fields = ref<QueryField[]>([])
function syncFields() { fields.value = [
{ key: 'keyword', type: 'input', label: '报价名称 / 版本', placeholder: '报价名称 / 版本', width: 180 },
{ 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()
const columns: DataTableColumn[] = [
{ 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 },
]
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;editingWarehouse.value=[s.warehouseName,s.warehouseCode].filter(Boolean).join(' · ')||'—';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}”吗?`,`${next==='ACTIVE'?'启用':'停用'}海外仓报价 · ${row.name.trim() || '—'}`,{customClass:'admin-density-confirm admin-workbench is-dialog',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();await submitExport('OVERSEAS_QUOTE_LIST', '海外报价', q)}catch(e){ElMessage.error(masterDataErrorMessage(e,'报价导出失败'))}finally{exporting.value=false}}
watch([page,pageSize],load);onMounted(()=>{void Promise.all([load(),loadOptions()])})
</script>
<template><AdminPage class="admin-density admin-workbench"><ListPageLayout aria-label="海外仓报价列表">
<section><div class="workbench-table is-page-surface"><DataTable appearance="integrated" column-settings-mode="show" :results-visible="true" 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-start><h1>海外仓报价</h1></template>
<template #filters><QueryToolbar v-model="filters" :fields="fields" ariaLabel="海外仓报价筛选" preference-key="overseas-quotes-query" :loading="loading" @search="search" /></template>
<template #notices><LoadErrorState v-if="error" title="海外仓报价加载失败" :message="error.message" @retry="load" /></template>
<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) && access.can('platform:download:list')" :loading="exporting" @click="exportCsv"><Download :size="16"/>导出</el-button></template>
<template #actions="{row}"><div class="density-actions row-actions admin-text-actions"><el-button data-tone="neutral" class="density-action" @click="openDetail(row as OverseasWarehouseQuote)">详情</el-button><el-button data-tone="neutral" class="density-action" v-if="access.can(permission.quoteEdit)" @click="openEdit(row as OverseasWarehouseQuote)">编辑</el-button><el-button class="density-action" v-if="access.can(permission.quoteStatus)" :data-tone="(row as OverseasWarehouseQuote).status==='ACTIVE'?'danger':'success'" @click="toggle(row as OverseasWarehouseQuote)">{{(row as OverseasWarehouseQuote).status==='ACTIVE'?'停用':'启用'}}</el-button></div></template>
<template #status="{row}"><el-tag :type="(row as OverseasWarehouseQuote).status==='ACTIVE'?'success':'info'">{{(row as OverseasWarehouseQuote).status==='ACTIVE'?'启用':'停用'}}</el-tag></template>
</DataTable></div></section>
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="detailOpen" :title="detail?.summary.name?.trim() ? `海外仓报价详情 · ${detail.summary.name.trim()}` : '海外仓报价详情'" width="min(980px, calc(100vw - 28px))" fullscreenable :show-confirm="false">
<div v-if="detail" class="density-dialog-sections">
<section class="density-dialog-section" data-tone="summary" aria-label="报价摘要">
<h3 class="density-dialog-section__title">报价信息</h3>
<dl class="detail-grid density-fields"><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 === 'ACTIVE' ? '启用' : '停用']]" :key="item[0] || ''"><dt>{{item[0]}}</dt><dd>{{item[1] || '—'}}</dd></div></dl>
</section>
<section class="density-dialog-section" data-tone="source" aria-label="仓储作业费用">
<h3 class="density-dialog-section__title">仓储作业费用</h3>
<el-table :data="detail.warehouseFees" border empty-text="暂无仓储作业费用"><el-table-column prop="item" label="费用项目" min-width="150"/><el-table-column prop="billingUnit" label="计费单位" width="100"/><el-table-column prop="unitPrice" label="单价" min-width="100"/><el-table-column label="最低收费" min-width="100"><template #default="{row}">{{row.minimumCharge ?? '—'}}</template></el-table-column><el-table-column label="备注" min-width="150"><template #default="{row}">{{row.remark || '—'}}</template></el-table-column></el-table>
</section>
<section class="density-dialog-section" data-tone="source" aria-label="中转派送价格">
<h3 class="density-dialog-section__title">中转派送价格</h3>
<el-table :data="detail.transitPrices" border empty-text="暂无中转派送价格"><el-table-column prop="fbaWarehouseCode" label="目的FBA仓" min-width="120"/><el-table-column prop="deliveryMethod" label="车型/提货方式" min-width="160"/><el-table-column label="预估迈数" min-width="100"><template #default="{row}">{{row.estimatedMiles ?? '—'}}</template></el-table-column><el-table-column prop="price" label="报价" min-width="100"/><el-table-column label="最低收费" min-width="100"><template #default="{row}">{{row.minimumCharge ?? '—'}}</template></el-table-column></el-table>
</section>
</div>
</AppDialog>
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="editorOpen" :title="editingId ? `编辑海外仓报价 · ${form.name.trim() || '—'}` : '新增海外仓报价'" width="min(1180px, calc(100vw - 28px))" fullscreenable :confirm-loading="saving" confirm-text="保存报价" @confirm="save">
<el-form class="density-form density-dialog-sections" label-position="left" label-width="90px" :disabled="saving">
<section class="density-dialog-section" data-tone="work" aria-label="报价基础信息">
<h3 class="density-dialog-section__title">基础信息</h3>
<table class="density-table density-table--editable quote-basics-table"><tbody>
<tr><td><el-form-item label="报价名称"><el-input v-model="form.name" placeholder="请输入报价名称" /></el-form-item></td><td><el-form-item label="海外仓"><span v-if="editingId">{{ editingWarehouse }}</span><el-select v-else v-model="form.warehouseId"><el-option v-for="o in options" :key="o.warehouseId" :label="`${o.warehouseName} · ${o.warehouseCode}`" :value="o.warehouseId"/></el-select></el-form-item></td></tr>
<tr><td><el-form-item label="服务商"><el-input v-model="form.providerName" placeholder="请输入服务商名称" /></el-form-item></td><td><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></td></tr>
<tr><td><el-form-item label="生效日期"><el-date-picker v-model="form.effectiveStart" type="date" value-format="YYYY-MM-DD"/></el-form-item></td><td><el-form-item label="失效日期"><el-date-picker v-model="form.effectiveEnd" type="date" value-format="YYYY-MM-DD"/></el-form-item></td></tr>
<tr><td><el-form-item label="报价版本"><span v-if="editingId">{{form.versionCode || '—'}}</span><el-input v-else v-model="form.versionCode" placeholder="请输入版本号" /></el-form-item></td><td><el-form-item label="备注"><el-input v-model="form.remark" placeholder="请输入备注(选填)" /></el-form-item></td></tr>
</tbody></table>
</section>
<section class="density-dialog-section" data-tone="work" aria-label="编辑仓储作业费用">
<h3 class="density-dialog-section__title quote-section-heading">仓储作业费用 <el-button @click="form.warehouseFees.push({item:'',billingUnit:'',unitPrice:0,minimumCharge:null,remark:null})">+ 添加费用</el-button></h3>
<div class="quote-lines-scroll"><table class="density-table density-table--editable quote-lines-table"><thead><tr><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="(f,i) in form.warehouseFees" :key="i"><td><el-button data-tone="danger" class="density-action" :aria-label="`删除第${i+1}项费用`" @click="form.warehouseFees.splice(i,1)">删除</el-button></td><td><el-input v-model="f.item" :aria-label="`第${i+1}项费用项目`" placeholder="请输入费用项目"/></td><td><el-input v-model="f.billingUnit" :aria-label="`第${i+1}项计费单位`" placeholder="请输入计费单位"/></td><td><el-input-number v-model="f.unitPrice" :aria-label="`第${i+1}项费用单价`" :min="0" placeholder="请输入单价" /></td><td><el-input-number v-model="f.minimumCharge" :aria-label="`第${i+1}项费用最低收费`" :min="0" placeholder="请输入最低收费" /></td></tr>
<tr v-if="!form.warehouseFees.length"><td colspan="5">暂无仓储作业费用</td></tr>
</tbody></table></div>
</section>
<section class="density-dialog-section" data-tone="work" aria-label="编辑中转派送价格">
<h3 class="density-dialog-section__title quote-section-heading">中转派送价格 <el-button @click="form.transitPrices.push({fbaWarehouseCode:'',deliveryMethod:'',estimatedMiles:null,price:0,minimumCharge:null,remark:null})">+ 添加派送价格</el-button></h3>
<div class="quote-lines-scroll"><table class="density-table density-table--editable quote-lines-table"><thead><tr><th scope="col">操作</th><th scope="col">目的FBA仓</th><th scope="col">车型/提货方式</th><th scope="col">报价</th><th scope="col">最低收费</th></tr></thead><tbody>
<tr v-for="(t,i) in form.transitPrices" :key="i"><td><el-button data-tone="danger" class="density-action" :aria-label="`删除第${i+1}项派送价格`" @click="form.transitPrices.splice(i,1)">删除</el-button></td><td><el-input v-model="t.fbaWarehouseCode" :aria-label="`第${i+1}项目的FBA仓`" placeholder="请输入目的 FBA 仓"/></td><td><el-input v-model="t.deliveryMethod" :aria-label="`第${i+1}项车型或提货方式`" placeholder="请输入车型或提货方式"/></td><td><el-input-number v-model="t.price" :aria-label="`第${i+1}项派送报价`" :min="0" placeholder="请输入报价" /></td><td><el-input-number v-model="t.minimumCharge" :aria-label="`第${i+1}项派送最低收费`" :min="0" placeholder="请输入最低收费" /></td></tr>
<tr v-if="!form.transitPrices.length"><td colspan="5">暂无中转派送价格</td></tr>
</tbody></table></div>
</section>
</el-form>
</AppDialog>
</ListPageLayout></AdminPage></template>
<style scoped>
.detail-grid.density-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.quote-basics-table { table-layout: fixed; }
.quote-basics-table td { width: 50%; }
.quote-section-heading { justify-content: space-between; }
.quote-lines-scroll { overflow-x: auto; }
.quote-lines-table { table-layout: fixed; min-width: 760px; }
.quote-lines-table th:first-child { width: 70px; }
.quote-lines-table th:nth-child(2) { width: 25%; }
@media (max-width: 600px) { .quote-basics-table tr { display: grid; } .quote-basics-table td { width: auto; } }
</style>