重构前端:抽离发布表单逻辑
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
import type {
|
||||
ChargeMode,
|
||||
ListingPublishOptions,
|
||||
PublishDepositRecommendConfig,
|
||||
PublishOptionGroup,
|
||||
PublishQuantityItem,
|
||||
PublishRatioConfig,
|
||||
PublishSalePriceConfig,
|
||||
} from '@/api/listingOptions'
|
||||
import type { DepositBreakdownItem, PublishForm, PublishPlatformPricing } from '@/types/publish'
|
||||
|
||||
export const dailyLossOptions = [10, 20, 30, 40, 50]
|
||||
export const commonOnlineTimes = ['00:00', '08:00', '10:00', '12:00', '14:00', '18:00', '20:00', '22:00', '23:59']
|
||||
|
||||
export function roundMoney(value: number) {
|
||||
return Math.round(value * 100) / 100
|
||||
}
|
||||
|
||||
export function roundRatio(value: number) {
|
||||
return Math.round(value * 10) / 10
|
||||
}
|
||||
|
||||
export function formatNumber(value: number) {
|
||||
return Number.isInteger(value) ? `${value}` : `${Math.round(value * 10) / 10}`
|
||||
}
|
||||
|
||||
export function readUnitPrice(priceText: string) {
|
||||
const normalized = priceText.replace(/,/g, ',').trim()
|
||||
const fractionMatch = normalized.match(/(\d+(?:\.\d+)?)\s*元\s*\/\s*(\d+(?:\.\d+)?)/)
|
||||
if (fractionMatch) {
|
||||
const amount = Number(fractionMatch[1])
|
||||
const count = Number(fractionMatch[2])
|
||||
return count > 0 ? amount / count : 0
|
||||
}
|
||||
const singleMatch = normalized.match(/(\d+(?:\.\d+)?)\s*元/)
|
||||
return singleMatch ? Number(singleMatch[1]) : 0
|
||||
}
|
||||
|
||||
export function calculateDailyLossRatioAdjustment(dailyLossMAmount: number) {
|
||||
return Math.min(Math.max(Math.floor((dailyLossMAmount - 10) / 10), 0), 4)
|
||||
}
|
||||
|
||||
export function isGridCardQuantityItem(item: { key: string; label: string }) {
|
||||
return item.key === 'gridCard9' || item.label.includes('9格体验卡')
|
||||
}
|
||||
|
||||
export function isQuantityItemDisabledForInsurance(item: { key: string; label: string }, seasonInsurance: string) {
|
||||
return seasonInsurance === '3*3' && isGridCardQuantityItem(item)
|
||||
}
|
||||
|
||||
export function calculateConsumablePrice(options: {
|
||||
quantityItems: PublishQuantityItem[]
|
||||
quantityValues: Record<string, number>
|
||||
quantityModes: Record<string, ChargeMode>
|
||||
seasonInsurance: string
|
||||
}) {
|
||||
const total = options.quantityItems.reduce((sum, item) => {
|
||||
const quantity = Number(options.quantityValues[item.key] || 0)
|
||||
const mode = options.quantityModes[item.key] || '收费'
|
||||
if (isQuantityItemDisabledForInsurance(item, options.seasonInsurance)) return sum
|
||||
if (quantity <= 0 || mode !== '收费') return sum
|
||||
return sum + quantity * readUnitPrice(item.price)
|
||||
}, 0)
|
||||
return roundMoney(total)
|
||||
}
|
||||
|
||||
export function calculateRecommendedDeposit(options: {
|
||||
depositRecommendConfig: PublishDepositRecommendConfig
|
||||
skinGroups: PublishOptionGroup[]
|
||||
selectedSkins: string[]
|
||||
}) {
|
||||
const baseAmount = Number(options.depositRecommendConfig.base_amount || 0)
|
||||
const skinAmount = options.depositRecommendConfig.skin_group_rules.reduce((sum, rule) => {
|
||||
const group = options.skinGroups.find((item) => item.key === rule.group_key)
|
||||
if (!group) return sum
|
||||
const selectedCount = group.options.filter((skin) => options.selectedSkins.includes(skin)).length
|
||||
return sum + selectedCount * Number(rule.amount_per_item || 0)
|
||||
}, 0)
|
||||
return roundMoney(baseAmount + skinAmount)
|
||||
}
|
||||
|
||||
export function buildDepositBreakdownItems(options: {
|
||||
depositRecommendConfig: PublishDepositRecommendConfig
|
||||
skinGroups: PublishOptionGroup[]
|
||||
selectedSkins: string[]
|
||||
}): DepositBreakdownItem[] {
|
||||
const items: DepositBreakdownItem[] = [
|
||||
{
|
||||
label: '基础押金',
|
||||
amount: Number(options.depositRecommendConfig.base_amount || 0),
|
||||
count: 1,
|
||||
},
|
||||
]
|
||||
for (const rule of options.depositRecommendConfig.skin_group_rules) {
|
||||
const group = options.skinGroups.find((item) => item.key === rule.group_key)
|
||||
if (!group) continue
|
||||
const count = group.options.filter((skin) => options.selectedSkins.includes(skin)).length
|
||||
if (count <= 0) continue
|
||||
items.push({
|
||||
label: rule.label,
|
||||
amount: Number(rule.amount_per_item || 0) * count,
|
||||
count,
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
export function calculateSellerReferenceRatio(options: {
|
||||
coinMAmount: number
|
||||
form: PublishForm
|
||||
ratioConfig: PublishRatioConfig
|
||||
skinGroups: PublishOptionGroup[]
|
||||
selectedSkins: string[]
|
||||
levelOptions: string[]
|
||||
dailyLossRatioAdjustment: number
|
||||
}) {
|
||||
const { coinMAmount, form, ratioConfig } = options
|
||||
if (coinMAmount <= 0 || !form.season_insurance || !form.stamina_level || !form.load_level) return 0
|
||||
const baseRatio = getInsuranceBaseRatio(ratioConfig, form.season_insurance)
|
||||
if (baseRatio <= 0) return 0
|
||||
return (
|
||||
baseRatio +
|
||||
calculateConfigPenalty(ratioConfig, options) +
|
||||
getCoinCorrection(ratioConfig, coinMAmount) +
|
||||
options.dailyLossRatioAdjustment
|
||||
)
|
||||
}
|
||||
|
||||
export function readFinalSaleRatio(defaultRatio: number, acceleratedSaleRatio: number | '', maxAcceleratedSaleRatio: number) {
|
||||
if (defaultRatio <= 0) return 0
|
||||
if (!hasAcceleratedSaleRatioInput(acceleratedSaleRatio)) return defaultRatio
|
||||
const ratio = Number(acceleratedSaleRatio)
|
||||
if (!Number.isFinite(ratio) || ratio <= 0) return defaultRatio
|
||||
return roundRatio(Math.min(Math.max(ratio, defaultRatio), maxAcceleratedSaleRatio))
|
||||
}
|
||||
|
||||
export function hasAcceleratedSaleRatioInput(value: number | '') {
|
||||
return value !== '' && value !== null
|
||||
}
|
||||
|
||||
export function calculatePlatformPricing(options: {
|
||||
coinMAmount: number
|
||||
coinWanAmount: number
|
||||
sellerRatio: number
|
||||
sellerCoinBasePrice: number
|
||||
sellerTotalPrice: number
|
||||
consumablePrice: number
|
||||
salePriceConfig: PublishSalePriceConfig
|
||||
}): PublishPlatformPricing {
|
||||
if (options.sellerRatio <= 0 || options.sellerCoinBasePrice <= 0) return emptyPlatformPricing()
|
||||
const fixedRule = findSaleFixedMarkupRule(options.salePriceConfig, options.coinMAmount)
|
||||
if (fixedRule) {
|
||||
return buildPlatformPricing(
|
||||
roundMoney(options.sellerCoinBasePrice + Number(fixedRule.markup_amount || 0)),
|
||||
'fixed_markup',
|
||||
options,
|
||||
)
|
||||
}
|
||||
const ratioRule = findSaleRatioAdjustmentRule(options.salePriceConfig, options.coinMAmount)
|
||||
const ratioSubtract = ratioRule ? Number(ratioRule.ratio_subtract || 0) : 0
|
||||
const buyerRatio = options.sellerRatio - ratioSubtract
|
||||
if (buyerRatio > 0 && ratioRule) {
|
||||
return buildPlatformPricing(roundMoney(options.coinWanAmount / buyerRatio), 'ratio_subtract', options)
|
||||
}
|
||||
return buildPlatformPricing(options.sellerCoinBasePrice, 'none', options)
|
||||
}
|
||||
|
||||
export function emptyPlatformPricing(): PublishPlatformPricing {
|
||||
return {
|
||||
buyerCoinBasePrice: 0,
|
||||
buyerTotalPrice: 0,
|
||||
buyerRatio: 0,
|
||||
platformMarkupAmount: 0,
|
||||
ruleType: 'none',
|
||||
}
|
||||
}
|
||||
|
||||
function buildPlatformPricing(
|
||||
buyerCoinBasePrice: number,
|
||||
ruleType: string,
|
||||
options: {
|
||||
coinWanAmount: number
|
||||
sellerTotalPrice: number
|
||||
consumablePrice: number
|
||||
},
|
||||
): PublishPlatformPricing {
|
||||
const buyerTotalPrice = roundMoney(buyerCoinBasePrice + options.consumablePrice)
|
||||
return {
|
||||
buyerCoinBasePrice,
|
||||
buyerTotalPrice,
|
||||
buyerRatio: calculateEffectiveRatio(options.coinWanAmount, buyerCoinBasePrice),
|
||||
platformMarkupAmount: roundMoney(buyerTotalPrice - options.sellerTotalPrice),
|
||||
ruleType,
|
||||
}
|
||||
}
|
||||
|
||||
function findSaleFixedMarkupRule(config: PublishSalePriceConfig, coinMAmount: number) {
|
||||
return [...config.fixed_markup_rules]
|
||||
.sort((a, b) => a.min_m - b.min_m)
|
||||
.find((item, index, rules) => isCoinInSaleRange(item, index, rules, coinMAmount, { includeLastMax: true }))
|
||||
}
|
||||
|
||||
function findSaleRatioAdjustmentRule(config: PublishSalePriceConfig, coinMAmount: number) {
|
||||
return [...config.ratio_adjustment_rules]
|
||||
.sort((a, b) => a.min_m - b.min_m)
|
||||
.find((item, index, rules) => isCoinInSaleRange(item, index, rules, coinMAmount, { excludeFirstMin: true }))
|
||||
}
|
||||
|
||||
function isCoinInSaleRange(
|
||||
item: { min_m: number; max_m: number },
|
||||
index: number,
|
||||
rules: Array<{ min_m: number; max_m: number }>,
|
||||
coinMAmount: number,
|
||||
options: { includeLastMax?: boolean; excludeFirstMin?: boolean } = {},
|
||||
) {
|
||||
const maxM = Number(item.max_m || 0)
|
||||
const minM = Number(item.min_m || 0)
|
||||
const minMatched = options.excludeFirstMin && index === 0 ? coinMAmount > minM : coinMAmount >= minM
|
||||
const isLastRule = index === rules.length - 1
|
||||
const maxMatched = maxM <= 0 || coinMAmount < maxM || (options.includeLastMax && isLastRule && coinMAmount <= maxM)
|
||||
return minMatched && maxMatched
|
||||
}
|
||||
|
||||
function calculateEffectiveRatio(coinWanAmount: number, price: number) {
|
||||
if (price <= 0) return 0
|
||||
return roundRatio(coinWanAmount / price)
|
||||
}
|
||||
|
||||
function getInsuranceBaseRatio(config: Pick<ListingPublishOptions['ratio_config'], 'insurance_base_ratios'>, insurance: string) {
|
||||
return config.insurance_base_ratios.find((item) => item.insurance === insurance)?.ratio || 0
|
||||
}
|
||||
|
||||
function calculateConfigPenalty(
|
||||
config: Pick<ListingPublishOptions['ratio_config'], 'config_items'>,
|
||||
options: {
|
||||
form: PublishForm
|
||||
skinGroups: PublishOptionGroup[]
|
||||
selectedSkins: string[]
|
||||
levelOptions: string[]
|
||||
},
|
||||
) {
|
||||
return config.config_items.reduce((sum, item) => {
|
||||
return isRatioConfigItemMatched(item, options) ? sum : sum + Number(item.missing_penalty || 0)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function isRatioConfigItemMatched(
|
||||
item: { kind: string; group_key?: string },
|
||||
options: {
|
||||
form: PublishForm
|
||||
skinGroups: PublishOptionGroup[]
|
||||
selectedSkins: string[]
|
||||
levelOptions: string[]
|
||||
},
|
||||
) {
|
||||
if (item.kind === 'skin_group') return hasSelectedSkinGroup(item.group_key || '', options)
|
||||
if (item.kind === 'max_stamina') return isMaxLevel(options.form.stamina_level, options.levelOptions)
|
||||
if (item.kind === 'max_load') return isMaxLevel(options.form.load_level, options.levelOptions)
|
||||
return false
|
||||
}
|
||||
|
||||
function hasSelectedSkinGroup(
|
||||
groupKey: string,
|
||||
options: {
|
||||
skinGroups: PublishOptionGroup[]
|
||||
selectedSkins: string[]
|
||||
},
|
||||
) {
|
||||
const group = options.skinGroups.find((item) => item.key === groupKey)
|
||||
if (!group) return false
|
||||
return group.options.some((skin) => options.selectedSkins.includes(skin))
|
||||
}
|
||||
|
||||
function isMaxLevel(value: string, levelOptions: string[]) {
|
||||
const currentLevel = readLevelNumber(value)
|
||||
const maxLevel = Math.max(...levelOptions.map(readLevelNumber).filter(Boolean))
|
||||
if (currentLevel > 0 && maxLevel > 0) return currentLevel >= maxLevel
|
||||
return value === levelOptions[levelOptions.length - 1]
|
||||
}
|
||||
|
||||
function readLevelNumber(value: string) {
|
||||
const match = value.match(/\d+/)
|
||||
return match ? Number(match[0]) : 0
|
||||
}
|
||||
|
||||
function getCoinCorrection(config: Pick<ListingPublishOptions['ratio_config'], 'coin_corrections'>, coinM: number) {
|
||||
return [...config.coin_corrections].sort((a, b) => b.threshold_m - a.threshold_m).find((item) => coinM > item.threshold_m)?.correction || 0
|
||||
}
|
||||
Reference in New Issue
Block a user