import type { Listing } from "@/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).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 === "收费" ? 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; 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 ""; 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; const coinWan = getCoinWan(item); if (coinWan >= 30000) return 30; if (coinWan >= 10000) return 20; return 10; } 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 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); }