feat(checkout): 修复后置损耗结算全额归号主、支持角色隔离实时试算与全流程UI重构

- fix(pricing): 后置消耗/损耗金额 100% 划归号主收益,超出预收部分平台不抽成,避免被平台费吸收
- feat(checkout): 前端新增结账实时试算预览组件 (CheckoutTrialPreview + useCheckoutSimulator),角色隔离展示(仅见自身收入/退款,屏蔽平台服务费)
- refactor(admin): 重构交易全流程时间线 (ProcessTimeline.vue),将平铺挤压指标重构成4大分类仪表盘微型卡片矩阵,升级可视化业务状态流转徽章与阶段色彩主题
- style(admin): 优化支付与交接流水元信息网格布局,防止超宽屏下长单行拉伸
This commit is contained in:
yml2213
2026-09-13 10:19:33 +08:00
parent 7734f315ce
commit dff0577385
10 changed files with 1426 additions and 153 deletions
@@ -0,0 +1,265 @@
<script setup lang="ts">
import type { CheckoutSimulation } from '../composables/useCheckoutSimulator'
import { money } from '../composables/useOrderSnapshot'
const props = withDefaults(
defineProps<{
simulation: CheckoutSimulation
variant?: 'desktop' | 'mobile'
}>(),
{ variant: 'desktop' }
)
</script>
<template>
<div v-if="props.simulation" :class="['trial-preview-container', props.variant]">
<!-- ───────────── 号主视角 ───────────── -->
<template v-if="props.simulation.role === 'owner'">
<div class="trial-header">
<span class="trial-title"> <i class="trial-icon">💰</i> 结账到手试算(仅您可见) </span>
<span v-if="props.simulation.isDepositSufficient" class="badge success">押金充足</span>
<span v-else class="badge danger">押金不足</span>
</div>
<div class="trial-body">
<div class="main-metric">
<span class="label">预计最终到手总额</span>
<strong class="value income">¥{{ money(props.simulation.totalIncomeYuan) }}</strong>
</div>
<div class="metric-breakdown">
<div class="breakdown-item">
<span>租金收益</span>
<span>¥{{ money(props.simulation.rentIncomeYuan) }}</span>
</div>
<div v-if="props.simulation.damageCompensationYuan > 0" class="breakdown-item highlight">
<span>损耗/损坏赔偿 (全额到手)</span>
<span class="text-green">+¥{{ money(props.simulation.damageCompensationYuan) }}</span>
</div>
</div>
<div v-if="props.simulation.isDepositSufficient" class="trial-note info">
<span>💡 租客押金充足,方案确认后款项将全额转入您的可用余额,平台 0 抽成。</span>
</div>
<div v-else class="trial-note warning">
<span
>⚠️ 租客押金不足差额 ¥{{
money(props.simulation.shortfallYuan)
}},无法自动完结,需协商调低金额或发起争议。</span
>
</div>
</div>
</template>
<!-- ───────────── 租客视角 ───────────── -->
<template v-else-if="props.simulation.role === 'renter'">
<div class="trial-header">
<span class="trial-title"> <i class="trial-icon">🧾</i> 费用与退款试算(仅您可见) </span>
<span v-if="props.simulation.isDepositSufficient" class="badge info">正常结算</span>
<span v-else class="badge danger">需补差额</span>
</div>
<div class="trial-body">
<div class="metrics-grid">
<div class="main-metric">
<span class="label">实际租用消费</span>
<strong class="value">¥{{ money(props.simulation.actualCostYuan) }}</strong>
</div>
<div class="main-metric">
<span class="label">预计原路退还</span>
<strong class="value refund">¥{{ money(props.simulation.totalRefundYuan) }}</strong>
</div>
</div>
<div class="metric-breakdown">
<div class="breakdown-item">
<span>实际使用租金</span>
<span>¥{{ money(props.simulation.buyerRentYuan) }}</span>
</div>
<div v-if="props.simulation.damageDeductYuan > 0" class="breakdown-item">
<span>损耗赔偿扣除 (从押金)</span>
<span class="text-danger">-¥{{ money(props.simulation.damageDeductYuan) }}</span>
</div>
<div v-if="props.simulation.rentRefundYuan > 0" class="breakdown-item">
<span>未用租金退还</span>
<span class="text-green">+¥{{ money(props.simulation.rentRefundYuan) }}</span>
</div>
<div class="breakdown-item">
<span>剩余押金退还</span>
<span class="text-green">+¥{{ money(props.simulation.depositRefundYuan) }}</span>
</div>
</div>
<div v-if="props.simulation.overshootYuan > 0" class="trial-note warning">
<span
>ℹ️ 哈夫币使用已打超预收,超出金额 ¥{{
money(props.simulation.overshootYuan)
}}
已自动从押金抵扣。</span
>
</div>
<div v-if="!props.simulation.isDepositSufficient" class="trial-note danger">
<span
>⚠️ 押金不足以抵扣打超与赔偿,差额 ¥{{
money(props.simulation.shortfallYuan)
}},无法自动同意。</span
>
</div>
</div>
</template>
</div>
</template>
<style scoped>
.trial-preview-container {
margin-top: 12px;
padding: 12px 14px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 13px;
color: #334155;
transition: all 0.2s ease;
}
.trial-preview-container.mobile {
margin: 10px 12px 16px;
padding: 10px 12px;
font-size: 12px;
}
.trial-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid #e2e8f0;
}
.trial-title {
display: flex;
align-items: center;
gap: 6px;
font-weight: 600;
color: #1e293b;
}
.trial-icon {
font-style: normal;
}
.badge {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
font-weight: 500;
}
.badge.success {
background: #dcfce7;
color: #15803d;
}
.badge.info {
background: #e0f2fe;
color: #0369a1;
}
.badge.danger {
background: #fee2e2;
color: #b91c1c;
}
.metrics-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 8px;
}
.main-metric {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 6px 10px;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 6px;
margin-bottom: 6px;
}
.main-metric .label {
font-size: 12px;
color: #64748b;
}
.main-metric .value {
font-size: 16px;
font-weight: 700;
color: #0f172a;
}
.main-metric .value.income {
color: #16a34a;
}
.main-metric .value.refund {
color: #0284c7;
}
.metric-breakdown {
display: flex;
flex-direction: column;
gap: 4px;
padding: 4px 6px;
}
.breakdown-item {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: #475569;
}
.breakdown-item.highlight {
font-weight: 600;
}
.text-green {
color: #16a34a;
font-weight: 600;
}
.text-danger {
color: #dc2626;
font-weight: 600;
}
.trial-note {
margin-top: 8px;
padding: 6px 10px;
border-radius: 6px;
font-size: 11px;
line-height: 1.4;
}
.trial-note.info {
background: #f0fdf4;
color: #166534;
border: 1px solid #bbf7d0;
}
.trial-note.warning {
background: #fffbeb;
color: #92400e;
border: 1px solid #fde68a;
}
.trial-note.danger {
background: #fef2f2;
color: #991b1b;
border: 1px solid #fecaca;
}
</style>