优化商品展示:固定封面、正确编号与价格、租期与复制信息
使用固定商品图避免远程图加载慢;编号改为 listing_no;价格按 price_cent 换算;去掉 Hero 横幅;公告仅一条滚动;展示租期;区分复制编号与复制完整信息。
This commit is contained in:
+62
-55
@@ -11,20 +11,23 @@ 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 type { CSSProperties } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
fetchListingsPage,
|
||||
fetchMobileHomeConfig,
|
||||
type HomeBannerSlide,
|
||||
type Listing,
|
||||
type ListingPublishOptions,
|
||||
type PublicListingQuery,
|
||||
} from "./api";
|
||||
import {
|
||||
formatCoin,
|
||||
formatEstimatedRentalDuration,
|
||||
formatListingCode,
|
||||
formatListingInfoText,
|
||||
formatMoney,
|
||||
formatRatio,
|
||||
getListingDeposit,
|
||||
getListingPrice,
|
||||
getMainChips,
|
||||
getOnlineTimeText,
|
||||
getRegions,
|
||||
@@ -148,13 +151,13 @@ function Showcase() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadError, setLoadError] = useState("");
|
||||
const [announcements, setAnnouncements] = useState<string[]>([]);
|
||||
const [banners, setBanners] = useState<HomeBannerSlide[]>([]);
|
||||
const [publishOptions, setPublishOptions] = useState<ListingPublishOptions>(initialOptions);
|
||||
const [filters, setFilters] = useState<Filters>(cloneFilters(emptyFilters));
|
||||
const [appliedFilters, setAppliedFilters] = useState<Filters>(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),
|
||||
@@ -187,17 +190,7 @@ function Showcase() {
|
||||
return [{ key: "skin", title: "稀有皮肤", options: fallbackSkins }];
|
||||
}, [publishOptions.skin_groups]);
|
||||
|
||||
const topBanner = useMemo(
|
||||
() =>
|
||||
banners[0] || {
|
||||
eyebrow: "大锤商行 · 资源号租赁",
|
||||
title: "哈夫币资源号实时租赁",
|
||||
badge: "24H",
|
||||
pill: "安全交易 · 诚信服务 · 专业保障",
|
||||
tone: "orange",
|
||||
},
|
||||
[banners]
|
||||
);
|
||||
const noticeText = announcements[0]?.trim() || "";
|
||||
|
||||
const displayListings = useMemo(() => {
|
||||
const items = [...listings];
|
||||
@@ -216,7 +209,6 @@ function Showcase() {
|
||||
.then((home) => {
|
||||
if (disposed) return;
|
||||
setAnnouncements(home.announcements);
|
||||
setBanners(home.banners);
|
||||
setPublishOptions(home.publish_options);
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -284,12 +276,12 @@ function Showcase() {
|
||||
setPage(1);
|
||||
}
|
||||
|
||||
async function copyListing(item: Listing) {
|
||||
async function copyListingCode(item: Listing) {
|
||||
const code = formatListingCode(item);
|
||||
const text = `${code} 纯币 ${formatCoin(item)} ¥${item.price} 比例 ${formatRatio(item)}`;
|
||||
try {
|
||||
await navigator.clipboard?.writeText(text);
|
||||
await navigator.clipboard?.writeText(code);
|
||||
setCopiedCode(code);
|
||||
setCopiedInfoCode("");
|
||||
window.setTimeout(() => {
|
||||
setCopiedCode((current) => (current === code ? "" : current));
|
||||
}, 1500);
|
||||
@@ -298,11 +290,19 @@ function Showcase() {
|
||||
}
|
||||
}
|
||||
|
||||
const heroStyle: CSSProperties | undefined = topBanner.image_url
|
||||
? {
|
||||
backgroundImage: `linear-gradient(90deg, rgba(255, 246, 237, .94), rgba(255, 246, 237, .72)), url(${topBanner.image_url})`,
|
||||
}
|
||||
: undefined;
|
||||
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 (
|
||||
<main className="show-page">
|
||||
@@ -323,23 +323,11 @@ function Showcase() {
|
||||
<Button type="primary">24h在线客服</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="warning-line" role="note" aria-label="未成年人保护提示">
|
||||
<div className="marquee-track">
|
||||
<span>禁止未成年人租号、购买游戏账号,严禁任何用户向未成年人出租、出售账号。</span>
|
||||
<span aria-hidden="true">禁止未成年人租号、购买游戏账号,严禁任何用户向未成年人出租、出售账号。</span>
|
||||
</div>
|
||||
<div className="warning-line" role="note">
|
||||
禁止未成年人租号、购买游戏账号,严禁任何用户向未成年人出租、出售账号。
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="hero-strip" style={heroStyle}>
|
||||
<div>
|
||||
<span>{topBanner.eyebrow || "大锤商行 · 资源号租赁"}</span>
|
||||
<strong>{topBanner.title || "哈夫币资源号实时租赁"}</strong>
|
||||
</div>
|
||||
<Tag color="orange">{topBanner.badge || "HOT"}</Tag>
|
||||
<p>{topBanner.pill || "安全交易 · 诚信服务 · 专业保障"}</p>
|
||||
</section>
|
||||
|
||||
<section className="search-stage">
|
||||
<Input.Search
|
||||
enterButton={<SearchOutlined />}
|
||||
@@ -350,13 +338,13 @@ function Showcase() {
|
||||
/>
|
||||
</section>
|
||||
|
||||
{announcements.length ? (
|
||||
{noticeText ? (
|
||||
<div className="notice-line" role="status" aria-live="polite">
|
||||
<span className="notice-label">公告</span>
|
||||
<div className="notice-marquee">
|
||||
<div className="marquee-track">
|
||||
<span>{announcements.join(" | ")}</span>
|
||||
<span aria-hidden="true">{announcements.join(" | ")}</span>
|
||||
<span>{noticeText}</span>
|
||||
<span aria-hidden="true">{noticeText}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -515,7 +503,14 @@ function Showcase() {
|
||||
{displayListings.length ? (
|
||||
<div className="listing-list">
|
||||
{displayListings.map((item) => (
|
||||
<ListingRow key={item.id} copied={copiedCode === formatListingCode(item)} item={item} onCopy={copyListing} />
|
||||
<ListingRow
|
||||
key={item.id}
|
||||
codeCopied={copiedCode === formatListingCode(item)}
|
||||
infoCopied={copiedInfoCode === formatListingCode(item)}
|
||||
item={item}
|
||||
onCopyCode={copyListingCode}
|
||||
onCopyInfo={copyListingInfo}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
@@ -582,14 +577,20 @@ function FilterRow<T>({ active, label, onSelect, options, render }: FilterRowPro
|
||||
|
||||
interface ListingRowProps {
|
||||
item: Listing;
|
||||
copied: boolean;
|
||||
onCopy: (item: Listing) => void;
|
||||
codeCopied: boolean;
|
||||
infoCopied: boolean;
|
||||
onCopyCode: (item: Listing) => void;
|
||||
onCopyInfo: (item: Listing) => void;
|
||||
}
|
||||
|
||||
function ListingRow({ copied, item, onCopy }: ListingRowProps) {
|
||||
const FIXED_LISTING_COVER = "/listing-cover.jpg";
|
||||
|
||||
function ListingRow({ codeCopied, infoCopied, item, onCopyCode, onCopyInfo }: ListingRowProps) {
|
||||
const code = formatListingCode(item);
|
||||
const cover = item.cover_url || item.screenshot_urls?.[0] || "";
|
||||
const totalPrice = Math.round(Number(item.price || 0) + Number(item.deposit_amount || 0));
|
||||
const price = getListingPrice(item);
|
||||
const deposit = getListingDeposit(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);
|
||||
@@ -612,15 +613,15 @@ function ListingRow({ copied, item, onCopy }: ListingRowProps) {
|
||||
return (
|
||||
<article className="listing-row">
|
||||
<div className="listing-cover">
|
||||
{cover ? <img alt={item.title || code} loading="lazy" src={cover} /> : <span>暂无截图</span>}
|
||||
<img alt={item.title || code} decoding="async" loading="lazy" src={FIXED_LISTING_COVER} />
|
||||
</div>
|
||||
<div className="listing-main">
|
||||
<div className="listing-head">
|
||||
<div>
|
||||
<strong>{item.title || `纯币${formatCoin(item)}资源号`}</strong>
|
||||
<Tag color="orange">{code}</Tag>
|
||||
<Button icon={<CopyOutlined />} size="small" type="text" onClick={() => onCopy(item)}>
|
||||
{copied ? "已复制" : ""}
|
||||
<Button icon={<CopyOutlined />} size="small" type="text" onClick={() => onCopyCode(item)}>
|
||||
{codeCopied ? "已复制" : "复制"}
|
||||
</Button>
|
||||
</div>
|
||||
<Tag color={item.is_accelerated_sale ? "orange" : "gold"}>{item.is_accelerated_sale ? "爆卖" : item.rank_level || "展示"}</Tag>
|
||||
@@ -650,15 +651,20 @@ function ListingRow({ copied, item, onCopy }: ListingRowProps) {
|
||||
|
||||
<div className="price-line">
|
||||
<span>
|
||||
租金(只含纯币) <strong>¥{Math.round(item.price || 0).toFixed(2)}</strong>
|
||||
租金 <strong>¥{formatMoney(price)}</strong>
|
||||
</span>
|
||||
<span>
|
||||
押金 <strong>¥{Math.round(item.deposit_amount || 0).toFixed(2)}</strong>
|
||||
租期 <strong className="rental-days">{rentalDuration}</strong>
|
||||
</span>
|
||||
<span>
|
||||
合计 <strong>¥{totalPrice.toFixed(2)}</strong>
|
||||
押金 <strong>¥{formatMoney(deposit)}</strong>
|
||||
</span>
|
||||
<Button type="primary">立即租用</Button>
|
||||
<span>
|
||||
合计 <strong>¥{formatMoney(totalPrice)}</strong>
|
||||
</span>
|
||||
<Button type="link" onClick={() => onCopyInfo(item)}>
|
||||
{infoCopied ? "已复制" : "复制信息"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -695,8 +701,9 @@ function backendSortKey(sort: string) {
|
||||
function ratioValue(item: Listing) {
|
||||
const configuredRatio = readAssetNumber(item, "publish_ratio");
|
||||
if (configuredRatio > 0) return configuredRatio;
|
||||
if (!item.price) return 0;
|
||||
return Number(item.haf_coin_amount || 0) / 10000 / item.price;
|
||||
const price = getListingPrice(item);
|
||||
if (!price) return 0;
|
||||
return Number(item.haf_coin_amount || 0) / 10000 / price;
|
||||
}
|
||||
|
||||
function toNumber(value: string) {
|
||||
|
||||
Reference in New Issue
Block a user