feat(client): make pricing pages standalone and quick order
This commit is contained in:
@@ -429,6 +429,12 @@ function quickOrderDraft() {
|
||||
} catch {
|
||||
// Keep the standard empty row when quick-order data is malformed.
|
||||
}
|
||||
const pricingTaxMode = String(route.query.pricingTaxMode ?? '')
|
||||
if (pricingTaxMode === 'TAX_INCLUDED' || pricingTaxMode === 'TAX_EXCLUDED') draft.pricingTaxMode = pricingTaxMode
|
||||
const grossWeightKg = Number(route.query.grossWeightKg)
|
||||
const volumeCbm = Number(route.query.volumeCbm)
|
||||
if (Number.isFinite(grossWeightKg) && grossWeightKg > 0) draft.items[0].grossWeightKg = grossWeightKg
|
||||
if (Number.isFinite(volumeCbm) && volumeCbm > 0) draft.items[0].volumeCbm = volumeCbm
|
||||
return draft
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { Calculator, RefreshCcw, Search } from '@lucide/vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
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 router = useRouter()
|
||||
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))
|
||||
@@ -12,13 +14,36 @@ function formatMoney(item: MerchantPriceEstimate) { return `${item.currency} ${i
|
||||
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 } }
|
||||
function placeOrder(channelCode: string, channelName: string) {
|
||||
if (!quotes.value.some((quote) => quote.channelCode === channelCode)) {
|
||||
ElMessage.warning('请先查询该渠道价格')
|
||||
return
|
||||
}
|
||||
router.push({
|
||||
name: 'first-leg-order-create',
|
||||
query: {
|
||||
quick: '1',
|
||||
receivingWarehouseId: String(originWarehouseId.value),
|
||||
channelCode,
|
||||
channelName,
|
||||
destinationType: 'FBA',
|
||||
destinationWarehouseName: destinationCode.value.trim().toUpperCase(),
|
||||
amazonWarehouseCode: destinationCode.value.trim().toUpperCase(),
|
||||
destinationCountryCode: 'US',
|
||||
pricingTaxMode: taxMode.value,
|
||||
grossWeightKg: String(weight.value),
|
||||
volumeCbm: String(volume.value),
|
||||
cargo: JSON.stringify([{ sku: '', packageCount: 1 }]),
|
||||
},
|
||||
})
|
||||
}
|
||||
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>
|
||||
<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-column label="操作" width="100" fixed="right"><template #default="{ row }"><el-button link type="primary" :disabled="!quotes.some((quote) => quote.channelCode === row.channelCode)" @click="placeOrder(row.channelCode, row.channelName)">下单</el-button></template></el-table-column></el-table></section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ test('keeps account and notification routes as fixed-header utilities', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('organises customer-relevant business pages under five task-oriented sidebar entries', () => {
|
||||
test('keeps standalone business pages at the client sidebar root', () => {
|
||||
assert.deepEqual(
|
||||
clientNavigation.filter((node) => !node.hidden).map((node) => ({
|
||||
title: node.title,
|
||||
@@ -52,8 +52,10 @@ test('organises customer-relevant business pages under five task-oriented sideba
|
||||
{ title: '首页', children: undefined },
|
||||
{
|
||||
title: '订单中心',
|
||||
children: ['头程订单', '报关资料', '船期价格', '海外采购', '美国本土卡', '美本土散板'],
|
||||
children: ['头程订单', '海外采购', '美国本土卡', '美本土散板'],
|
||||
},
|
||||
{ title: '报关资料', children: undefined },
|
||||
{ title: '船期价格', children: undefined },
|
||||
{
|
||||
title: '海外仓',
|
||||
children: ['贴标申请', '仓储转运', '货物废弃', '退货换标'],
|
||||
@@ -86,11 +88,11 @@ test('flattens visible leaves with unique paths', () => {
|
||||
assert.deepEqual(paths, [
|
||||
'/',
|
||||
'/orders/first-leg',
|
||||
'/first-leg/customs-documents',
|
||||
'/first-leg/sailing-prices',
|
||||
'/cross-border/procurement',
|
||||
'/cross-border/domestic-trucking',
|
||||
'/cross-border/pallet-shipping',
|
||||
'/first-leg/customs-documents',
|
||||
'/first-leg/sailing-prices',
|
||||
'/overseas-warehouse/labeling',
|
||||
'/overseas-warehouse/storage-transfer',
|
||||
'/overseas-warehouse/cargo-disposal',
|
||||
@@ -112,7 +114,7 @@ test('filters inaccessible leaves and empty navigation groups', () => {
|
||||
const visible = filterNavigationByPermissions(clientNavigation, ['account:profile:view', 'order:first-leg:list', 'order:first-leg:customs-list', 'order:first-leg:pricing-list'])
|
||||
|
||||
assert.deepEqual(getNavigationLeaves(visible).map((item) => item.path), ['/orders/first-leg', '/first-leg/customs-documents', '/first-leg/sailing-prices'])
|
||||
assert.deepEqual(visible.map((item) => item.title), ['订单中心'])
|
||||
assert.deepEqual(visible.map((item) => item.title), ['订单中心', '报关资料', '船期价格'])
|
||||
assert.equal(getFirstAccessiblePath(['account:profile:view']), '/account/profile')
|
||||
assert.equal(getFirstAccessiblePath([]), '/forbidden')
|
||||
})
|
||||
|
||||
@@ -52,13 +52,13 @@ export const clientNavigation: ClientNavigationNode[] = [
|
||||
icon: ClipboardList,
|
||||
children: [
|
||||
requireLeaf('first-leg-orders', '头程订单'),
|
||||
requireLeaf('first-leg-customs-documents', '报关资料'),
|
||||
requireLeaf('first-leg-sailing-prices', '船期价格'),
|
||||
requireLeaf('cross-border-procurement'),
|
||||
requireLeaf('cross-border-domestic-trucking'),
|
||||
requireLeaf('cross-border-pallet-shipping'),
|
||||
],
|
||||
},
|
||||
requireLeaf('first-leg-customs-documents', '报关资料'),
|
||||
requireLeaf('first-leg-sailing-prices', '船期价格'),
|
||||
{
|
||||
key: 'overseas-warehouse-center',
|
||||
title: '海外仓',
|
||||
|
||||
Reference in New Issue
Block a user