Files
admin-web/src/modules/inbounds/fee/InboundFeePanel.vue
T
2026-08-18 09:09:34 +08:00

524 lines
26 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, reactive, ref, watch } from 'vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { CircleDollarSign, PencilLine, RefreshCw } from '@lucide/vue'
import { firstLegBusinessTypeLabel, firstLegTaxModeLabel } from '@/shared/utils/firstLegBusinessLabels'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import OperationErrorToast from '@/shared/components/OperationErrorToast.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
compact?: boolean
}>()
const emit = defineEmits<{
changed: [snapshot: InboundFeeSnapshot]
loaded: [snapshot: InboundFeeSnapshot | null]
}>()
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({
expectedFeeVersion: 0,
chargeableQuantity: 0,
unitPrice: 0,
additionalAmount: 0,
reason: '',
})
const adjustmentRules: FormRules = {
chargeableQuantity: [{ required: true, message: '请输入调整后的计费量', trigger: 'change' }],
unitPrice: [{ 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 === 'CALCULATED'
&& snapshot.value?.nextProcessStatus === 'WAIT_SPLIT'
))
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: '未知状态', 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 firstLegTaxModeLabel(value)
}
function roundingModeLabel(value: string) {
const labels: Record<string, string> = {
UP: '远离零方向', DOWN: '向零方向', CEILING: '向上', FLOOR: '向下',
HALF_UP: '四舍五入', HALF_DOWN: '五舍六入', HALF_EVEN: '银行家舍入',
}
return labels[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
emit('loaded', result)
if (!options.preserveAdjustment && adjustOpen.value) fillAdjustmentForm(result)
} catch (error) {
if (sequence !== loadSequence) return
snapshot.value = null
emit('loaded', 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.expectedFeeVersion = Number(value.feeVersion)
adjustmentForm.chargeableQuantity = Number(value.chargeableQuantity)
adjustmentForm.unitPrice = Number(value.unitPrice)
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 {
expectedFeeVersion: adjustmentForm.expectedFeeVersion,
chargeableQuantity: Number(adjustmentForm.chargeableQuantity),
unitPrice: Number(adjustmentForm.unitPrice),
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" :class="{ 'fee-heading--compact': compact }">
<div v-if="!compact" 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>
<strong>{{ formatMoney(snapshot.unitPrice, snapshot.currency) }} / {{ chargeUnitLabel }}</strong>
<small>{{ snapshot.channelCode }}</small>
</div>
<div class="fee-summary-item">
<span>附加费</span>
<strong>{{ formatMoney(snapshot.additionalAmount, snapshot.currency) }}</strong>
<small>{{ snapshot.additionalAmount ? '已计入总额' : '无附加费' }}</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 v-if="!compact">实际量与折算量取大后计费,金额以服务端快照为准。</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>
<details class="fee-advanced">
<summary>查看价格来源与时间记录</summary>
<div class="fee-detail-grid">
<section v-if="snapshot.ruleCode" 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>{{ firstLegBusinessTypeLabel(snapshot.businessType) }}</dd></div>
<div><dt>计费口径</dt><dd>{{ chargeModeLabel }}</dd></div>
<div><dt>舍入规则</dt><dd>{{ snapshot.roundingScale == null ? '—' : `保留 ${snapshot.roundingScale} 位,${roundingModeLabel(snapshot.roundingMode)}` }}</dd></div>
</dl>
</section>
<section aria-labelledby="rate-title">
<div class="section-title"><div><h4 id="rate-title">渠道价格</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 }}</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></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>
</details>
</div>
<OperationErrorToast :details="adjustmentError" />
<AppDialog
v-model="adjustOpen"
title="调整入库费用"
width="min(760px, 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-form ref="adjustFormRef" :model="adjustmentForm" :rules="adjustmentRules" label-position="top" class="adjustment-form" :disabled="adjusting">
<div class="adjustment-amounts">
<el-form-item label="调整后计费量" prop="chargeableQuantity" required>
<el-input-number v-model="adjustmentForm.chargeableQuantity" aria-label="调整后计费量" :min="0.0001" :max="999999999999.9999" :precision="4" :step="1" controls-position="right" />
<span class="adjustment-unit">{{ chargeUnitLabel }}</span>
</el-form-item>
<el-form-item label="调整后单价" prop="unitPrice" required>
<el-input-number v-model="adjustmentForm.unitPrice" aria-label="调整后单价" :min="0" :max="999999999999.9999" :precision="4" :step="0.1" controls-position="right" />
<span class="adjustment-unit">{{ snapshot?.currency }} / {{ chargeUnitLabel }}</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 class="adjustment-unit">{{ snapshot?.currency }}</span>
</el-form-item>
</div>
<div class="adjustment-total"><span>当前服务端计算金额</span><strong>{{ formatMoney(snapshot?.totalAmount, snapshot?.currency) }}</strong><small>提交后以服务端返回的新版本金额为准</small></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--compact { justify-content: flex-end; padding-bottom: 6px; }
.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(230px, 1.3fr) repeat(4, minmax(135px, 1fr)); border-top: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
.fee-total, .fee-summary-item { display: flex; min-width: 0; flex-direction: column; justify-content: center; gap: 3px; padding: 11px 14px; }
.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: 21px; letter-spacing: -.01em; }
.fee-summary-item strong { font-size: 15px; }
.version-status { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.calculation-section, .fee-timeline { padding: 10px 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: 7px; border-top: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
.calculation-flow > div { display: flex; min-width: 0; min-height: 46px; flex-direction: row; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 10px; 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: 13px; overflow-wrap: anywhere; }
.calculation-flow small { display: none; }
.calculation-flow .is-chargeable { background: #f7fbfa; }
.calculation-flow .is-chargeable strong { color: #087d70; }
.amount-equation { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; margin-top: 5px; padding: 4px 0 0; 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; }
.fee-advanced { border-top: 1px solid var(--cc-border); }
.fee-advanced > summary { padding: 10px 2px; color: var(--cc-primary); font-size: 12px; font-weight: 600; cursor: pointer; }
.fee-advanced:not([open]) > summary { border-bottom: 0; }
.fee-advanced .fee-detail-grid > section:only-child { grid-column: 1 / -1; }
.adjustment-alert { margin-top: 12px; }
.adjustment-form { margin-top: 18px; }
.adjustment-amounts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; }
.adjustment-amounts :deep(.el-form-item) { min-width: 0; margin-bottom: 18px; }
.adjustment-amounts :deep(.el-form-item__content) { display: grid; grid-template-columns: minmax(0, 1fr) auto; flex-wrap: nowrap; column-gap: 8px; }
.adjustment-amounts :deep(.el-input-number) { width: 100%; }
.adjustment-unit { min-width: max-content; color: var(--cc-muted); font-size: 12px; white-space: nowrap; }
.adjustment-total { display: grid; grid-template-columns: auto minmax(150px, 1fr) auto; align-items: baseline; gap: 18px; margin: 0 0 18px; padding: 13px 16px; border-radius: 8px; background: #f8fafc; }
.adjustment-total span { color: var(--cc-muted); font-size: 12px; }
.adjustment-total strong { justify-self: center; font-size: 18px; white-space: nowrap; }
.adjustment-total small { color: var(--cc-muted); font-size: 11px; text-align: right; }
.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 { 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; }
.adjustment-amounts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.adjustment-total { grid-template-columns: auto 1fr; }
.adjustment-total strong { justify-self: end; }
.adjustment-total small { grid-column: 1 / -1; text-align: left; }
}
@media (max-width: 520px) {
.adjustment-amounts { grid-template-columns: 1fr; gap: 0; }
.adjustment-total { grid-template-columns: 1fr; gap: 6px; }
.adjustment-total strong { justify-self: start; }
}
@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>