- fix(pricing): 后置消耗/损耗金额 100% 划归号主收益,超出预收部分平台不抽成,避免被平台费吸收 - feat(checkout): 前端新增结账实时试算预览组件 (CheckoutTrialPreview + useCheckoutSimulator),角色隔离展示(仅见自身收入/退款,屏蔽平台服务费) - refactor(admin): 重构交易全流程时间线 (ProcessTimeline.vue),将平铺挤压指标重构成4大分类仪表盘微型卡片矩阵,升级可视化业务状态流转徽章与阶段色彩主题 - style(admin): 优化支付与交接流水元信息网格布局,防止超宽屏下长单行拉伸
266 lines
6.8 KiB
Vue
266 lines
6.8 KiB
Vue
<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>
|