307 lines
10 KiB
TypeScript
307 lines
10 KiB
TypeScript
import type { Listing } from '@/features/listings/api/listings'
|
||
|
||
export interface ListingDisplayChip {
|
||
label: string
|
||
value: string
|
||
}
|
||
|
||
export interface ListingDisplayResource {
|
||
key: string
|
||
label: string
|
||
price: string
|
||
quantity: number
|
||
mode: string
|
||
amount: number
|
||
}
|
||
|
||
export function getCoinWan(item: Listing) {
|
||
return Math.round(Number(item.haf_coin_amount || 0) / 10000)
|
||
}
|
||
|
||
export function getCoinM(item: Listing) {
|
||
return getCoinWan(item) / 100
|
||
}
|
||
|
||
export function formatListingCode(item: Listing) {
|
||
return `SP${String(item.id).padStart(6, '0')}`
|
||
}
|
||
|
||
export function formatHafCoinM(amountWan: number) {
|
||
const amountM = amountWan / 100
|
||
const rounded = Math.round(amountM * 10) / 10
|
||
return `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)}M`
|
||
}
|
||
|
||
export function getListingDisplayPrice(item: Listing) {
|
||
return Number(item.price || 0)
|
||
}
|
||
|
||
export function getListingRentPrice(item: Listing) {
|
||
const buyerCoinBasePrice = readPriceBreakdownNumber(item, 'buyer_coin_base_price')
|
||
if (buyerCoinBasePrice > 0) return Math.round(buyerCoinBasePrice)
|
||
return Math.max(0, Math.round(getListingDisplayPrice(item) - getListingConsumablePrice(item)))
|
||
}
|
||
|
||
export function getListingConsumablePrice(item: Listing) {
|
||
const consumablePrice = readPriceBreakdownNumber(item, 'consumable_price')
|
||
if (consumablePrice > 0) return Math.round(consumablePrice)
|
||
return getListingResources(item).reduce((sum, resource) => sum + resource.amount, 0)
|
||
}
|
||
|
||
export function getListingSellerPrice(item: Listing) {
|
||
const priceBreakdown = item.asset_summary?.price_breakdown
|
||
if (typeof priceBreakdown === 'object' && priceBreakdown !== null) {
|
||
const price = readUnknownNumber((priceBreakdown as Record<string, unknown>).seller_total_price)
|
||
if (price > 0) return price
|
||
}
|
||
return getListingDisplayPrice(item)
|
||
}
|
||
|
||
export function getRatioValue(item: Listing) {
|
||
const ratio = readAssetNumber(item, 'publish_ratio')
|
||
if (ratio > 0) return ratio
|
||
const price = getListingDisplayPrice(item)
|
||
if (price <= 0) return 0
|
||
return getCoinWan(item) / price
|
||
}
|
||
|
||
export function formatRatio(item: Listing) {
|
||
const ratio = getRatioValue(item)
|
||
return ratio > 0 ? `1:${formatRatioNumber(ratio)}` : '--'
|
||
}
|
||
|
||
export function getValuePerYuanText(item: Listing) {
|
||
return formatRatio(item)
|
||
}
|
||
|
||
export function getLoginMethod(item: Listing) {
|
||
return item.login_platform.trim()
|
||
}
|
||
|
||
export function getServerRegion(item: Listing) {
|
||
return item.server_region.trim()
|
||
}
|
||
|
||
export function getListingTitle(item: Listing) {
|
||
const parts = [
|
||
`纯币${formatHafCoinM(getCoinWan(item))}`,
|
||
formatInsuranceSlotText(readAssetString(item, 'season_insurance')),
|
||
formatLevelShort(readAssetString(item, 'stamina_level'), '体'),
|
||
formatLevelShort(readAssetString(item, 'load_level'), '负'),
|
||
formatResourceShort(item, 'armor6', '六甲'),
|
||
formatResourceShort(item, 'helmet6', '六头'),
|
||
...getSkinNames(item).slice(0, 4),
|
||
].filter(Boolean)
|
||
return parts.join('/')
|
||
}
|
||
|
||
export function getListingSubtitle(item: Listing) {
|
||
return getValuePerYuanText(item)
|
||
}
|
||
|
||
export function getListingChips(item: Listing): ListingDisplayChip[] {
|
||
const totalAsset = readAssetNumber(item, 'total_asset_wan')
|
||
const chips: ListingDisplayChip[] = [
|
||
{ label: '哈夫币', value: formatHafCoinM(getCoinWan(item)) },
|
||
{ label: '保险格数', value: readAssetString(item, 'season_insurance') },
|
||
{ label: '体力', value: readAssetString(item, 'stamina_level') },
|
||
{ label: '负重', value: readAssetString(item, 'load_level') },
|
||
{ label: '段位', value: item.rank_level },
|
||
]
|
||
const awmAmmo = getResourceQuantity(item, 'awmAmmo')
|
||
if (awmAmmo > 0) {
|
||
chips.push({ label: 'AWM', value: `${awmAmmo}发` })
|
||
}
|
||
if (totalAsset > 0) {
|
||
chips.push({ label: '总资产', value: formatHafCoinM(totalAsset) })
|
||
}
|
||
const online = getOnlineTimeText(item)
|
||
if (online) {
|
||
chips.push({ label: '方便上号', value: online })
|
||
}
|
||
return chips.filter(chip => chip.value)
|
||
}
|
||
|
||
export function getListingResources(item: Listing): ListingDisplayResource[] {
|
||
const resources = item.asset_summary?.resources
|
||
if (!Array.isArray(resources)) return []
|
||
return resources
|
||
.map(resource => {
|
||
if (typeof resource !== 'object' || resource === null) return null
|
||
const row = resource as Record<string, unknown>
|
||
return {
|
||
key: typeof row.key === 'string' ? row.key : '',
|
||
label: typeof row.label === 'string' ? row.label : '',
|
||
price: typeof row.price === 'string' ? row.price : '',
|
||
quantity: readUnknownNumber(row.quantity),
|
||
mode: typeof row.mode === 'string' ? row.mode : '',
|
||
amount:
|
||
row.mode === '收费'
|
||
? Math.round(
|
||
readUnknownNumber(row.quantity) *
|
||
readUnitPrice(typeof row.price === 'string' ? row.price : '')
|
||
)
|
||
: 0,
|
||
}
|
||
})
|
||
.filter((resource): resource is ListingDisplayResource => {
|
||
return Boolean(resource?.key && resource.label && resource.quantity > 0)
|
||
})
|
||
}
|
||
|
||
export function getResourceQuantity(item: Listing, resourceKey: string) {
|
||
return getListingResources(item).find(resource => resource.key === resourceKey)?.quantity || 0
|
||
}
|
||
|
||
export function hasGiftResources(item: Listing) {
|
||
return getListingResources(item).some(resource => resource.mode === '赠送')
|
||
}
|
||
|
||
export function hasAcceleratedSaleRatio(item: Listing) {
|
||
if (item.is_accelerated_sale) return true
|
||
|
||
const priceBreakdown = item.asset_summary?.price_breakdown
|
||
if (typeof priceBreakdown !== 'object' || priceBreakdown === null) return false
|
||
|
||
const row = priceBreakdown as Record<string, unknown>
|
||
const referenceRatio = readUnknownNumber(row.seller_reference_ratio)
|
||
const sellerRatio = readUnknownNumber(row.seller_ratio)
|
||
const acceleratedRatio = readUnknownNumber(row.accelerated_sale_ratio)
|
||
|
||
if (referenceRatio <= 0) return false
|
||
return sellerRatio > referenceRatio || acceleratedRatio > referenceRatio
|
||
}
|
||
|
||
export function getSkinGroup(item: Listing, groupKey: string) {
|
||
const skinGroups = item.asset_summary?.skin_groups
|
||
if (
|
||
typeof skinGroups !== 'object' ||
|
||
skinGroups === null ||
|
||
!Array.isArray((skinGroups as Record<string, unknown>)[groupKey])
|
||
) {
|
||
return []
|
||
}
|
||
return ((skinGroups as Record<string, unknown>)[groupKey] as unknown[]).filter(
|
||
(skin): skin is string => typeof skin === 'string'
|
||
)
|
||
}
|
||
|
||
export function getSkinNames(item: Listing) {
|
||
const skinGroups = item.asset_summary?.skin_groups
|
||
if (typeof skinGroups !== 'object' || skinGroups === null) return []
|
||
return Object.values(skinGroups as Record<string, unknown>)
|
||
.flatMap(group => (Array.isArray(group) ? group : []))
|
||
.filter((skin): skin is string => typeof skin === 'string')
|
||
}
|
||
|
||
export function assetRegions(item: Listing) {
|
||
const regions = item.asset_summary?.common_regions
|
||
return Array.isArray(regions)
|
||
? regions.filter((region): region is string => typeof region === 'string')
|
||
: []
|
||
}
|
||
|
||
export function getOnlineTimeText(item: Listing) {
|
||
const onlineTime = item.asset_summary?.online_time
|
||
if (typeof onlineTime !== 'object' || onlineTime === null) return ''
|
||
const start = (onlineTime as Record<string, unknown>).start
|
||
const end = (onlineTime as Record<string, unknown>).end
|
||
if (typeof start !== 'string' || typeof end !== 'string' || !start || !end) return ''
|
||
if (start === '全天' || end === '全天' || (start === '00:00' && end === '23:59')) return '全天'
|
||
return `${start.replace(':00', '')}-${end.replace(':00', '')}点`
|
||
}
|
||
|
||
export function getDailyLoss(item: Listing) {
|
||
const dailyLossM = getDailyLossM(item)
|
||
return dailyLossM > 0 ? `${formatCompactNumber(dailyLossM)}M` : ''
|
||
}
|
||
|
||
export function getDailyLossM(item: Listing) {
|
||
const configuredLoss = readAssetNumber(item, 'daily_loss_m')
|
||
if (configuredLoss > 0) return configuredLoss
|
||
return 0
|
||
}
|
||
|
||
export function getEstimatedRentalDays(item: Listing) {
|
||
const coinM = getCoinM(item)
|
||
const dailyLossM = getDailyLossM(item)
|
||
if (coinM <= 0 || dailyLossM <= 0) return 0
|
||
return coinM / dailyLossM
|
||
}
|
||
|
||
export function formatEstimatedRentalDuration(item: Listing) {
|
||
const days = getEstimatedRentalDays(item)
|
||
if (days <= 0) return '--'
|
||
return `${Math.max(1, Math.round(days))}天`
|
||
}
|
||
|
||
export function readAssetString(item: Listing, key: string) {
|
||
const value = item.asset_summary?.[key]
|
||
return typeof value === 'string' ? value : ''
|
||
}
|
||
|
||
export function readAssetNumber(item: Listing, key: string) {
|
||
return readUnknownNumber(item.asset_summary?.[key])
|
||
}
|
||
|
||
function readUnknownNumber(value: unknown) {
|
||
if (typeof value === 'number') return value
|
||
if (typeof value === 'string' && value.trim()) {
|
||
const parsed = Number(value)
|
||
return Number.isFinite(parsed) ? parsed : 0
|
||
}
|
||
return 0
|
||
}
|
||
|
||
function readPriceBreakdownNumber(item: Listing, key: string) {
|
||
const priceBreakdown = item.asset_summary?.price_breakdown
|
||
if (typeof priceBreakdown !== 'object' || priceBreakdown === null) return 0
|
||
return readUnknownNumber((priceBreakdown as Record<string, unknown>)[key])
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
function formatInsuranceSlotText(value: string) {
|
||
const parts = value.split('*').map(item => Number(item))
|
||
const rows = parts[0] || 0
|
||
const cols = parts[1] || 0
|
||
if (!Number.isFinite(rows) || !Number.isFinite(cols) || rows <= 0 || cols <= 0) {
|
||
return value
|
||
}
|
||
return `${rows * cols}格`
|
||
}
|
||
|
||
function formatLevelShort(value: string, suffix: string) {
|
||
const level = value.match(/\d+/)?.[0]
|
||
return level ? `${level}${suffix}` : value
|
||
}
|
||
|
||
function formatResourceShort(item: Listing, key: string, label: string) {
|
||
const quantity = getResourceQuantity(item, key)
|
||
return quantity > 0 ? `${quantity}${label}` : ''
|
||
}
|
||
|
||
function roundMoney(value: number) {
|
||
return Math.round(value * 100) / 100
|
||
}
|
||
|
||
function formatRatioNumber(value: number) {
|
||
const rounded = roundMoney(value)
|
||
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(2)
|
||
}
|
||
|
||
function formatCompactNumber(value: number) {
|
||
const rounded = Math.round(value * 10) / 10
|
||
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(1)
|
||
}
|