feat(listing): 展示保险后预计到手价

This commit is contained in:
yml2213
2026-11-04 21:36:13 +08:00
parent 12e9b6d0b2
commit 6355ca21b0
7 changed files with 222 additions and 3 deletions
@@ -37,7 +37,12 @@ import {
} from '@/features/seller/composables/usePublishDraft'
import type { PublishForm } from '@/shared/types/publish'
import { yuanToCent } from '@/shared/utils/money'
import { dailyLossOptions, formatNumber, roundRatio } from '@/shared/utils/pricing'
import {
calculateInsuranceFeeAmount,
dailyLossOptions,
formatNumber,
roundRatio,
} from '@/shared/utils/pricing'
import { useDistributionSiteStore } from '@/stores/distributionSite'
const draftSaveDelay = 400
@@ -113,6 +118,14 @@ export function usePublishForm(options: UsePublishFormOptions) {
receiveCostDelta,
isQualityOwner,
})
const calculatedInsuranceFee = computed(() =>
form.compensation_plan === 'insurance'
? calculateInsuranceFeeAmount(pricing.calculatedSellerPrice.value, insuranceFeeBps.value)
: 0
)
const calculatedTakeHomePrice = computed(() =>
Math.max(pricing.calculatedSellerPrice.value - calculatedInsuranceFee.value, 0)
)
const qualityOwnerSaleRatioSelected = computed(
() =>
isQualityOwner.value &&
@@ -1033,6 +1046,8 @@ export function usePublishForm(options: UsePublishFormOptions) {
isQualityOwnerExclusiveRatioActive,
insuranceFeeBps,
insuranceFeePercent,
calculatedInsuranceFee,
calculatedTakeHomePrice,
virtualAssetSaleAgreementChecked,
sellerAgreementChecked,
passwordAndDeviceConfirmed,
@@ -1109,6 +1109,56 @@
background: #faf5ff;
}
.take-home-preview {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1px;
overflow: hidden;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #e5e7eb;
}
.take-home-preview > div {
display: grid;
align-content: center;
gap: 4px;
min-width: 0;
padding: 10px;
background: #fff;
}
.take-home-preview span {
color: #858c96;
font-size: 10px;
line-height: 1.4;
}
.take-home-preview strong {
overflow-wrap: anywhere;
color: #20242a;
font-size: 15px;
font-weight: 800;
}
.take-home-preview strong.is-deduction {
color: #c2410c;
}
.take-home-preview .take-home-net {
grid-column: 1 / -1;
background: #fff7ed;
}
.take-home-preview .take-home-net span {
color: #9a3412;
}
.take-home-preview .take-home-net strong {
color: #f97316;
font-size: 19px;
}
.coverage-note {
margin: 0;
color: #858c96;
@@ -34,6 +34,8 @@ const {
qualityOwnerSaleRatio,
minAcceleratedSaleRatio,
insuranceFeePercent,
calculatedInsuranceFee,
calculatedTakeHomePrice,
showBanRecordRiskHint,
unlockDemulanEnabled,
nextSeasonPassEnabled,
@@ -937,6 +939,32 @@ const screenshotGuides: Record<string, string> = {
</em>
</button>
</div>
<div
class="take-home-preview"
:class="{ 'has-insurance': form.compensation_plan === 'insurance' }"
>
<div>
<span>卖家发布价格</span>
<strong>¥{{ formatMoney(calculatedSellerPrice) }}</strong>
</div>
<div>
<span v-if="form.compensation_plan === 'insurance'">
无忧租号保险({{ insuranceFeePercent.toFixed(1) }}%)
</span>
<span v-else>普通赔付</span>
<strong :class="{ 'is-deduction': form.compensation_plan === 'insurance' }">
{{
form.compensation_plan === 'insurance'
? `-¥${formatMoney(calculatedInsuranceFee)}`
: '¥0.0'
}}
</strong>
</div>
<div class="take-home-net">
<span>预计到手价格</span>
<strong>¥{{ formatMoney(calculatedTakeHomePrice) }}</strong>
</div>
</div>
<p class="coverage-note">
无忧租号保险在普通赔付基础上增加账号市场价值保障,服务费按实际到手租金扣除。
</p>
@@ -923,6 +923,10 @@
text-align: right;
}
.summary-breakdown strong.is-deduction {
color: #c2410c;
}
.summary-list {
display: grid;
gap: 8px;
@@ -1256,6 +1260,56 @@
box-shadow: 0 0 0 1px rgba(124, 58, 237, 0.08);
}
.take-home-preview {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1px;
overflow: hidden;
border: 1px solid #e5e7eb;
border-radius: 9px;
background: #e5e7eb;
}
.take-home-preview > div {
display: grid;
align-content: center;
gap: 6px;
min-width: 0;
padding: 12px 14px;
background: #fff;
}
.take-home-preview span {
color: #6b7280;
font-size: 11px;
font-weight: 700;
line-height: 1.45;
}
.take-home-preview strong {
overflow-wrap: anywhere;
color: #1f2937;
font-size: 17px;
font-weight: 900;
}
.take-home-preview strong.is-deduction {
color: #c2410c;
}
.take-home-preview .take-home-net {
background: #fff7ed;
}
.take-home-preview .take-home-net span {
color: #9a3412;
}
.take-home-preview .take-home-net strong {
color: var(--color-orange-primary);
font-size: 21px;
}
.coverage-note {
margin: 0;
color: #7b8492;
@@ -1350,4 +1404,12 @@
.coverage-option-grid {
grid-template-columns: 1fr;
}
.take-home-preview {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.take-home-preview .take-home-net {
grid-column: 1 / -1;
}
}
@@ -33,6 +33,8 @@ const {
qualityOwnerSaleRatio,
minAcceleratedSaleRatio,
insuranceFeePercent,
calculatedInsuranceFee,
calculatedTakeHomePrice,
virtualAssetSaleAgreementChecked,
sellerAgreementChecked,
passwordAndDeviceConfirmed,
@@ -766,6 +768,32 @@ function selectDailyLoss(value: string | number) {
</em>
</button>
</div>
<div
class="take-home-preview"
:class="{ 'has-insurance': form.compensation_plan === 'insurance' }"
>
<div>
<span>卖家发布价格</span>
<strong>¥{{ formatMoney(calculatedSellerPrice) }}</strong>
</div>
<div>
<span v-if="form.compensation_plan === 'insurance'">
无忧租号保险({{ insuranceFeePercent.toFixed(1) }}%)
</span>
<span v-else>普通赔付</span>
<strong :class="{ 'is-deduction': form.compensation_plan === 'insurance' }">
{{
form.compensation_plan === 'insurance'
? `-¥${formatMoney(calculatedInsuranceFee)}`
: '¥0.0'
}}
</strong>
</div>
<div class="take-home-net">
<span>预计到手价格</span>
<strong>¥{{ formatMoney(calculatedTakeHomePrice) }}</strong>
</div>
</div>
<p class="coverage-note">
普通赔付覆盖封禁、追回、洗号等情况;无忧租号保险在此基础上增加账号市场价值保障。
</p>
@@ -786,13 +814,17 @@ function selectDailyLoss(value: string | number) {
<aside class="summary-column">
<div class="summary-panel">
<div class="summary-main">
<span>卖家发布价格</span>
<span>预计到手价格</span>
<strong>{{
calculatedSellerPrice ? `¥${formatMoney(calculatedSellerPrice)}` : '--'
calculatedTakeHomePrice ? `¥${formatMoney(calculatedTakeHomePrice)}` : '--'
}}</strong>
</div>
<div class="summary-breakdown">
<div class="summary-breakdown-title">价格明细</div>
<div>
<span>卖家发布价格</span>
<strong>¥{{ formatMoney(calculatedSellerPrice) }}</strong>
</div>
<div>
<span>纯币价格</span>
<strong>{{
@@ -803,6 +835,10 @@ function selectDailyLoss(value: string | number) {
<span>额外物品价格</span>
<strong>¥{{ formatMoney(calculatedConsumablePrice) }}</strong>
</div>
<div v-if="form.compensation_plan === 'insurance'">
<span>无忧租号保险({{ insuranceFeePercent.toFixed(1) }}%)</span>
<strong class="is-deduction">-¥{{ formatMoney(calculatedInsuranceFee) }}</strong>
</div>
<div>
<span>押金价格</span>
<strong>{{
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest'
import { calculateInsuranceFeeAmount } from '../pricing'
describe('calculateInsuranceFeeAmount', () => {
it('普通赔付不收取保险服务费', () => {
expect(calculateInsuranceFeeAmount(238.1, 0)).toBe(0)
})
it('按普通保险费率计算并四舍五入到分', () => {
expect(calculateInsuranceFeeAmount(238.1, 800)).toBe(19.05)
})
it('按极品专区专享费率计算', () => {
expect(calculateInsuranceFeeAmount(238.1, 400)).toBe(9.52)
})
it('保险服务费不超过卖家租金', () => {
expect(calculateInsuranceFeeAmount(1, 10_000)).toBe(1)
})
})
+7
View File
@@ -26,6 +26,13 @@ export function roundMoney(value: number) {
return Math.round(value * 10) / 10
}
export function calculateInsuranceFeeAmount(ownerIncomeYuan: number, feeBps: number) {
const ownerIncomeCent = Math.max(Math.round(Number(ownerIncomeYuan || 0) * 100), 0)
const safeFeeBps = Math.max(Math.round(Number(feeBps || 0)), 0)
if (ownerIncomeCent <= 0 || safeFeeBps <= 0) return 0
return Math.min(Math.round((ownerIncomeCent * safeFeeBps) / 10_000), ownerIncomeCent) / 100
}
/**
* 将比例四舍五入到一位小数
*/