import CopyOutlined from "@ant-design/icons/es/icons/CopyOutlined"; import DownOutlined from "@ant-design/icons/es/icons/DownOutlined"; import ReloadOutlined from "@ant-design/icons/es/icons/ReloadOutlined"; import SearchOutlined from "@ant-design/icons/es/icons/SearchOutlined"; import Alert from "antd/es/alert"; import Button from "antd/es/button"; import ConfigProvider from "antd/es/config-provider"; import Empty from "antd/es/empty"; import Input from "antd/es/input"; import Pagination from "antd/es/pagination"; import Select from "antd/es/select"; import Spin from "antd/es/spin"; import Tag from "antd/es/tag"; import Tooltip from "antd/es/tooltip"; import { useEffect, useMemo, useState } from "react"; import { fetchListingsPage, fetchMobileHomeConfig, type Listing, type ListingPublishOptions, type PublicListingQuery, } from "./api"; import { formatCoin, formatEstimatedRentalDuration, formatListingCode, formatListingInfoText, formatMoney, formatRatio, getConsumablePrice, getListingDeposit, getListingPrice, getMainChips, getOnlineTimeText, getRegions, getResourceQuantity, getResources, getSkinNames, readAssetNumber, readAssetString, } from "./listingDisplay"; const pageSize = 10; const initialOptions: ListingPublishOptions = { server_options: [], login_method_options: [], rank_options: [], insurance_options: [], level_options: [], skin_groups: [], }; const fallbackLevels = ["4级", "5级", "6级", "7级"]; const fallbackInsurance = ["1*2", "2*2", "2*3", "3*3"]; const fallbackLogins = ["QQ", "微信", "Steam", "不限"]; const fallbackRanks = ["青铜", "白银", "黄金", "铂金", "钻石", "黑鹰", "巅峰"]; const fallbackSkins = ["信条", "坠星者", "处刑者", "影锋", "暗星", "电锯", "蚀金玫瑰", "维什戴尔"]; const coinRanges = [ { label: "15-50", min: "15", max: "50" }, { label: "50-100", min: "50", max: "100" }, { label: "100-150", min: "100", max: "150" }, { label: "150-200", min: "150", max: "200" }, { label: "200-250", min: "200", max: "250" }, { label: "300以上", min: "300", max: "" }, ]; const priceRanges = [ { label: "0-50", min: "0", max: "50" }, { label: "50-100", min: "50", max: "100" }, { label: "100-150", min: "100", max: "150" }, { label: "150-200", min: "150", max: "200" }, { label: "200以上", min: "200", max: "" }, ]; const sortOptions = [ { key: "recommended", label: "默认排序" }, { key: "priceDesc", label: "价格排序" }, { key: "ratioDesc", label: "比例排序" }, { key: "coinDesc", label: "哈夫币排序" }, { key: "ratioAsc", label: "绝密KD排序" }, { key: "coinAsc", label: "AWM排序" }, { key: "priceAsc", label: "六头排序" }, { key: "loadDesc", label: "六甲排序" }, ]; interface Filters { keyword: string; minCoin: string; maxCoin: string; minPrice: string; maxPrice: string; minKd: string; maxKd: string; stamina: string[]; load: string[]; insurance: string[]; skinNames: string[]; loginMethods: string[]; ranks: string[]; zones: string[]; } const emptyFilters: Filters = { keyword: "", minCoin: "", maxCoin: "", minPrice: "", maxPrice: "", minKd: "", maxKd: "", stamina: [], load: [], insurance: [], skinNames: [], loginMethods: [], ranks: [], zones: [], }; export default function App() { return ( ); } function Showcase() { const [listings, setListings] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); const [loadError, setLoadError] = useState(""); const [announcements, setAnnouncements] = useState([]); const [publishOptions, setPublishOptions] = useState(initialOptions); const [filters, setFilters] = useState(cloneFilters(emptyFilters)); const [appliedFilters, setAppliedFilters] = useState(cloneFilters(emptyFilters)); const [activeSort, setActiveSort] = useState("recommended"); const [advancedOpen, setAdvancedOpen] = useState(false); const [copiedCode, setCopiedCode] = useState(""); const [copiedInfoCode, setCopiedInfoCode] = useState(""); const levelOptions = useMemo( () => (publishOptions.level_options.length ? publishOptions.level_options : fallbackLevels), [publishOptions.level_options] ); const insuranceOptions = useMemo( () => (publishOptions.insurance_options.length ? publishOptions.insurance_options : fallbackInsurance), [publishOptions.insurance_options] ); const loginOptions = useMemo( () => (publishOptions.login_method_options.length ? publishOptions.login_method_options : fallbackLogins), [publishOptions.login_method_options] ); const rankOptions = useMemo( () => (publishOptions.rank_options.length ? publishOptions.rank_options : fallbackRanks), [publishOptions.rank_options] ); const zoneOptions = useMemo(() => publishOptions.server_options, [publishOptions.server_options]); const skinSections = useMemo(() => { const groups = publishOptions.skin_groups.filter((group) => ["melee", "operatorGold", "operatorRed", "weapon"].includes(group.key) ); if (groups.length) { return groups.map((group) => ({ key: group.key, title: groupTitle(group.key, group.title), options: group.options, })); } return [{ key: "skin", title: "稀有皮肤", options: fallbackSkins }]; }, [publishOptions.skin_groups]); const noticeText = announcements[0]?.trim() || ""; const displayListings = useMemo(() => { const items = [...listings]; if (activeSort === "ratioAsc") return items.sort((a, b) => ratioValue(a) - ratioValue(b)); if (activeSort === "ratioDesc") return items.sort((a, b) => ratioValue(b) - ratioValue(a)); if (activeSort === "coinAsc") return items.sort((a, b) => a.haf_coin_amount - b.haf_coin_amount); if (activeSort === "loadDesc") { return items.sort((a, b) => getResourceQuantity(b, "armor6") - getResourceQuantity(a, "armor6")); } return items; }, [activeSort, listings]); useEffect(() => { let disposed = false; fetchMobileHomeConfig() .then((home) => { if (disposed) return; setAnnouncements(home.announcements); setPublishOptions(home.publish_options); }) .catch(() => { if (!disposed) setPublishOptions(initialOptions); }); return () => { disposed = true; }; }, []); useEffect(() => { let disposed = false; setLoading(true); setLoadError(""); fetchListingsPage(buildQuery(page, appliedFilters, activeSort)) .then((result) => { if (disposed) return; setListings(result.items); setTotal(result.total); }) .catch((error) => { if (disposed) return; setListings([]); setTotal(0); setLoadError(error instanceof Error ? error.message : "加载失败"); }) .finally(() => { if (!disposed) setLoading(false); }); return () => { disposed = true; }; }, [activeSort, appliedFilters, page]); function updateFilter(key: K, value: Filters[K]) { setFilters((current) => ({ ...current, [key]: value })); } function toggleFilterList(key: "stamina" | "load" | "insurance" | "skinNames", value: string) { setFilters((current) => { const nextList = current[key].includes(value) ? current[key].filter((item) => item !== value) : [...current[key], value]; return { ...current, [key]: nextList }; }); } function applyFilters(nextFilters = filters) { setAppliedFilters(cloneFilters(nextFilters)); setPage(1); } function resetFilters() { const nextFilters = cloneFilters(emptyFilters); setFilters(nextFilters); setAppliedFilters(cloneFilters(nextFilters)); setActiveSort("recommended"); setPage(1); } function selectRange(minKey: "minCoin" | "minPrice", maxKey: "maxCoin" | "maxPrice", min: string, max: string) { setFilters((current) => { if (current[minKey] === min && current[maxKey] === max) { return { ...current, [minKey]: "", [maxKey]: "" }; } return { ...current, [minKey]: min, [maxKey]: max }; }); } function setSort(sortKey: string) { setActiveSort(sortKey); setPage(1); } async function copyListingCode(item: Listing) { const code = formatListingCode(item); try { await navigator.clipboard?.writeText(code); setCopiedCode(code); setCopiedInfoCode(""); window.setTimeout(() => { setCopiedCode((current) => (current === code ? "" : current)); }, 1500); } catch { setCopiedCode(""); } } async function copyListingInfo(item: Listing) { const code = formatListingCode(item); try { await navigator.clipboard?.writeText(formatListingInfoText(item)); setCopiedInfoCode(code); setCopiedCode(""); window.setTimeout(() => { setCopiedInfoCode((current) => (current === code ? "" : current)); }, 1500); } catch { setCopiedInfoCode(""); } } return (
大锤商行 哈夫租号
已实名认证 · 安全交易
禁止未成年人租号、购买游戏账号,严禁任何用户向未成年人出租、出售账号。
} onChange={(event) => updateFilter("keyword", event.target.value)} onSearch={() => applyFilters()} placeholder="请输入编号" value={filters.keyword} />
{noticeText ? (
公告
{noticeText}
) : null}
筛选条件
filters.minCoin === item.min && filters.maxCoin === item.max} label="哈夫币(M)" options={coinRanges} render={(item) => item.label} onSelect={(item) => selectRange("minCoin", "maxCoin", item.min, item.max)} /> filters.minPrice === item.min && filters.maxPrice === item.max} label="租金" options={priceRanges} render={(item) => item.label} onSelect={(item) => selectRange("minPrice", "maxPrice", item.min, item.max)} /> filters.insurance.includes(item)} label="保险" options={insuranceOptions} render={insuranceLabel} onSelect={(item) => toggleFilterList("insurance", item)} /> filters.stamina.includes(item)} label="体力" options={levelOptions} render={(item) => item} onSelect={(item) => toggleFilterList("stamina", item)} /> filters.load.includes(item)} label="负重" options={levelOptions} render={(item) => item} onSelect={(item) => toggleFilterList("load", item)} />
IP地址 ({ label: item, value: item }))} placeholder="不限" value={filters.loginMethods} onChange={(value) => updateFilter("loginMethods", value)} />
段位 updateFilter("minCoin", event.target.value)} /> updateFilter("maxCoin", event.target.value)} />
{skinSections.map((section) => ( filters.skinNames.includes(item)} label={section.title} options={section.options.slice(0, 18)} render={(item) => item} onSelect={(item) => toggleFilterList("skinNames", item)} /> ))}
{sortOptions.map((option) => ( ))}
{loadError ? applyFilters()}>重试} /> : null} {displayListings.length ? (
{displayListings.map((item) => ( ))}
) : ( )}
`第 ${page} 页 / 共 ${Math.max(1, Math.ceil(count / pageSize))} 页`} total={total} onChange={setPage} />
); } interface FilterRowProps { label: string; options: T[]; active: (item: T) => boolean; render: (item: T) => string; onSelect: (item: T) => void; } function FilterRow({ active, label, onSelect, options, render }: FilterRowProps) { return (
{label}
{options.map((item) => { const text = render(item); const selected = active(item); return ( ); })}
); } interface ListingRowProps { item: Listing; codeCopied: boolean; infoCopied: boolean; onCopyCode: (item: Listing) => void; onCopyInfo: (item: Listing) => void; } const FIXED_LISTING_COVER = "/listing-cover.jpg"; function ListingRow({ codeCopied, infoCopied, item, onCopyCode, onCopyInfo }: ListingRowProps) { const code = formatListingCode(item); const price = getListingPrice(item); const deposit = getListingDeposit(item); const consumable = getConsumablePrice(item); const totalPrice = Math.round((price + deposit) * 100) / 100; const rentalDuration = formatEstimatedRentalDuration(item); const remark = getAssetText(item, "remark"); const regions = getRegions(item); const skinNames = getSkinNames(item); const fireLevel = readAssetNumber(item, "fire_level"); const resources = getResources(item); const resourceTooltip = ( ); const details = [ ["哈夫币", formatCoin(item)], ["保险格", readAssetString(item, "season_insurance") || "-"], ["体力", readAssetString(item, "stamina_level") || "-"], ["负重", readAssetString(item, "load_level") || "-"], ["六头", String(getResourceQuantity(item, "helmet6") || "-")], ["六甲", String(getResourceQuantity(item, "armor6") || "-")], ["AW子弹", String(getResourceQuantity(item, "awmAmmo") || "-")], ["绝密KD", String(readAssetNumber(item, "secret_kd") || "0.00")], ["比例", formatRatio(item)], ["红皮", skinNames.slice(0, 2).join("、") || "-"], ["登录方式", item.login_platform || "-"], ["IP", regions[0] || item.server_region || "-"], ]; return (
{item.title
{item.title || `纯币${formatCoin(item)}资源号`} {code}
{item.is_accelerated_sale ? "爆卖" : item.rank_level || "展示"}
{details.map(([label, value]) => (
{label}: {value}
))}
{getOnlineTimeText(item) ? {getOnlineTimeText(item)} : null} {fireLevel ? 烽火 {fireLevel} : null} {getMainChips(item) .slice(0, 5) .map((chip) => ( {chip.label} ))} {resources.length ? 额外{resources.length}项 : null} {remark ? 备注:{remark} : null}
租金(包含额外物品) ¥{formatMoney(price)} 额外消耗品 ¥{formatMoney(consumable)} 租期 {rentalDuration} 押金 ¥{formatMoney(deposit)} 合计 ¥{formatMoney(totalPrice)}
); } function ResourceTooltipContent({ consumable, resources, }: { consumable: number; resources: ReturnType; }) { if (!resources.length) { return
暂无额外物品
; } return (
额外物品明细 {resources.length} 项
合计 ¥{formatMoney(consumable)}
{resources.map((resource) => (
{resource.label} ×{resource.quantity} {resource.mode || "-"}
{resource.price || "-"} {resource.mode === "收费" && resource.amount > 0 ? `¥${formatMoney(resource.amount)}` : "—"}
))}
); } function buildQuery(nextPage: number, filters: Filters, sort: string): PublicListingQuery { return { page: nextPage, page_size: pageSize, keyword: filters.keyword.trim(), sort: backendSortKey(sort), zone: filters.zones.join(","), login_method: filters.loginMethods.join(","), rank: filters.ranks.join(","), min_coin: toNumber(filters.minCoin), max_coin: toNumber(filters.maxCoin), min_price: toNumber(filters.minPrice), max_price: toNumber(filters.maxPrice), min_secret_kd: toNumber(filters.minKd), max_secret_kd: toNumber(filters.maxKd), stamina: filters.stamina.join(","), load: filters.load.join(","), insurance: filters.insurance.join(","), skin_name: filters.skinNames.join(","), }; } function backendSortKey(sort: string) { if (sort === "priceAsc" || sort === "priceDesc" || sort === "coinDesc") return sort; return "recommended"; } function ratioValue(item: Listing) { const configuredRatio = readAssetNumber(item, "publish_ratio"); if (configuredRatio > 0) return configuredRatio; const price = getListingPrice(item); if (!price) return 0; return Number(item.haf_coin_amount || 0) / 10000 / price; } function toNumber(value: string) { if (!value.trim()) return undefined; const parsed = Number(value); return Number.isFinite(parsed) ? parsed : undefined; } function cloneFilters(filters: Filters): Filters { return { ...filters, stamina: [...filters.stamina], load: [...filters.load], insurance: [...filters.insurance], skinNames: [...filters.skinNames], loginMethods: [...filters.loginMethods], ranks: [...filters.ranks], zones: [...filters.zones], }; } function groupTitle(key: string, title: string) { if (key === "melee") return "刀皮"; if (key === "operatorGold") return "干员金皮"; if (key === "operatorRed") return "干员红皮"; if (key === "weapon") return "枪皮"; return title; } function insuranceLabel(value: string) { const [rows, cols] = value.split("*").map((item) => Number(item)); if (rows > 0 && cols > 0) return `${rows * cols}格`; return value; } function getAssetText(item: Listing, key: string) { const value = item.asset_summary?.[key]; return typeof value === "string" ? value : ""; }