Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6355ca21b0 | ||
|
|
12e9b6d0b2 |
@@ -288,7 +288,7 @@ func awardOrderSide(tx *gorm.DB, order *model.RentalOrder, cfg Config, role stri
|
||||
setOrderPointsSnapshot(order, role, existing.BasisAmountCent, existing.PointsPerYuan, existing.PointsMultiplierBps, existing.Points, existing.CreatedAt)
|
||||
return nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
if role == "renter" {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
* 将比例四舍五入到一位小数
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user