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).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 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 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)[groupKey]) ) { return [] } return ((skinGroups as Record)[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) .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).start const end = (onlineTime as Record).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)[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) }