优化
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const source = readFileSync(new URL('./InboundBillingPanel.vue', import.meta.url), 'utf8')
|
||||
|
||||
test('renders loading, pending, isolated error, and published states', () => {
|
||||
assert.match(source, /v-if="props\.loading"/)
|
||||
assert.match(source, /v-else-if="props\.error"/)
|
||||
assert.match(source, /role="alert"/)
|
||||
assert.match(source, /v-else-if="props\.unavailable \|\| !props\.data"/)
|
||||
assert.match(source, /role="status"/)
|
||||
assert.match(source, /已入库 · 费用已发布/)
|
||||
assert.match(source, /\$emit\('retry'\)/)
|
||||
})
|
||||
|
||||
test('shows the confirmed merchant inbound and fee fields without write actions', () => {
|
||||
for (const label of ['入仓码', '平台收货仓', '实际毛重', '实测体积', '托盘与库位', '换算与精度', '最终计费量', '基础费用', '附加费用', '费用合计', '平台备注']) {
|
||||
assert.match(source, new RegExp(label))
|
||||
}
|
||||
assert.match(source, /formatInboundMoney\(props\.data\.latestFee\.totalAmount/)
|
||||
assert.match(source, /getRoundingModeLabel\(props\.data\.latestFee\.roundingMode\)/)
|
||||
assert.doesNotMatch(source, /确认费用|调整费用|发送费用|下载照片/)
|
||||
assert.doesNotMatch(source, /internalNotes|statusHistory|files/)
|
||||
assert.match(source, /latestFee\.ruleCode/)
|
||||
assert.match(source, /latestFee\.rateCode/)
|
||||
assert.match(source, /latestFee\.chargeRuleVersion/)
|
||||
assert.match(source, /latestFee\.carrierRateVersion/)
|
||||
})
|
||||
|
||||
test('stacks core information for 720 and 360 pixel layouts', () => {
|
||||
assert.match(source, /@media \(max-width: 720px\)/)
|
||||
assert.match(source, /\.inbound-overview, \.inbound-measurements, \.fee-calculation-grid \{ grid-template-columns: 1fr;/)
|
||||
assert.match(source, /\.inbound-billing-state \.el-button \{[^}]*min-height: 44px;/)
|
||||
assert.match(source, /@media \(max-width: 360px\)/)
|
||||
})
|
||||
@@ -0,0 +1,207 @@
|
||||
<script setup lang="ts">
|
||||
import { Clock3, PackageCheck, RefreshCw, TriangleAlert } from '@lucide/vue'
|
||||
import type { MerchantInboundBilling } from '@/modules/first-leg/types/inboundBilling'
|
||||
import {
|
||||
formatInboundDateTime,
|
||||
formatInboundMeasure,
|
||||
formatInboundMoney,
|
||||
formatInboundNumber,
|
||||
getChargeModeLabel,
|
||||
getRoundingModeLabel,
|
||||
getTaxModeLabel,
|
||||
} from '@/modules/first-leg/utils/inboundBilling'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
data?: MerchantInboundBilling | null
|
||||
loading?: boolean
|
||||
unavailable?: boolean
|
||||
unavailableMessage?: string
|
||||
error?: string
|
||||
}>(), {
|
||||
data: null,
|
||||
loading: false,
|
||||
unavailable: false,
|
||||
unavailableMessage: '入库及费用结果尚未发布',
|
||||
error: '',
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
retry: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="inbound-billing-panel" aria-labelledby="inbound-billing-title">
|
||||
<header class="inbound-billing-heading">
|
||||
<h3 id="inbound-billing-title">入库与费用</h3>
|
||||
<el-tag v-if="props.data" type="success" effect="light">已入库 · 费用已发布</el-tag>
|
||||
</header>
|
||||
|
||||
<div v-if="props.loading" class="inbound-billing-loading" aria-live="polite" aria-busy="true">
|
||||
<el-skeleton :rows="4" animated />
|
||||
<span class="sr-only">正在加载入库与费用结果</span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="props.error" class="inbound-billing-state is-error" role="alert">
|
||||
<TriangleAlert :size="22" />
|
||||
<div>
|
||||
<strong>入库与费用加载失败</strong>
|
||||
<span>{{ props.error }}</span>
|
||||
</div>
|
||||
<el-button @click="$emit('retry')"><RefreshCw :size="15" />重新加载</el-button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="props.unavailable || !props.data" class="inbound-billing-state is-pending" role="status">
|
||||
<Clock3 :size="22" />
|
||||
<div>
|
||||
<strong>{{ props.unavailableMessage }}</strong>
|
||||
<span>客服完成测量审核、入库并发布费用后,将在这里展示最终结果。</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<dl class="inbound-overview">
|
||||
<div><dt>入仓码</dt><dd>{{ props.data.inbound.summary.inboundCode }}</dd></div>
|
||||
<div><dt>入库完成</dt><dd>{{ formatInboundDateTime(props.data.inbound.summary.inboundedAt) }}</dd></div>
|
||||
<div><dt>平台收货仓</dt><dd>{{ props.data.inbound.summary.warehouseName || '-' }}</dd></div>
|
||||
<div><dt>目的仓</dt><dd>{{ props.data.inbound.summary.destinationCode || props.data.inbound.summary.destinationName || '-' }}</dd></div>
|
||||
</dl>
|
||||
|
||||
<div class="inbound-result-section">
|
||||
<h4>实测结果</h4>
|
||||
<dl class="inbound-measurements">
|
||||
<div><dt>实际箱数</dt><dd>{{ formatInboundNumber(props.data.inbound.summary.totalPackageCount, 0) }}</dd></div>
|
||||
<div><dt>实际毛重</dt><dd>{{ formatInboundMeasure(props.data.inbound.summary.actualGrossWeightKg, 'kg') }}</dd></div>
|
||||
<div><dt>实际净重</dt><dd>{{ formatInboundMeasure(props.data.inbound.summary.actualNetWeightKg, 'kg') }}</dd></div>
|
||||
<div><dt>实测体积</dt><dd>{{ formatInboundMeasure(props.data.inbound.summary.actualVolumeCbm, 'm³') }}</dd></div>
|
||||
<div><dt>材积重</dt><dd>{{ formatInboundMeasure(props.data.inbound.summary.volumeWeightKg, 'kg') }}</dd></div>
|
||||
<div><dt>测量完成</dt><dd>{{ formatInboundDateTime(props.data.inbound.summary.measureCompletedAt) }}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="inbound-result-section">
|
||||
<div class="inbound-section-title">
|
||||
<h4>托盘与库位</h4>
|
||||
<span>{{ props.data.inbound.palletLocations.length }} 个托盘</span>
|
||||
</div>
|
||||
<div v-if="props.data.inbound.palletLocations.length" class="pallet-location-list">
|
||||
<article v-for="item in props.data.inbound.palletLocations" :key="`${item.palletId}-${item.locationId}`">
|
||||
<PackageCheck :size="18" />
|
||||
<div class="pallet-location-main">
|
||||
<strong>{{ item.palletCode }}</strong>
|
||||
<span>库位 {{ item.locationCode }}</span>
|
||||
</div>
|
||||
<div class="pallet-location-quantity">
|
||||
<span>{{ formatInboundMeasure(item.allocatedVolumeCbm, 'm³') }}</span>
|
||||
<span>{{ formatInboundNumber(item.allocatedPackageCount, 0) }} 箱</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<p v-else class="inbound-inline-empty">暂无托盘和库位明细</p>
|
||||
</div>
|
||||
|
||||
<div class="inbound-result-section fee-result">
|
||||
<div class="inbound-section-title">
|
||||
<h4>费用结果</h4>
|
||||
<span>{{ props.data.latestFee.feeNo }} · 第 {{ props.data.latestFee.feeVersion }} 版</span>
|
||||
</div>
|
||||
<dl class="fee-calculation-grid">
|
||||
<div><dt>计费方式</dt><dd>{{ getChargeModeLabel(props.data.latestFee.chargeMode) }} · {{ getTaxModeLabel(props.data.latestFee.taxMode) }}</dd></div>
|
||||
<div>
|
||||
<dt>换算与精度</dt>
|
||||
<dd>
|
||||
系数 {{ formatInboundNumber(props.data.latestFee.conversionFactor) }} ·
|
||||
保留 {{ props.data.latestFee.roundingScale }} 位 {{ getRoundingModeLabel(props.data.latestFee.roundingMode) }}
|
||||
</dd>
|
||||
</div>
|
||||
<div><dt>实际重量</dt><dd>{{ formatInboundMeasure(props.data.latestFee.actualWeightKg, 'kg') }}</dd></div>
|
||||
<div><dt>实际体积</dt><dd>{{ formatInboundMeasure(props.data.latestFee.actualVolumeCbm, 'm³') }}</dd></div>
|
||||
<div><dt>折算量</dt><dd>{{ formatInboundMeasure(props.data.latestFee.convertedQuantity, props.data.latestFee.chargeUnit) }}</dd></div>
|
||||
<div><dt>最终计费量</dt><dd>{{ formatInboundMeasure(props.data.latestFee.chargeableQuantity, props.data.latestFee.chargeUnit) }}</dd></div>
|
||||
<div><dt>单价</dt><dd>{{ formatInboundMoney(props.data.latestFee.unitPrice, props.data.latestFee.currency) }} / {{ props.data.latestFee.chargeUnit }}</dd></div>
|
||||
<div><dt>基础费用</dt><dd>{{ formatInboundMoney(props.data.latestFee.subtotalAmount, props.data.latestFee.currency) }}</dd></div>
|
||||
<div><dt>附加费用</dt><dd>{{ formatInboundMoney(props.data.latestFee.additionalAmount, props.data.latestFee.currency) }}</dd></div>
|
||||
</dl>
|
||||
<div class="fee-total">
|
||||
<div>
|
||||
<span>费用合计</span>
|
||||
<small>{{ props.data.latestFee.ruleCode }} v{{ props.data.latestFee.chargeRuleVersion }} · {{ props.data.latestFee.rateCode }} v{{ props.data.latestFee.carrierRateVersion }}</small>
|
||||
</div>
|
||||
<strong>{{ formatInboundMoney(props.data.latestFee.totalAmount, props.data.latestFee.currency) }}</strong>
|
||||
</div>
|
||||
<p class="fee-sent-time">发布于 {{ formatInboundDateTime(props.data.latestFee.sentAt) }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="props.data.inbound.publicRemark" class="inbound-public-remark">
|
||||
<strong>平台备注</strong>
|
||||
<p>{{ props.data.inbound.publicRemark }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.inbound-billing-panel { margin-top: 22px; }
|
||||
.inbound-billing-heading, .inbound-section-title, .fee-total { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.inbound-billing-heading h3, .inbound-result-section h4 { margin: 0; color: var(--navy); }
|
||||
.inbound-billing-heading h3 { font-size: 15px; }
|
||||
.inbound-result-section h4 { font-size: 13px; }
|
||||
.inbound-billing-loading { min-height: 180px; padding: 18px 0; }
|
||||
.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
||||
.inbound-billing-state { display: grid; min-height: 112px; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 13px; margin-top: 12px; padding: 18px; border: 1px dashed #cbd5da; }
|
||||
.inbound-billing-state > svg { color: var(--primary); }
|
||||
.inbound-billing-state > div { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
|
||||
.inbound-billing-state strong { color: var(--navy); font-size: 13px; }
|
||||
.inbound-billing-state span { overflow-wrap: anywhere; color: var(--muted); font-size: 11px; line-height: 1.55; }
|
||||
.inbound-billing-state.is-error { border-style: solid; border-color: #e8c5bd; background: #fff8f6; }
|
||||
.inbound-billing-state.is-error > svg { color: #b5472f; }
|
||||
.inbound-overview { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 12px 0 0; border: 1px solid var(--border); }
|
||||
.inbound-overview > div { min-width: 0; padding: 13px; border-bottom: 1px solid var(--border); }
|
||||
.inbound-overview > div:nth-child(odd) { border-right: 1px solid var(--border); }
|
||||
.inbound-overview > div:nth-last-child(-n + 2) { border-bottom: 0; }
|
||||
.inbound-overview dt, .inbound-measurements dt, .fee-calculation-grid dt { color: var(--muted); font-size: 10px; }
|
||||
.inbound-overview dd, .inbound-measurements dd, .fee-calculation-grid dd { margin: 5px 0 0; overflow-wrap: anywhere; color: var(--navy); font-size: 12px; font-weight: 650; }
|
||||
.inbound-result-section { margin-top: 18px; }
|
||||
.inbound-section-title > span { color: var(--muted); font-size: 10px; text-align: right; }
|
||||
.inbound-measurements { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 10px 0 0; background: #f6f8f8; }
|
||||
.inbound-measurements > div { min-width: 0; padding: 12px; }
|
||||
.pallet-location-list { margin-top: 9px; border-top: 1px solid var(--border); }
|
||||
.pallet-location-list article { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 11px; padding: 12px 0; border-bottom: 1px solid var(--border); }
|
||||
.pallet-location-list article > svg { color: var(--primary); }
|
||||
.pallet-location-main, .pallet-location-quantity { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
|
||||
.pallet-location-main strong { overflow-wrap: anywhere; color: var(--navy); font-size: 12px; }
|
||||
.pallet-location-main span, .pallet-location-quantity span { color: var(--muted); font-size: 10px; }
|
||||
.pallet-location-quantity { align-items: flex-end; }
|
||||
.inbound-inline-empty { margin: 9px 0 0; padding: 14px; background: #f6f8f8; color: var(--muted); font-size: 11px; text-align: center; }
|
||||
.fee-result { padding-top: 18px; border-top: 1px solid var(--border); }
|
||||
.fee-calculation-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 10px 0 0; border: 1px solid var(--border); }
|
||||
.fee-calculation-grid > div { min-width: 0; padding: 11px 12px; border-bottom: 1px solid var(--border); }
|
||||
.fee-calculation-grid > div:not(:nth-child(3n)) { border-right: 1px solid var(--border); }
|
||||
.fee-calculation-grid > div:nth-last-child(-n + 3) { border-bottom: 0; }
|
||||
.fee-total { padding: 15px 0 0; }
|
||||
.fee-total > div { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
|
||||
.fee-total span { color: var(--navy); font-size: 12px; font-weight: 650; }
|
||||
.fee-total small, .fee-sent-time { color: var(--muted); font-size: 10px; }
|
||||
.fee-total strong { flex: 0 0 auto; color: var(--primary); font-size: 21px; }
|
||||
.fee-sent-time { margin: 5px 0 0; text-align: right; }
|
||||
.inbound-public-remark { margin-top: 16px; padding: 13px 14px; background: #f2f7f6; }
|
||||
.inbound-public-remark strong { color: var(--navy); font-size: 11px; }
|
||||
.inbound-public-remark p { margin: 5px 0 0; overflow-wrap: anywhere; color: var(--muted); font-size: 11px; line-height: 1.55; white-space: pre-wrap; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.inbound-billing-state { grid-template-columns: auto minmax(0, 1fr); }
|
||||
.inbound-billing-state .el-button { grid-column: 1 / -1; width: 100%; min-height: 44px; }
|
||||
.inbound-overview, .inbound-measurements, .fee-calculation-grid { grid-template-columns: 1fr; }
|
||||
.inbound-overview > div, .inbound-overview > div:nth-child(odd), .fee-calculation-grid > div, .fee-calculation-grid > div:not(:nth-child(3n)) { border-right: 0; border-bottom: 1px solid var(--border); }
|
||||
.inbound-overview > div:last-child, .fee-calculation-grid > div:last-child { border-bottom: 0; }
|
||||
.pallet-location-list article { grid-template-columns: auto minmax(0, 1fr); }
|
||||
.pallet-location-quantity { grid-column: 2; align-items: flex-start; flex-direction: row; flex-wrap: wrap; gap: 8px; }
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.inbound-billing-heading { align-items: flex-start; flex-direction: column; }
|
||||
.fee-total { align-items: flex-start; flex-direction: column; }
|
||||
.fee-total strong { font-size: 19px; }
|
||||
.fee-sent-time { text-align: left; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user