This commit is contained in:
2026-08-04 21:01:39 +08:00
parent 88bd939170
commit 5cb1ca307d
77 changed files with 9914 additions and 131 deletions
@@ -0,0 +1,477 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { CircleDollarSign, PencilLine, RefreshCw } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import {
ApiResponseError,
getApiErrorDetails,
type ApiErrorDetails,
} from '@/shared/api/http'
import {
adjustPublishedInboundFee,
createFeeAdjustmentIdempotencyKey,
getLatestInboundFee,
} from './api'
import type { AdjustPublishedInboundFeeRequest, InboundFeeSnapshot } from './types'
const props = defineProps<{
customerId?: string
tenantId?: string
inboundId: string
}>()
const emit = defineEmits<{
changed: [snapshot: InboundFeeSnapshot]
}>()
const access = useAccessPreviewStore()
const loading = ref(false)
const adjusting = ref(false)
const notFound = ref(false)
const loadError = ref<ApiErrorDetails | null>(null)
const adjustmentError = ref<ApiErrorDetails | null>(null)
const conflictRefreshed = ref(false)
const snapshot = ref<InboundFeeSnapshot | null>(null)
const adjustOpen = ref(false)
const adjustFormRef = ref<FormInstance>()
let loadSequence = 0
let adjustmentFingerprint = ''
let adjustmentIdempotencyKey = ''
const adjustmentForm = reactive({
subtotalAmount: 0,
additionalAmount: 0,
reason: '',
})
const adjustmentRules: FormRules = {
subtotalAmount: [{ required: true, message: '请输入调整后的基础运费', trigger: 'change' }],
additionalAmount: [{ required: true, message: '请输入调整后的附加费', trigger: 'change' }],
reason: [
{ required: true, whitespace: true, message: '请填写费用调整原因', trigger: 'blur' },
{ min: 2, max: 500, message: '调整原因需为 2–500 个字符', trigger: 'blur' },
],
}
const canView = computed(() => access.can('firstleg:fba-pricing:view'))
const canAdjust = computed(() => access.can('firstleg:fba-pricing:adjust'))
const customerContextId = computed(() => props.customerId || props.tenantId || '')
const adjustmentEligible = computed(() => (
snapshot.value?.feeStatus === 'SENT'
&& snapshot.value.nextProcessStatus === 'INBOUNDED'
))
const chargeModeLabel = computed(() => snapshot.value?.chargeMode === 'KG' ? '按重量计费' : '按体积计费')
const chargeUnitLabel = computed(() => snapshot.value?.chargeUnit === 'KG' ? 'KG' : 'CBM')
const statusInfo = computed(() => feeStatusInfo(snapshot.value?.feeStatus))
function feeStatusInfo(status?: string) {
const map: Record<string, { label: string; type: 'success' | 'warning' | 'info' }> = {
CALCULATED: { label: '已试算', type: 'warning' },
CONFIRMED: { label: '已确认', type: 'info' },
SENT: { label: '已发布', type: 'success' },
SUPERSEDED: { label: '历史版本', type: 'info' },
}
return map[status || ''] || { label: status || '未知状态', type: 'info' as const }
}
function formatNumber(value: number | null | undefined, digits = 3) {
if (value === null || value === undefined || !Number.isFinite(Number(value))) return '—'
return new Intl.NumberFormat('zh-CN', { maximumFractionDigits: digits }).format(Number(value))
}
function formatMoney(value: number | null | undefined, currency?: string) {
if (value === null || value === undefined || !Number.isFinite(Number(value))) return '—'
try {
return new Intl.NumberFormat('zh-CN', {
style: 'currency', currency: currency || 'CNY', minimumFractionDigits: 2,
}).format(Number(value))
} catch {
return `${formatNumber(value, 2)} ${currency || ''}`.trim()
}
}
function formatDateTime(value: string | null | undefined) {
if (!value) return '—'
const date = new Date(value)
if (Number.isNaN(date.getTime())) return value.replace('T', ' ')
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', hour12: false,
}).format(date)
}
function taxModeLabel(value: string) {
return value === 'TAX_INCLUDED' ? '包税' : value === 'TAX_EXCLUDED' ? '不包税' : value
}
function roundingModeLabel(value: string) {
const labels: Record<string, string> = {
UP: '远离零方向', DOWN: '向零方向', CEILING: '向上', FLOOR: '向下',
HALF_UP: '四舍五入', HALF_DOWN: '五舍六入', HALF_EVEN: '银行家舍入',
}
return labels[value] || value
}
async function loadFee(options: { preserveAdjustment?: boolean } = {}) {
const sequence = ++loadSequence
loadError.value = null
notFound.value = false
if (!customerContextId.value || !props.inboundId || !canView.value) {
snapshot.value = null
loading.value = false
return
}
loading.value = true
try {
const result = await getLatestInboundFee(customerContextId.value, props.inboundId)
if (sequence !== loadSequence) return
snapshot.value = result
if (!options.preserveAdjustment && adjustOpen.value) fillAdjustmentForm(result)
} catch (error) {
if (sequence !== loadSequence) return
snapshot.value = null
if (error instanceof ApiResponseError && error.code === 'A0404') notFound.value = true
else loadError.value = getApiErrorDetails(error, '费用快照加载失败,请稍后重试')
} finally {
if (sequence === loadSequence) loading.value = false
}
}
function fillAdjustmentForm(value: InboundFeeSnapshot) {
adjustmentForm.subtotalAmount = Number(value.subtotalAmount)
adjustmentForm.additionalAmount = Number(value.additionalAmount)
adjustmentForm.reason = ''
adjustmentError.value = null
conflictRefreshed.value = false
adjustmentFingerprint = ''
adjustmentIdempotencyKey = ''
}
function openAdjustment() {
if (!snapshot.value || !adjustmentEligible.value) return
fillAdjustmentForm(snapshot.value)
adjustOpen.value = true
}
function adjustmentPayload(): AdjustPublishedInboundFeeRequest {
return {
subtotalAmount: Number(adjustmentForm.subtotalAmount),
additionalAmount: Number(adjustmentForm.additionalAmount),
reason: adjustmentForm.reason.trim(),
}
}
function getAdjustmentKey(payload: AdjustPublishedInboundFeeRequest) {
const fingerprint = JSON.stringify(payload)
if (!adjustmentIdempotencyKey || fingerprint !== adjustmentFingerprint) {
adjustmentFingerprint = fingerprint
adjustmentIdempotencyKey = createFeeAdjustmentIdempotencyKey()
}
return adjustmentIdempotencyKey
}
async function submitAdjustment() {
if (!snapshot.value) return
const valid = await adjustFormRef.value?.validate().catch(() => false)
if (!valid) return
const payload = adjustmentPayload()
adjustmentError.value = null
conflictRefreshed.value = false
adjusting.value = true
try {
const result = await adjustPublishedInboundFee(
customerContextId.value,
props.inboundId,
payload,
getAdjustmentKey(payload),
)
snapshot.value = result
adjustOpen.value = false
ElMessage.success(`费用已调整为第 ${result.feeVersion} 版`)
emit('changed', result)
} catch (error) {
adjustmentError.value = getApiErrorDetails(error, '费用调整失败,请检查后重试')
if (error instanceof ApiResponseError && error.code === 'A0409') {
await loadFee({ preserveAdjustment: true })
conflictRefreshed.value = true
adjustmentFingerprint = ''
adjustmentIdempotencyKey = ''
}
} finally {
adjusting.value = false
}
}
watch(
() => [customerContextId.value, props.inboundId, access.permissionSignature],
() => {
adjustOpen.value = false
snapshot.value = null
void loadFee()
},
{ immediate: true },
)
defineExpose({ reload: loadFee })
</script>
<template>
<section class="inbound-fee-panel" :aria-busy="loading" aria-labelledby="inbound-fee-title">
<header class="fee-heading">
<div class="fee-heading-copy">
<span class="fee-heading-icon" aria-hidden="true"><CircleDollarSign :size="20" /></span>
<div>
<h3 id="inbound-fee-title">入库费用快照</h3>
<p>展示审核完成后锁定的计费依据和对客费用,不在此重复触发试算或发布。</p>
</div>
</div>
<div class="fee-heading-actions">
<el-tooltip content="刷新费用快照" placement="top">
<el-button
circle
aria-label="刷新费用快照"
:loading="loading"
:disabled="!customerContextId || !inboundId || !canView"
@click="loadFee()"
><RefreshCw :size="16" /></el-button>
</el-tooltip>
<el-button
v-if="canAdjust && adjustmentEligible"
data-testid="adjust-fee-button"
type="primary"
@click="openAdjustment"
><PencilLine :size="16" />调整已发布费用</el-button>
</div>
</header>
<el-alert v-if="!canView" type="warning" :closable="false" show-icon>
<template #title>当前账号没有查看入库费用的权限</template>
<p>需要权限:firstleg:fba-pricing:view</p>
</el-alert>
<div v-else-if="loading && !snapshot" class="fee-loading" aria-live="polite">
<el-skeleton :rows="6" animated />
<span class="sr-only">正在加载费用快照</span>
</div>
<el-alert v-else-if="loadError" type="error" :closable="false" show-icon role="alert">
<template #title>费用快照加载失败</template>
<p>{{ loadError.message }}</p>
<ApiErrorMeta :details="loadError" />
<el-button size="small" @click="loadFee()">重新加载</el-button>
</el-alert>
<el-empty v-else-if="notFound" :image-size="64" description="尚未生成费用快照">
<p class="empty-note">测量审核完成后,系统会自动生成并发布费用。</p>
</el-empty>
<div v-else-if="snapshot" class="fee-content">
<div class="fee-summary" aria-label="费用关键结果">
<div class="fee-total">
<span>对客费用总额 · {{ snapshot.currency }}</span>
<strong>{{ formatMoney(snapshot.totalAmount, snapshot.currency) }}</strong>
<small>基础运费小计 {{ formatMoney(snapshot.subtotalAmount, snapshot.currency) }} · 附加费 {{ formatMoney(snapshot.additionalAmount, snapshot.currency) }}</small>
</div>
<div class="fee-summary-item">
<span>最终计费量</span>
<strong>{{ formatNumber(snapshot.chargeableQuantity, snapshot.roundingScale) }} {{ chargeUnitLabel }}</strong>
<small>{{ chargeModeLabel }}</small>
</div>
<div class="fee-summary-item">
<span>快照版本</span>
<div class="version-status"><strong>V{{ snapshot.feeVersion }}</strong><el-tag :type="statusInfo.type" effect="light">{{ statusInfo.label }}</el-tag></div>
<small>{{ snapshot.feeNo }}</small>
</div>
</div>
<section class="calculation-section" aria-labelledby="calculation-title">
<div class="section-title">
<div><h4 id="calculation-title">计费过程</h4><p>实际量与折算量取大后计费,金额以服务端快照为准。</p></div>
<el-tag effect="plain">{{ taxModeLabel(snapshot.taxMode) }}</el-tag>
</div>
<div class="calculation-flow">
<div><span>实际重量</span><strong>{{ formatNumber(snapshot.actualWeightKg, 3) }} KG</strong></div>
<div><span>实际体积</span><strong>{{ formatNumber(snapshot.actualVolumeCbm, 4) }} CBM</strong></div>
<div><span>折算量</span><strong>{{ formatNumber(snapshot.convertedQuantity, snapshot.roundingScale) }} {{ chargeUnitLabel }}</strong><small>系数 {{ formatNumber(snapshot.conversionFactor, 4) }}</small></div>
<div class="is-chargeable"><span>取大后的计费量</span><strong>{{ formatNumber(snapshot.chargeableQuantity, snapshot.roundingScale) }} {{ chargeUnitLabel }}</strong></div>
</div>
<div class="amount-equation" aria-label="费用计算公式">
<span>{{ formatNumber(snapshot.chargeableQuantity, snapshot.roundingScale) }} {{ chargeUnitLabel }}</span>
<i>×</i>
<span>{{ formatMoney(snapshot.unitPrice, snapshot.currency) }} / {{ chargeUnitLabel }}</span>
<i>+</i>
<span>附加费 {{ formatMoney(snapshot.additionalAmount, snapshot.currency) }}</span>
<i>=</i>
<strong>{{ formatMoney(snapshot.totalAmount, snapshot.currency) }}</strong>
</div>
</section>
<div class="fee-detail-grid">
<section aria-labelledby="rule-title">
<div class="section-title"><div><h4 id="rule-title">客户计费规则</h4><p>{{ snapshot.ruleCode }} · V{{ snapshot.chargeRuleVersion }}</p></div></div>
<dl>
<div><dt>客户</dt><dd>{{ snapshot.customerName }}</dd></div>
<div><dt>业务类型</dt><dd>{{ snapshot.businessType }}</dd></div>
<div><dt>计费口径</dt><dd>{{ chargeModeLabel }}</dd></div>
<div><dt>舍入规则</dt><dd>保留 {{ snapshot.roundingScale }} 位,{{ roundingModeLabel(snapshot.roundingMode) }}</dd></div>
</dl>
</section>
<section aria-labelledby="rate-title">
<div class="section-title"><div><h4 id="rate-title">船司 FBA 价格</h4><p>{{ snapshot.rateCode }} · V{{ snapshot.carrierRateVersion }}</p></div></div>
<dl>
<div><dt>起运仓</dt><dd>{{ snapshot.originWarehouseName }}</dd></div>
<div><dt>渠道 / 船司</dt><dd>{{ snapshot.channelCode }} / {{ snapshot.carrierName }}</dd></div>
<div><dt>FBA 目的仓</dt><dd>{{ snapshot.destinationCode }}</dd></div>
<div><dt>价格有效期</dt><dd>{{ formatDateTime(snapshot.rateEffectiveFrom) }} 至 {{ formatDateTime(snapshot.rateEffectiveTo) }}</dd></div>
</dl>
</section>
</div>
<section class="fee-timeline" aria-labelledby="fee-timeline-title">
<div class="section-title"><div><h4 id="fee-timeline-title">快照时间</h4><p>每个节点均由后端状态机记录。</p></div></div>
<ol>
<li><span>试算</span><strong>{{ formatDateTime(snapshot.calculatedAt) }}</strong></li>
<li :class="{ pending: !snapshot.confirmedAt }"><span>确认</span><strong>{{ formatDateTime(snapshot.confirmedAt) }}</strong></li>
<li :class="{ pending: !snapshot.sentAt }"><span>发布给客户</span><strong>{{ formatDateTime(snapshot.sentAt) }}</strong></li>
</ol>
</section>
</div>
<AppDialog
v-model="adjustOpen"
title="调整已发布费用"
width="min(560px, calc(100vw - 28px))"
confirm-text="确认调整并生成新版本"
confirm-type="warning"
:confirm-loading="adjusting"
@confirm="submitAdjustment"
>
<el-alert type="warning" :closable="false" show-icon title="调整会生成新的已发布版本,不会回退实物入库状态。" />
<el-alert v-if="conflictRefreshed" class="adjustment-alert" type="warning" :closable="false" show-icon>
<template #title>费用已被其他操作员更新,系统已刷新最新快照</template>
<p>当前输入仍保留,请先核对最新版本后再提交。</p>
</el-alert>
<el-alert v-if="adjustmentError" class="adjustment-alert" type="error" :closable="false" show-icon role="alert">
<template #title>费用调整失败</template>
<p>{{ adjustmentError.message }}</p>
<ApiErrorMeta :details="adjustmentError" />
</el-alert>
<el-form ref="adjustFormRef" :model="adjustmentForm" :rules="adjustmentRules" label-position="top" class="adjustment-form" :disabled="adjusting">
<div class="adjustment-amounts">
<el-form-item label="调整后基础运费" prop="subtotalAmount" required>
<el-input-number v-model="adjustmentForm.subtotalAmount" aria-label="调整后基础运费" :min="0" :max="999999999999.99" :precision="2" :step="100" controls-position="right" />
<span>{{ snapshot?.currency }}</span>
</el-form-item>
<el-form-item label="调整后附加费" prop="additionalAmount" required>
<el-input-number v-model="adjustmentForm.additionalAmount" aria-label="调整后附加费" :min="0" :max="999999999999.99" :precision="2" :step="10" controls-position="right" />
<span>{{ snapshot?.currency }}</span>
</el-form-item>
</div>
<div class="adjustment-total"><span>调整后总额</span><strong>{{ formatMoney(adjustmentForm.subtotalAmount + adjustmentForm.additionalAmount, snapshot?.currency) }}</strong></div>
<el-form-item label="调整原因" prop="reason" required>
<el-input v-model="adjustmentForm.reason" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="说明价格变更、补充费用或修正原因,记录将进入审计历史" />
</el-form-item>
</el-form>
</AppDialog>
</section>
</template>
<style scoped>
.inbound-fee-panel { min-width: 0; color: var(--cc-text); }
.fee-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding-bottom: 16px; }
.fee-heading-copy { display: flex; min-width: 0; align-items: flex-start; gap: 12px; }
.fee-heading-icon { display: grid; width: 38px; height: 38px; flex: 0 0 38px; place-items: center; border-radius: 10px; background: #eef4ff; color: var(--cc-primary); }
.fee-heading h3, .section-title h4 { margin: 0; }
.fee-heading h3 { font-size: 16px; }
.fee-heading p, .section-title p { margin: 5px 0 0; color: var(--cc-muted); font-size: 12px; line-height: 1.55; }
.fee-heading-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
.fee-heading-actions .el-button { margin-left: 0; }
.fee-loading { padding: 20px 0; }
.inbound-fee-panel :deep(.el-alert p) { margin: 5px 0 0; line-height: 1.55; }
.inbound-fee-panel :deep(.el-alert .el-button) { margin-top: 8px; }
.empty-note { margin: 0; color: var(--cc-muted); font-size: 12px; }
.fee-content { border-top: 1px solid var(--cc-border); }
.fee-summary { display: grid; grid-template-columns: minmax(260px, 1.35fr) repeat(2, minmax(180px, 1fr)); border-bottom: 1px solid var(--cc-border); background: #f8fafc; }
.fee-total, .fee-summary-item { display: flex; min-width: 0; flex-direction: column; justify-content: center; gap: 6px; padding: 20px; }
.fee-summary-item { border-left: 1px solid var(--cc-border); }
.fee-summary span, .fee-summary small { color: var(--cc-muted); font-size: 11px; }
.fee-summary strong { font-size: 18px; line-height: 1.25; overflow-wrap: anywhere; }
.fee-total strong { color: #0f5c49; font-size: 27px; letter-spacing: -.02em; }
.version-status { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.calculation-section, .fee-timeline { padding: 22px 0; border-bottom: 1px solid var(--cc-border); }
.section-title { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.section-title h4 { font-size: 14px; }
.calculation-flow { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: 16px; border: 1px solid var(--cc-border); border-radius: 12px; overflow: hidden; }
.calculation-flow > div { display: flex; min-width: 0; min-height: 88px; flex-direction: column; justify-content: center; gap: 6px; padding: 14px 16px; border-right: 1px solid var(--cc-border); }
.calculation-flow > div:last-child { border-right: 0; }
.calculation-flow span, .calculation-flow small { color: var(--cc-muted); font-size: 11px; }
.calculation-flow strong { font-size: 15px; overflow-wrap: anywhere; }
.calculation-flow .is-chargeable { background: #f0fdf9; }
.calculation-flow .is-chargeable strong { color: #087d70; }
.amount-equation { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 12px; padding: 12px 14px; border-radius: 8px; background: #f8fafc; color: #475467; font-size: 12px; }
.amount-equation i { color: #98a2b3; font-style: normal; }
.amount-equation strong { color: var(--cc-text); font-size: 15px; }
.fee-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border-bottom: 1px solid var(--cc-border); }
.fee-detail-grid > section { min-width: 0; padding: 22px 20px 22px 0; }
.fee-detail-grid > section + section { padding-right: 0; padding-left: 20px; border-left: 1px solid var(--cc-border); }
.fee-detail-grid dl { margin: 14px 0 0; }
.fee-detail-grid dl > div { display: grid; grid-template-columns: 104px minmax(0, 1fr); gap: 12px; padding: 9px 0; border-top: 1px solid #edf0f4; font-size: 12px; }
.fee-detail-grid dt { color: var(--cc-muted); }
.fee-detail-grid dd { min-width: 0; margin: 0; font-weight: 600; overflow-wrap: anywhere; }
.fee-timeline { border-bottom: 0; }
.fee-timeline ol { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0; margin: 18px 0 0; padding: 0; list-style: none; }
.fee-timeline li { position: relative; display: flex; min-width: 0; flex-direction: column; gap: 5px; padding: 0 18px 0 20px; }
.fee-timeline li::before { position: absolute; top: 4px; left: 0; width: 9px; height: 9px; border-radius: 50%; background: #12b76a; content: ''; }
.fee-timeline li::after { position: absolute; top: 8px; right: 0; left: 10px; z-index: -1; height: 1px; background: var(--cc-border); content: ''; }
.fee-timeline li:last-child::after { display: none; }
.fee-timeline li.pending::before { background: #98a2b3; }
.fee-timeline span { color: var(--cc-muted); font-size: 11px; }
.fee-timeline strong { font-size: 12px; overflow-wrap: anywhere; }
.adjustment-alert { margin-top: 12px; }
.adjustment-form { margin-top: 18px; }
.adjustment-amounts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.adjustment-amounts :deep(.el-form-item__content) { flex-wrap: nowrap; }
.adjustment-amounts :deep(.el-input-number) { width: 100%; }
.adjustment-amounts :deep(.el-form-item__content > span) { margin-left: 8px; color: var(--cc-muted); font-size: 12px; }
.adjustment-total { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin: 0 0 18px; padding: 13px 14px; border-radius: 8px; background: #f8fafc; }
.adjustment-total span { color: var(--cc-muted); font-size: 12px; }
.adjustment-total strong { font-size: 18px; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
@media (max-width: 900px) {
.fee-summary { grid-template-columns: 1fr 1fr; }
.fee-total { grid-column: 1 / -1; }
.fee-summary-item:first-of-type { border-left: 0; }
.calculation-flow { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.calculation-flow > div:nth-child(2) { border-right: 0; }
.calculation-flow > div:nth-child(-n+2) { border-bottom: 1px solid var(--cc-border); }
}
@media (max-width: 720px) {
.fee-heading { align-items: stretch; flex-direction: column; }
.fee-heading-actions { justify-content: flex-start; }
.fee-heading-actions .el-button:not(.is-circle) { flex: 1; min-height: 44px; }
.fee-summary, .calculation-flow, .fee-detail-grid, .fee-timeline ol, .adjustment-amounts { grid-template-columns: 1fr; }
.fee-summary-item { border-top: 1px solid var(--cc-border); border-left: 0; }
.calculation-flow > div { min-height: 76px; border-right: 0; border-bottom: 1px solid var(--cc-border); }
.calculation-flow > div:last-child { border-bottom: 0; }
.fee-detail-grid > section, .fee-detail-grid > section + section { padding: 20px 0; border-left: 0; }
.fee-detail-grid > section + section { border-top: 1px solid var(--cc-border); }
.fee-timeline ol { gap: 16px; }
.fee-timeline li { padding-left: 20px; }
.fee-timeline li::after { display: none; }
}
@media (max-width: 360px) {
.fee-heading-copy { gap: 8px; }
.fee-heading-icon { width: 34px; height: 34px; flex-basis: 34px; }
.fee-total, .fee-summary-item { padding: 16px 12px; }
.fee-total strong { font-size: 22px; }
.fee-detail-grid dl > div { grid-template-columns: 1fr; gap: 4px; }
.amount-equation { align-items: flex-start; flex-direction: column; }
.amount-equation i { display: none; }
}
</style>