Files
client-web/src/modules/first-leg/views/SailingPricesView.vue
T

28 lines
6.8 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 { computed, onMounted, ref, watch } from 'vue'
import { Calculator, RefreshCcw, Search } from '@lucide/vue'
import { ElMessage } from 'element-plus'
import { estimateMerchantFirstLegPrice, getMerchantPricingChannels, getMerchantWarehouses, type MerchantPriceEstimate, type MerchantPricingChannel, type MerchantWarehouse } from '../api/firstLeg'
const loading = ref(false); const querying = ref(false); const errorMessage = ref('')
const warehouses = ref<MerchantWarehouse[]>([]); const channels = ref<MerchantPricingChannel[]>([]); const quotes = ref<MerchantPriceEstimate[]>([])
const originWarehouseId = ref<string | number>(''); const destinationCode = ref(''); const weight = ref<number | undefined>(); const volume = ref<number | undefined>(); const taxMode = ref<'TAX_INCLUDED' | 'TAX_EXCLUDED'>('TAX_INCLUDED')
const hasInput = computed(() => Boolean(originWarehouseId.value && destinationCode.value.trim() && weight.value && volume.value))
function formatMoney(item: MerchantPriceEstimate) { return `${item.currency} ${item.estimatedAmount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` }
async function loadBase() { loading.value = true; errorMessage.value = ''; try { const result = await getMerchantWarehouses({ warehouseType: 'DOMESTIC' }); warehouses.value = result.records; if (result.records[0]) { originWarehouseId.value = result.records[0].id; await loadChannels() } } catch (error) { errorMessage.value = error instanceof Error ? error.message : '船期价格加载失败' } finally { loading.value = false } }
async function loadChannels() { if (!originWarehouseId.value) return; channels.value = await getMerchantPricingChannels(originWarehouseId.value) }
async function query() { if (!hasInput.value) { ElMessage.warning('请填写起运仓、目的仓、重量和体积'); return }; querying.value = true; quotes.value = []; try { const results = await Promise.allSettled(channels.value.map((channel) => estimateMerchantFirstLegPrice({ originWarehouseId: originWarehouseId.value, channelCode: channel.channelCode, destinationCode: destinationCode.value.trim(), destinationType: 'FBA', pricingTaxMode: taxMode.value, actualWeightKg: weight.value!, actualVolumeCbm: volume.value! }))); quotes.value = results.flatMap((item) => item.status === 'fulfilled' ? [item.value] : []) } finally { querying.value = false } }
watch(originWarehouseId, () => { void loadChannels() }); onMounted(() => { void loadBase() })
</script>
<template>
<div class="pricing-page"><section class="pricing-hero"><div><span class="eyebrow">SAILING PRICE INQUIRY</span><h1>船期价格查询</h1><p>输入货物重量和目的仓,获取匹配渠道、参考船期与预估运费。</p></div><el-button :loading="loading" @click="loadBase"><RefreshCcw :size="15" />刷新</el-button></section>
<section class="pricing-form"><el-form label-position="top"><el-form-item label="起运仓"><el-select v-model="originWarehouseId" filterable :loading="loading" placeholder="选择起运仓"><el-option v-for="item in warehouses" :key="String(item.id)" :label="`${item.warehouseName} · ${item.warehouseCode}`" :value="item.id" /></el-select></el-form-item><el-form-item label="货物重量"><el-input v-model.number="weight" type="number" placeholder="请输入重量"><template #append>KG</template></el-input></el-form-item><el-form-item label="货物立方数"><el-input v-model.number="volume" type="number" placeholder="请输入体积"><template #append>CBM</template></el-input></el-form-item><el-form-item label="目的仓"><el-input v-model="destinationCode" placeholder="如 ABE4" /></el-form-item><el-form-item label="报价方式"><el-radio-group v-model="taxMode"><el-radio-button value="TAX_INCLUDED" label="包税" /><el-radio-button value="TAX_EXCLUDED" label="不包税" /></el-radio-group></el-form-item><el-form-item><el-button type="primary" :loading="querying" @click="query"><Search :size="16" />立即查询</el-button></el-form-item></el-form></section>
<section class="pricing-results"><div class="section-title"><div><h2>匹配渠道</h2><span>{{ channels.length }} 个渠道 · 参考价格以服务端估价为准</span></div><Calculator :size="20" /></div><el-skeleton v-if="loading || querying" :rows="4" animated /><el-alert v-else-if="errorMessage" :title="errorMessage" type="error" show-icon /><el-empty v-else-if="!channels.length" description="暂无可用船期价格" /><el-table v-else :data="channels" stripe><el-table-column label="渠道" min-width="160"><template #default="{ row }"><strong>{{ row.channelName }}</strong><small>{{ row.channelCode }}</small></template></el-table-column><el-table-column label="参考船期" min-width="150"><template #default="{ row }">{{ row.estimatedDepartureText || row.estimatedDepartureDate || '待更新' }}</template></el-table-column><el-table-column label="最晚进仓" min-width="130"><template #default="{ row }">{{ row.latestWarehouseEntry || '—' }}</template></el-table-column><el-table-column label="参考时效" width="120"><template #default="{ row }">{{ row.referenceTransit || '—' }}</template></el-table-column><el-table-column label="预估运费" width="150"><template #default="{ row }"><span v-if="quotes.find((quote) => quote.channelCode === row.channelCode)">{{ formatMoney(quotes.find((quote) => quote.channelCode === row.channelCode)!) }}</span><span v-else class="muted">查询后显示</span></template></el-table-column><el-table-column label="附加费说明" min-width="180" prop="surchargeDescription" /></el-table></section>
</div>
</template>
<style scoped>
.pricing-page { display:flex; flex-direction:column; gap:14px; }.pricing-hero { display:flex; justify-content:space-between; align-items:flex-start; padding:22px 24px; border:1px solid var(--client-border); border-radius:var(--client-radius); background:linear-gradient(135deg,#eff7fb,#fff); }.eyebrow { color:var(--primary); font-size:11px; letter-spacing:.12em; }.pricing-hero h1 { margin:7px 0 5px; color:var(--navy); font-size:24px; }.pricing-hero p { margin:0; color:var(--muted); font-size:13px; }.pricing-form { padding:18px 20px 4px; border:1px solid var(--client-border); border-radius:var(--client-radius); background:#fff; }.pricing-form :deep(form) { display:grid; grid-template-columns:1.3fr 1fr 1fr 1.2fr 1.1fr auto; align-items:end; gap:14px; }.pricing-form :deep(.el-form-item) { margin-bottom:14px; }.pricing-results { padding:4px; overflow:hidden; border:1px solid var(--client-border); border-radius:var(--client-radius); background:#fff; }.section-title { display:flex; align-items:center; justify-content:space-between; padding:16px 18px; }.section-title h2 { margin:0 0 4px; color:var(--navy); font-size:16px; }.section-title span { color:var(--muted); font-size:12px; }.section-title svg { color:var(--primary); }.pricing-results small { display:block; margin-top:3px; color:var(--muted); font-size:11px; }.muted { color:var(--muted); }
</style>