refactor(listing): 卖家发布定价改用整数分与基点
This commit is contained in:
@@ -85,6 +85,8 @@ function statusTag(meta: StatusMeta) {
|
||||
|
||||
function sellerPriceCent(row: SellerListing) {
|
||||
const breakdown = recordValue(row.asset_summary?.price_breakdown)
|
||||
const cent = numberValue(breakdown.seller_total_price_cent)
|
||||
if (cent > 0) return Math.round(cent)
|
||||
const price = numberValue(breakdown.seller_total_price)
|
||||
return price > 0 ? Math.round(price * 100) : row.price_cent
|
||||
}
|
||||
@@ -103,11 +105,20 @@ function onlineTimeText(summary: Record<string, unknown>) {
|
||||
return `${start || '—'} 至 ${end || '—'}`
|
||||
}
|
||||
|
||||
function ratioText(value: unknown) {
|
||||
const ratio = Number(value)
|
||||
function ratioText(bpsValue: unknown, legacyRatio: unknown) {
|
||||
const bps = Number(bpsValue)
|
||||
if (Number.isFinite(bps) && bps > 0) return `1:${(bps / 10000).toFixed(1)}`
|
||||
const ratio = Number(legacyRatio)
|
||||
return Number.isFinite(ratio) && ratio > 0 ? `1:${ratio.toFixed(1)}` : '—'
|
||||
}
|
||||
|
||||
function moneyCent(centValue: unknown, legacyYuanValue: unknown) {
|
||||
const cent = Number(centValue)
|
||||
if (Number.isFinite(cent) && cent > 0) return Math.round(cent)
|
||||
const yuan = Number(legacyYuanValue)
|
||||
return Number.isFinite(yuan) ? Math.round(yuan * 100) : 0
|
||||
}
|
||||
|
||||
function screenshotsOf(row: SellerListing) {
|
||||
const summary = row.asset_summary ?? {}
|
||||
const groups = recordValue(summary.screenshot_groups)
|
||||
@@ -423,27 +434,42 @@ export function SellerListingDetailPage() {
|
||||
<div className="seller-price-grid">
|
||||
<div>
|
||||
<span>参考比例</span>
|
||||
<strong>{ratioText(breakdown.seller_reference_ratio)}</strong>
|
||||
<strong>
|
||||
{ratioText(breakdown.seller_reference_ratio_bps, breakdown.seller_reference_ratio)}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>卖家比例</span>
|
||||
<strong>{ratioText(breakdown.seller_ratio)}</strong>
|
||||
<strong>{ratioText(breakdown.seller_ratio_bps, breakdown.seller_ratio)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>纯币基础价</span>
|
||||
<strong>{formatMoney(numberValue(breakdown.seller_coin_base_price) * 100)}</strong>
|
||||
<strong>
|
||||
{formatMoney(
|
||||
moneyCent(breakdown.seller_coin_base_price_cent, breakdown.seller_coin_base_price)
|
||||
)}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>消耗品价值</span>
|
||||
<strong>{formatMoney(numberValue(breakdown.consumable_price) * 100)}</strong>
|
||||
<strong>
|
||||
{formatMoney(moneyCent(breakdown.consumable_price_cent, breakdown.consumable_price))}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>买家比例</span>
|
||||
<strong>{ratioText(breakdown.buyer_ratio)}</strong>
|
||||
<strong>{ratioText(breakdown.buyer_ratio_bps, breakdown.buyer_ratio)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>平台加价</span>
|
||||
<strong>{formatMoney(numberValue(breakdown.platform_markup_amount) * 100)}</strong>
|
||||
<strong>
|
||||
{formatMoney(
|
||||
moneyCent(
|
||||
breakdown.platform_markup_amount_cent,
|
||||
breakdown.platform_markup_amount
|
||||
)
|
||||
)}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -36,18 +36,23 @@ import { uploadFile } from '@/shared/api/files'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import {
|
||||
dailyLossOptions,
|
||||
calculateConsumablePrice,
|
||||
calculateDailyLossRatioAdjustment,
|
||||
calculatePlatformPricing,
|
||||
calculateRecommendedDeposit,
|
||||
calculateSellerReferenceRatio,
|
||||
buildDepositBreakdownItems,
|
||||
formatNumber,
|
||||
calculateConsumablePriceCent,
|
||||
calculateDailyLossRatioAdjustmentBPS,
|
||||
calculatePlatformPricingCent,
|
||||
calculateRecommendedDepositCent,
|
||||
calculateSellerCoinBasePriceCent,
|
||||
calculateSellerReferenceRatioBPS,
|
||||
bpsToRatio,
|
||||
formatRatioBPS,
|
||||
hafCoinUnitsToM,
|
||||
hasAcceleratedSaleRatioInput,
|
||||
isQuantityItemDisabledForInsurance,
|
||||
readFinalSaleRatio,
|
||||
roundMoney,
|
||||
roundRatio,
|
||||
maxAcceleratedSaleRatioBPS,
|
||||
parseHafCoinMToUnits,
|
||||
parseYuanToCent,
|
||||
ratioToBPS,
|
||||
readFinalSaleRatioBPS,
|
||||
} from '@/shared/utils/pricing'
|
||||
import { formatMoney } from '@/shared/utils/format'
|
||||
import { confirmModal } from '@/shared/utils/confirm'
|
||||
@@ -69,7 +74,6 @@ import {
|
||||
} from './options'
|
||||
import { defaultPublishForm, type PublishForm } from './types'
|
||||
|
||||
const COIN_UNIT = 1_000_000
|
||||
const MAX_SCREENSHOTS = 9
|
||||
const SCAN_ONLY_MARK = '微信'
|
||||
const SCAN_LOGIN_METHOD = '扫码'
|
||||
@@ -108,6 +112,16 @@ function numberOrDefault(value: unknown, fallback: number) {
|
||||
return Number.isFinite(parsed) ? parsed : fallback
|
||||
}
|
||||
|
||||
function ratioInputFromBreakdown(
|
||||
breakdown: Record<string, unknown>,
|
||||
bpsKey: string,
|
||||
ratioKey: string
|
||||
): number | '' {
|
||||
const bps = Number(breakdown[bpsKey])
|
||||
if (Number.isFinite(bps) && bps > 0) return bps / 10000
|
||||
return numberOrEmpty(breakdown[ratioKey])
|
||||
}
|
||||
|
||||
function toTimeValue(value: string): Dayjs | null {
|
||||
const match = /^(\d{1,2}):(\d{2})/.exec(value.trim())
|
||||
if (!match) return null
|
||||
@@ -267,12 +281,16 @@ export function SellerListingFormPage() {
|
||||
setForm({
|
||||
server_region: listing.server_region || '',
|
||||
face_owner: stringValue(summary.face_owner),
|
||||
haf_coin_amount: listing.haf_coin_amount > 0 ? listing.haf_coin_amount / COIN_UNIT : '',
|
||||
haf_coin_amount: listing.haf_coin_amount > 0 ? hafCoinUnitsToM(listing.haf_coin_amount) : '',
|
||||
rank_level: listing.rank_level || '',
|
||||
secret_kd: stringValue(summary.secret_kd),
|
||||
fire_level: numberOrEmpty(summary.fire_level),
|
||||
daily_loss_m: numberOrDefault(summary.daily_loss_m, 10),
|
||||
accelerated_sale_ratio: numberOrEmpty(breakdown.accelerated_sale_ratio),
|
||||
accelerated_sale_ratio: ratioInputFromBreakdown(
|
||||
breakdown,
|
||||
'accelerated_sale_ratio_bps',
|
||||
'accelerated_sale_ratio'
|
||||
),
|
||||
season_insurance: stringValue(summary.season_insurance),
|
||||
stamina_level: stringValue(summary.stamina_level),
|
||||
load_level: stringValue(summary.load_level),
|
||||
@@ -350,44 +368,43 @@ export function SellerListingFormPage() {
|
||||
.filter(Boolean).length
|
||||
|
||||
const pricing = useMemo(() => {
|
||||
const coinMAmount = Number(form.haf_coin_amount || 0)
|
||||
const coinWanAmount = coinMAmount * 100
|
||||
const dailyLossMAmount = Number(form.daily_loss_m || 10)
|
||||
const dailyLossRatioAdjustment = calculateDailyLossRatioAdjustment(dailyLossMAmount)
|
||||
const referenceRatio = calculateSellerReferenceRatio({
|
||||
coinMAmount,
|
||||
const hafCoinAmount = parseHafCoinMToUnits(form.haf_coin_amount === '' ? 0 : form.haf_coin_amount)
|
||||
const coinMAmount = hafCoinUnitsToM(hafCoinAmount)
|
||||
const dailyLossMAmount = Math.trunc(Number(form.daily_loss_m || 10))
|
||||
const dailyLossRatioAdjustmentBPS = calculateDailyLossRatioAdjustmentBPS(dailyLossMAmount)
|
||||
const referenceRatioBPS = calculateSellerReferenceRatioBPS({
|
||||
hafCoinAmount,
|
||||
form,
|
||||
ratioConfig: options.ratio_config,
|
||||
skinGroups: options.skin_groups,
|
||||
selectedSkins,
|
||||
levelOptions: options.level_options,
|
||||
dailyLossRatioAdjustment,
|
||||
dailyLossRatioAdjustmentBPS,
|
||||
})
|
||||
const maxAcceleratedSaleRatio = referenceRatio > 0 ? roundRatio(referenceRatio + 10) : 0
|
||||
const finalRatio = readFinalSaleRatio(
|
||||
referenceRatio,
|
||||
const maxRatioBPS = maxAcceleratedSaleRatioBPS(referenceRatioBPS)
|
||||
const finalRatioBPS = readFinalSaleRatioBPS(
|
||||
referenceRatioBPS,
|
||||
form.accelerated_sale_ratio,
|
||||
maxAcceleratedSaleRatio
|
||||
maxRatioBPS
|
||||
)
|
||||
const coinBasePrice = finalRatio > 0 ? roundMoney(coinWanAmount / finalRatio) : 0
|
||||
const consumablePrice = calculateConsumablePrice({
|
||||
const coinBasePriceCent =
|
||||
finalRatioBPS > 0 ? calculateSellerCoinBasePriceCent(hafCoinAmount, finalRatioBPS) : 0
|
||||
const consumablePriceCent = calculateConsumablePriceCent({
|
||||
quantityItems: options.quantity_items,
|
||||
quantityValues,
|
||||
quantityModes,
|
||||
seasonInsurance: form.season_insurance,
|
||||
})
|
||||
const sellerPrice =
|
||||
finalRatio > 0 ? roundMoney(coinBasePrice + consumablePrice) : 0
|
||||
const platformPricing = calculatePlatformPricing({
|
||||
coinMAmount,
|
||||
coinWanAmount,
|
||||
sellerRatio: finalRatio,
|
||||
sellerCoinBasePrice: coinBasePrice,
|
||||
sellerTotalPrice: sellerPrice,
|
||||
consumablePrice,
|
||||
const sellerPriceCent = finalRatioBPS > 0 ? coinBasePriceCent + consumablePriceCent : 0
|
||||
const platformPricing = calculatePlatformPricingCent({
|
||||
hafCoinAmount,
|
||||
sellerRatioBPS: finalRatioBPS,
|
||||
sellerCoinBasePriceCent: coinBasePriceCent,
|
||||
sellerTotalPriceCent: sellerPriceCent,
|
||||
consumablePriceCent,
|
||||
salePriceConfig,
|
||||
})
|
||||
const recommendedDeposit = calculateRecommendedDeposit({
|
||||
const recommendedDepositCent = calculateRecommendedDepositCent({
|
||||
depositRecommendConfig: options.deposit_recommend_config,
|
||||
skinGroups: options.skin_groups,
|
||||
selectedSkins,
|
||||
@@ -398,29 +415,27 @@ export function SellerListingFormPage() {
|
||||
selectedSkins,
|
||||
})
|
||||
return {
|
||||
hafCoinAmount,
|
||||
coinMAmount,
|
||||
coinWanAmount,
|
||||
dailyLossMAmount,
|
||||
dailyLossRatioAdjustment,
|
||||
referenceRatio,
|
||||
maxAcceleratedSaleRatio,
|
||||
finalRatio,
|
||||
coinBasePrice,
|
||||
consumablePrice,
|
||||
sellerPrice,
|
||||
dailyLossRatioAdjustmentBPS,
|
||||
referenceRatioBPS,
|
||||
maxAcceleratedSaleRatioBPS: maxRatioBPS,
|
||||
finalRatioBPS,
|
||||
coinBasePriceCent,
|
||||
consumablePriceCent,
|
||||
sellerPriceCent,
|
||||
platformPricing,
|
||||
finalPrice: platformPricing.buyerTotalPrice,
|
||||
recommendedDeposit,
|
||||
finalPriceCent: platformPricing.buyerTotalPriceCent,
|
||||
recommendedDepositCent,
|
||||
depositBreakdown,
|
||||
}
|
||||
}, [form, options, salePriceConfig, quantityValues, quantityModes, selectedSkins])
|
||||
|
||||
const depositAmount = Number(form.depositAmountYuan)
|
||||
const depositAmountCent = parseYuanToCent(form.depositAmountYuan === '' ? '' : form.depositAmountYuan)
|
||||
const depositBelowRecommended =
|
||||
pricing.recommendedDeposit > 0 &&
|
||||
(form.depositAmountYuan === '' ||
|
||||
!Number.isFinite(depositAmount) ||
|
||||
depositAmount < pricing.recommendedDeposit)
|
||||
pricing.recommendedDepositCent > 0 &&
|
||||
(!Number.isFinite(depositAmountCent) || depositAmountCent < pricing.recommendedDepositCent)
|
||||
|
||||
function isQuantityDisabled(item: PublishQuantityItem) {
|
||||
return isQuantityItemDisabledForInsurance(item, form.season_insurance)
|
||||
@@ -535,27 +550,38 @@ export function SellerListingFormPage() {
|
||||
}
|
||||
|
||||
function buildAssetSummary(): Record<string, unknown> {
|
||||
const acceleratedRatioBPS = hasAcceleratedSaleRatioInput(form.accelerated_sale_ratio)
|
||||
? ratioToBPS(form.accelerated_sale_ratio as number)
|
||||
: pricing.referenceRatioBPS
|
||||
const platformPricing = pricing.platformPricing
|
||||
return {
|
||||
face_owner: form.face_owner,
|
||||
secret_kd: form.secret_kd,
|
||||
fire_level: Number(form.fire_level),
|
||||
daily_loss_m: pricing.dailyLossMAmount,
|
||||
publish_ratio: pricing.platformPricing.buyerRatio,
|
||||
publish_ratio: bpsToRatio(platformPricing.buyerRatioBPS),
|
||||
price_breakdown: {
|
||||
seller_reference_ratio: pricing.referenceRatio,
|
||||
seller_ratio: pricing.finalRatio,
|
||||
seller_coin_base_price: pricing.coinBasePrice,
|
||||
seller_total_price: pricing.sellerPrice,
|
||||
consumable_price: pricing.consumablePrice,
|
||||
daily_loss_ratio_adjustment: pricing.dailyLossRatioAdjustment,
|
||||
accelerated_sale_ratio: hasAcceleratedSaleRatioInput(form.accelerated_sale_ratio)
|
||||
? Number(form.accelerated_sale_ratio)
|
||||
: pricing.referenceRatio,
|
||||
buyer_coin_base_price: pricing.platformPricing.buyerCoinBasePrice,
|
||||
buyer_total_price: pricing.finalPrice,
|
||||
buyer_ratio: pricing.platformPricing.buyerRatio,
|
||||
platform_markup_amount: pricing.platformPricing.platformMarkupAmount,
|
||||
platform_rule_type: pricing.platformPricing.ruleType,
|
||||
seller_reference_ratio_bps: pricing.referenceRatioBPS,
|
||||
seller_ratio_bps: pricing.finalRatioBPS,
|
||||
buyer_ratio_bps: platformPricing.buyerRatioBPS,
|
||||
seller_coin_base_price_cent: pricing.coinBasePriceCent,
|
||||
seller_total_price_cent: pricing.sellerPriceCent,
|
||||
consumable_price_cent: pricing.consumablePriceCent,
|
||||
buyer_coin_base_price_cent: platformPricing.buyerCoinBasePriceCent,
|
||||
buyer_total_price_cent: pricing.finalPriceCent,
|
||||
platform_markup_amount_cent: platformPricing.platformMarkupAmountCent,
|
||||
seller_reference_ratio: bpsToRatio(pricing.referenceRatioBPS),
|
||||
seller_ratio: bpsToRatio(pricing.finalRatioBPS),
|
||||
seller_coin_base_price: pricing.coinBasePriceCent / 100,
|
||||
seller_total_price: pricing.sellerPriceCent / 100,
|
||||
consumable_price: pricing.consumablePriceCent / 100,
|
||||
daily_loss_ratio_adjustment: bpsToRatio(pricing.dailyLossRatioAdjustmentBPS),
|
||||
accelerated_sale_ratio: bpsToRatio(acceleratedRatioBPS),
|
||||
buyer_coin_base_price: platformPricing.buyerCoinBasePriceCent / 100,
|
||||
buyer_total_price: pricing.finalPriceCent / 100,
|
||||
buyer_ratio: bpsToRatio(platformPricing.buyerRatioBPS),
|
||||
platform_markup_amount: platformPricing.platformMarkupAmountCent / 100,
|
||||
platform_rule_type: platformPricing.ruleType,
|
||||
},
|
||||
season_insurance: form.season_insurance,
|
||||
stamina_level: form.stamina_level,
|
||||
@@ -565,6 +591,7 @@ export function SellerListingFormPage() {
|
||||
key: item.key,
|
||||
label: item.label,
|
||||
price: item.price,
|
||||
price_cent: item.price_cent,
|
||||
quantity: isQuantityDisabled(item) ? 0 : Number(quantityValues[item.key] || 0),
|
||||
mode: isQuantityDisabled(item) ? '赠送' : quantityModes[item.key] || '收费',
|
||||
}))
|
||||
@@ -618,27 +645,36 @@ export function SellerListingFormPage() {
|
||||
if (options.next_season_pass_enabled && !form.next_season_pass) {
|
||||
return '请选择下赛季是否购买通行证'
|
||||
}
|
||||
if (form.depositAmountYuan === '' || Number(form.depositAmountYuan) < 0) {
|
||||
if (form.depositAmountYuan === '' || !Number.isFinite(depositAmountCent)) {
|
||||
return '请填写押金'
|
||||
}
|
||||
if (!Number.isFinite(Number(form.depositAmountYuan))) return '押金格式不正确'
|
||||
if (pricing.recommendedDeposit > 0 && Number(form.depositAmountYuan) < pricing.recommendedDeposit) {
|
||||
return `押金不能低于智能推荐 ¥${pricing.recommendedDeposit}`
|
||||
if (depositAmountCent < 0) return '押金格式不正确'
|
||||
if (
|
||||
pricing.recommendedDepositCent > 0 &&
|
||||
depositAmountCent < pricing.recommendedDepositCent
|
||||
) {
|
||||
return `押金不能低于智能推荐 ${formatMoney(pricing.recommendedDepositCent)}`
|
||||
}
|
||||
if (pricing.consumablePrice > 0 && Number(form.depositAmountYuan) <= pricing.consumablePrice) {
|
||||
return `押金必须大于额外消耗品总价值 ¥${pricing.consumablePrice}`
|
||||
if (
|
||||
pricing.consumablePriceCent > 0 &&
|
||||
depositAmountCent <= pricing.consumablePriceCent
|
||||
) {
|
||||
return `押金必须大于额外消耗品总价值 ${formatMoney(pricing.consumablePriceCent)}`
|
||||
}
|
||||
if (pricing.finalPrice <= 0) {
|
||||
if (pricing.finalPriceCent <= 0) {
|
||||
return '暂无法计算买家价,请确认后台发布选项与价格规则已配置'
|
||||
}
|
||||
if (hasAcceleratedSaleRatioInput(form.accelerated_sale_ratio)) {
|
||||
const ratio = Number(form.accelerated_sale_ratio)
|
||||
if (!Number.isFinite(ratio) || ratio <= 0) return '加速出售比例格式不正确'
|
||||
if (pricing.referenceRatio > 0 && ratio < pricing.referenceRatio) {
|
||||
return `加速出售比例不能低于参考比例 1:${formatNumber(pricing.referenceRatio)}`
|
||||
const ratioBPS = ratioToBPS(form.accelerated_sale_ratio as number)
|
||||
if (ratioBPS <= 0) return '加速出售比例格式不正确'
|
||||
if (pricing.referenceRatioBPS > 0 && ratioBPS < pricing.referenceRatioBPS) {
|
||||
return `加速出售比例不能低于参考比例 1:${formatRatioBPS(pricing.referenceRatioBPS)}`
|
||||
}
|
||||
if (pricing.maxAcceleratedSaleRatio > 0 && ratio > pricing.maxAcceleratedSaleRatio) {
|
||||
return `加速出售比例不能超过 1:${formatNumber(pricing.maxAcceleratedSaleRatio)}`
|
||||
if (
|
||||
pricing.maxAcceleratedSaleRatioBPS > 0 &&
|
||||
ratioBPS > pricing.maxAcceleratedSaleRatioBPS
|
||||
) {
|
||||
return `加速出售比例不能超过 1:${formatRatioBPS(pricing.maxAcceleratedSaleRatioBPS)}`
|
||||
}
|
||||
}
|
||||
if (screenshotUrls.length === 0) return '请至少上传 1 张账号截图'
|
||||
@@ -674,11 +710,11 @@ export function SellerListingFormPage() {
|
||||
server_region: form.server_region,
|
||||
login_platform: form.login_method,
|
||||
rank_level: form.rank_level,
|
||||
haf_coin_amount: Math.round(pricing.coinMAmount * COIN_UNIT),
|
||||
haf_coin_amount: pricing.hafCoinAmount,
|
||||
asset_summary: buildAssetSummary(),
|
||||
screenshot_urls: screenshotUrls,
|
||||
price_cent: Math.round(pricing.finalPrice * 100),
|
||||
deposit_amount_cent: Math.round(Number(form.depositAmountYuan) * 100),
|
||||
price_cent: pricing.finalPriceCent,
|
||||
deposit_amount_cent: depositAmountCent,
|
||||
agreed_virtual_asset_sale: virtualAgreement,
|
||||
agreed_seller_agreement: sellerAgreement,
|
||||
}
|
||||
@@ -1211,10 +1247,12 @@ export function SellerListingFormPage() {
|
||||
className="seller-number-input"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => updateForm('depositAmountYuan', pricing.recommendedDeposit)}
|
||||
disabled={pricing.recommendedDeposit <= 0}
|
||||
onClick={() =>
|
||||
updateForm('depositAmountYuan', pricing.recommendedDepositCent / 100)
|
||||
}
|
||||
disabled={pricing.recommendedDepositCent <= 0}
|
||||
>
|
||||
使用推荐 ¥{pricing.recommendedDeposit}
|
||||
使用推荐 {formatMoney(pricing.recommendedDepositCent)}
|
||||
</Button>
|
||||
</div>
|
||||
<small>{options.price_config.deposit_hint}</small>
|
||||
@@ -1222,7 +1260,7 @@ export function SellerListingFormPage() {
|
||||
{pricing.depositBreakdown.map(item => (
|
||||
<Tag key={`${item.label}-${item.count}`} bordered={false}>
|
||||
{item.label}
|
||||
{item.count > 1 ? ` x${item.count}` : ''} ¥{item.amount}
|
||||
{item.count > 1 ? ` x${item.count}` : ''} {formatMoney(item.amountCent)}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
@@ -1230,7 +1268,7 @@ export function SellerListingFormPage() {
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message={`押金不能低于智能推荐 ¥${pricing.recommendedDeposit}`}
|
||||
message={`押金不能低于智能推荐 ${formatMoney(pricing.recommendedDepositCent)}`}
|
||||
/>
|
||||
) : null}
|
||||
</label>
|
||||
@@ -1245,31 +1283,34 @@ export function SellerListingFormPage() {
|
||||
form.accelerated_sale_ratio === '' ? null : form.accelerated_sale_ratio
|
||||
}
|
||||
placeholder={
|
||||
pricing.referenceRatio > 0
|
||||
? `默认 ${formatNumber(pricing.referenceRatio)},最高 ${formatNumber(pricing.maxAcceleratedSaleRatio)}`
|
||||
pricing.referenceRatioBPS > 0
|
||||
? `默认 ${formatRatioBPS(pricing.referenceRatioBPS)},最高 ${formatRatioBPS(pricing.maxAcceleratedSaleRatioBPS)}`
|
||||
: '完善资料后自动生成可设置范围'
|
||||
}
|
||||
onChange={value => updateForm('accelerated_sale_ratio', value ?? '')}
|
||||
className="seller-number-input"
|
||||
/>
|
||||
<Button
|
||||
disabled={pricing.referenceRatio <= 0}
|
||||
disabled={pricing.referenceRatioBPS <= 0}
|
||||
onClick={() => updateForm('accelerated_sale_ratio', '')}
|
||||
>
|
||||
参考比例
|
||||
</Button>
|
||||
<Button
|
||||
disabled={pricing.maxAcceleratedSaleRatio <= 0}
|
||||
disabled={pricing.maxAcceleratedSaleRatioBPS <= 0}
|
||||
onClick={() =>
|
||||
updateForm('accelerated_sale_ratio', pricing.maxAcceleratedSaleRatio)
|
||||
updateForm(
|
||||
'accelerated_sale_ratio',
|
||||
pricing.maxAcceleratedSaleRatioBPS / 10000
|
||||
)
|
||||
}
|
||||
>
|
||||
最高
|
||||
</Button>
|
||||
</div>
|
||||
<small>
|
||||
{pricing.referenceRatio > 0
|
||||
? `可设置 1:${formatNumber(pricing.referenceRatio)} ~ 1:${formatNumber(pricing.maxAcceleratedSaleRatio)},比例越高出租越快。`
|
||||
{pricing.referenceRatioBPS > 0
|
||||
? `可设置 1:${formatRatioBPS(pricing.referenceRatioBPS)} ~ 1:${formatRatioBPS(pricing.maxAcceleratedSaleRatioBPS)},比例越高出租越快。`
|
||||
: options.price_config.ratio_description || '请先完善赛季保险、体力和负重。'}
|
||||
</small>
|
||||
</label>
|
||||
@@ -1334,7 +1375,7 @@ export function SellerListingFormPage() {
|
||||
<Card className="seller-price-panel">
|
||||
<Typography.Text className="seller-eyebrow">BUYER PRICE · 买家挂牌价</Typography.Text>
|
||||
<div className="seller-price-panel__main">
|
||||
<strong>{formatMoney(Math.round(pricing.finalPrice * 100))}</strong>
|
||||
<strong>{formatMoney(pricing.finalPriceCent)}</strong>
|
||||
<span>
|
||||
{pricing.platformPricing.ruleType === 'fixed_markup'
|
||||
? '固定加价'
|
||||
@@ -1347,40 +1388,40 @@ export function SellerListingFormPage() {
|
||||
<div>
|
||||
<span>参考比例</span>
|
||||
<strong>
|
||||
{pricing.referenceRatio > 0
|
||||
? `1:${formatNumber(pricing.referenceRatio)}`
|
||||
{pricing.referenceRatioBPS > 0
|
||||
? `1:${formatRatioBPS(pricing.referenceRatioBPS)}`
|
||||
: '--'}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>卖家比例</span>
|
||||
<strong>
|
||||
{pricing.finalRatio > 0 ? `1:${formatNumber(pricing.finalRatio)}` : '--'}
|
||||
{pricing.finalRatioBPS > 0 ? `1:${formatRatioBPS(pricing.finalRatioBPS)}` : '--'}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>买家比例</span>
|
||||
<strong>
|
||||
{pricing.platformPricing.buyerRatio > 0
|
||||
? `1:${formatNumber(pricing.platformPricing.buyerRatio)}`
|
||||
{pricing.platformPricing.buyerRatioBPS > 0
|
||||
? `1:${formatRatioBPS(pricing.platformPricing.buyerRatioBPS)}`
|
||||
: '--'}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>纯币基础价</span>
|
||||
<strong>{formatMoney(Math.round(pricing.coinBasePrice * 100))}</strong>
|
||||
<strong>{formatMoney(pricing.coinBasePriceCent)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>消耗品价值</span>
|
||||
<strong>{formatMoney(Math.round(pricing.consumablePrice * 100))}</strong>
|
||||
<strong>{formatMoney(pricing.consumablePriceCent)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>卖家到手价</span>
|
||||
<strong>{formatMoney(Math.round(pricing.sellerPrice * 100))}</strong>
|
||||
<strong>{formatMoney(pricing.sellerPriceCent)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>推荐押金</span>
|
||||
<strong>{formatMoney(Math.round(pricing.recommendedDeposit * 100))}</strong>
|
||||
<strong>{formatMoney(pricing.recommendedDepositCent)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
|
||||
@@ -73,7 +73,10 @@ function sellerReferencePrice(row: SellerListing) {
|
||||
const summary = row.asset_summary ?? {}
|
||||
const breakdown = summary.price_breakdown
|
||||
if (breakdown && typeof breakdown === 'object') {
|
||||
const value = Number((breakdown as Record<string, unknown>).seller_total_price)
|
||||
const record = breakdown as Record<string, unknown>
|
||||
const cent = Number(record.seller_total_price_cent)
|
||||
if (Number.isFinite(cent) && cent > 0) return Math.round(cent)
|
||||
const value = Number(record.seller_total_price)
|
||||
if (Number.isFinite(value) && value > 0) return Math.round(value * 100)
|
||||
}
|
||||
return row.price_cent
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface PublishQuantityItem {
|
||||
key: QuantityKey
|
||||
label: string
|
||||
price: string
|
||||
/** 规范化单价(分);大于 0 时优先于 price 字符串。 */
|
||||
price_cent?: number
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
@@ -40,16 +42,22 @@ export interface PublishDepositSkinGroupRule {
|
||||
group_key: string
|
||||
label: string
|
||||
amount_per_item: number
|
||||
/** 规范化单件押金(分);大于 0 时优先于 amount_per_item。 */
|
||||
amount_per_item_cent?: number
|
||||
}
|
||||
|
||||
export interface PublishDepositRecommendConfig {
|
||||
base_amount: number
|
||||
/** 规范化基础押金(分);大于 0 时优先于 base_amount。 */
|
||||
base_amount_cent?: number
|
||||
skin_group_rules: PublishDepositSkinGroupRule[]
|
||||
}
|
||||
|
||||
export interface PublishInsuranceBaseRatio {
|
||||
insurance: string
|
||||
ratio: number
|
||||
/** 规范化比例(基点);大于 0 时优先于 ratio。 */
|
||||
ratio_bps?: number
|
||||
}
|
||||
|
||||
export interface PublishRatioConfigItem {
|
||||
@@ -58,23 +66,31 @@ export interface PublishRatioConfigItem {
|
||||
kind: string
|
||||
group_key?: string
|
||||
missing_penalty: number
|
||||
/** 规范化缺失扣减(基点);大于 0 时优先于 missing_penalty。 */
|
||||
missing_penalty_bps?: number
|
||||
}
|
||||
|
||||
export interface PublishCoinCorrection {
|
||||
threshold_m: number
|
||||
correction: number
|
||||
/** 规范化哈夫币修正(基点);大于 0 时优先于 correction。 */
|
||||
correction_bps?: number
|
||||
}
|
||||
|
||||
export interface PublishSaleFixedMarkupRule {
|
||||
min_m: number
|
||||
max_m: number
|
||||
markup_amount: number
|
||||
/** 规范化固定加价(分);大于 0 时优先于 markup_amount。 */
|
||||
markup_amount_cent?: number
|
||||
}
|
||||
|
||||
export interface PublishSaleRatioAdjustmentRule {
|
||||
min_m: number
|
||||
max_m: number
|
||||
ratio_subtract: number
|
||||
/** 规范化比例修正(基点);大于 0 时优先于 ratio_subtract。 */
|
||||
ratio_subtract_bps?: number
|
||||
}
|
||||
|
||||
export interface PublishRatioConfig {
|
||||
@@ -289,6 +305,7 @@ function normalizeQuantityItems(values?: unknown[]): PublishQuantityItem[] {
|
||||
key: typeof row.key === 'string' ? row.key.trim() : '',
|
||||
label: typeof row.label === 'string' ? row.label.trim() : '',
|
||||
price: typeof row.price === 'string' ? row.price.trim() : '',
|
||||
price_cent: readOptionalPositiveInteger(row.price_cent),
|
||||
placeholder: typeof row.placeholder === 'string' ? row.placeholder.trim() : undefined,
|
||||
}
|
||||
})
|
||||
@@ -330,11 +347,12 @@ function normalizeDepositRecommendConfig(value?: unknown): PublishDepositRecomme
|
||||
const row = isRecord(value) ? value : {}
|
||||
const config = {
|
||||
base_amount: readNumber(row.base_amount),
|
||||
base_amount_cent: readOptionalPositiveInteger(row.base_amount_cent),
|
||||
skin_group_rules: normalizeDepositSkinGroupRules(
|
||||
Array.isArray(row.skin_group_rules) ? row.skin_group_rules : []
|
||||
),
|
||||
}
|
||||
if (config.base_amount <= 0) {
|
||||
if (config.base_amount <= 0 && !config.base_amount_cent) {
|
||||
config.base_amount = emptyListingPublishOptions.deposit_recommend_config.base_amount
|
||||
}
|
||||
if (config.skin_group_rules.length === 0) {
|
||||
@@ -353,6 +371,7 @@ function normalizeDepositSkinGroupRules(values: unknown[]): PublishDepositSkinGr
|
||||
group_key: typeof row.group_key === 'string' ? row.group_key.trim() : '',
|
||||
label: typeof row.label === 'string' ? row.label.trim() : '',
|
||||
amount_per_item: readNumber(row.amount_per_item),
|
||||
amount_per_item_cent: readOptionalPositiveInteger(row.amount_per_item_cent),
|
||||
}
|
||||
})
|
||||
.filter(item => item.group_key && item.label && item.amount_per_item >= 0)
|
||||
@@ -401,9 +420,10 @@ function normalizeInsuranceBaseRatios(values: unknown[]): PublishInsuranceBaseRa
|
||||
return {
|
||||
insurance: typeof row.insurance === 'string' ? row.insurance.trim() : '',
|
||||
ratio: readNumber(row.ratio),
|
||||
ratio_bps: readOptionalPositiveInteger(row.ratio_bps),
|
||||
}
|
||||
})
|
||||
.filter(item => item.insurance && item.ratio > 0)
|
||||
.filter(item => item.insurance && (item.ratio > 0 || Boolean(item.ratio_bps)))
|
||||
}
|
||||
|
||||
function normalizeRatioConfigItems(values: unknown[]): PublishRatioConfigItem[] {
|
||||
@@ -416,6 +436,7 @@ function normalizeRatioConfigItems(values: unknown[]): PublishRatioConfigItem[]
|
||||
kind: typeof row.kind === 'string' ? row.kind.trim() : '',
|
||||
group_key: typeof row.group_key === 'string' ? row.group_key.trim() : '',
|
||||
missing_penalty: readNumber(row.missing_penalty),
|
||||
missing_penalty_bps: readOptionalPositiveInteger(row.missing_penalty_bps),
|
||||
}
|
||||
})
|
||||
.filter(item => item.key && item.label && item.kind)
|
||||
@@ -428,9 +449,12 @@ function normalizeCoinCorrections(values: unknown[]): PublishCoinCorrection[] {
|
||||
return {
|
||||
threshold_m: readNumber(row.threshold_m),
|
||||
correction: readNumber(row.correction),
|
||||
correction_bps: readOptionalPositiveInteger(row.correction_bps),
|
||||
}
|
||||
})
|
||||
.filter(item => item.threshold_m >= 0 && item.correction > 0)
|
||||
.filter(
|
||||
item => item.threshold_m >= 0 && (item.correction > 0 || Boolean(item.correction_bps))
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeSaleFixedMarkupRules(values: unknown[]): PublishSaleFixedMarkupRule[] {
|
||||
@@ -441,9 +465,15 @@ function normalizeSaleFixedMarkupRules(values: unknown[]): PublishSaleFixedMarku
|
||||
min_m: readNumber(row.min_m),
|
||||
max_m: readNumber(row.max_m),
|
||||
markup_amount: readNumber(row.markup_amount),
|
||||
markup_amount_cent: readOptionalPositiveInteger(row.markup_amount_cent),
|
||||
}
|
||||
})
|
||||
.filter(item => item.min_m >= 0 && item.max_m >= item.min_m && item.markup_amount >= 0)
|
||||
.filter(
|
||||
item =>
|
||||
item.min_m >= 0 &&
|
||||
(item.max_m === 0 || item.max_m >= item.min_m) &&
|
||||
item.markup_amount >= 0
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeSaleRatioAdjustmentRules(values: unknown[]): PublishSaleRatioAdjustmentRule[] {
|
||||
@@ -454,6 +484,7 @@ function normalizeSaleRatioAdjustmentRules(values: unknown[]): PublishSaleRatioA
|
||||
min_m: readNumber(row.min_m),
|
||||
max_m: readNumber(row.max_m),
|
||||
ratio_subtract: readNumber(row.ratio_subtract),
|
||||
ratio_subtract_bps: readOptionalPositiveInteger(row.ratio_subtract_bps),
|
||||
}
|
||||
})
|
||||
.filter(
|
||||
@@ -469,6 +500,11 @@ function readNumber(value: unknown) {
|
||||
return Number.isFinite(parsed) ? parsed : 0
|
||||
}
|
||||
|
||||
function readOptionalPositiveInteger(value: unknown) {
|
||||
const parsed = Math.trunc(readNumber(value))
|
||||
return parsed > 0 ? parsed : undefined
|
||||
}
|
||||
|
||||
function readPositiveInteger(value: unknown, fallback: number) {
|
||||
const parsed = Math.trunc(readNumber(value))
|
||||
return parsed > 0 ? parsed : fallback
|
||||
|
||||
@@ -43,7 +43,7 @@ export interface PublishDraft {
|
||||
|
||||
export interface DepositBreakdownItem {
|
||||
label: string
|
||||
amount: number
|
||||
amountCent: number
|
||||
count: number
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user