Files
hfb_sys/frontend/src/shared/utils/listingDisplay.ts
T
2026-06-08 19:44:07 +08:00

308 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) {
if (item.listing_no) return item.listing_no
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 roundMoney(buyerCoinBasePrice)
return Math.max(0, roundMoney(getListingDisplayPrice(item) - getListingConsumablePrice(item)))
}
export function getListingConsumablePrice(item: Listing) {
const consumablePrice = readPriceBreakdownNumber(item, 'consumable_price')
if (consumablePrice > 0) return roundMoney(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 === '收费'
? roundMoney(
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 rounded.toFixed(1)
}
function formatCompactNumber(value: number) {
const rounded = Math.round(value * 10) / 10
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(1)
}