展示枪皮与红皮,多皮肤支持悬浮查看完整列表
列表按分组展示红皮/枪皮,超出预览数时悬浮显示全部;复制信息输出完整皮肤名单。
This commit is contained in:
+94
-22
@@ -13,7 +13,7 @@ import Pagination from "antd/es/pagination";
|
||||
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 { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import {
|
||||
fetchListingsPage,
|
||||
fetchMobileHomeConfig,
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
formatListingInfoText,
|
||||
formatMoney,
|
||||
formatRatio,
|
||||
formatSkinPreview,
|
||||
getConsumablePrice,
|
||||
getListingDeposit,
|
||||
getListingPrice,
|
||||
@@ -38,7 +39,7 @@ import {
|
||||
getRegions,
|
||||
getResourceQuantity,
|
||||
getResources,
|
||||
getSkinNames,
|
||||
getSkinGroup,
|
||||
readAssetNumber,
|
||||
readAssetString,
|
||||
} from "./listingDisplay";
|
||||
@@ -730,27 +731,35 @@ function ListingRow({ codeCopied, infoCopied, item, onCopyCode, onCopyInfo }: Li
|
||||
const rentalDuration = formatEstimatedRentalDuration(item);
|
||||
const remark = getAssetText(item, "remark");
|
||||
const regions = getRegions(item);
|
||||
const skinNames = getSkinNames(item);
|
||||
const redSkins = getSkinGroup(item, "operatorRed");
|
||||
const weaponSkins = getSkinGroup(item, "weapon");
|
||||
const fireLevel = readAssetNumber(item, "fire_level");
|
||||
const resources = getResources(item);
|
||||
const resourceTooltip = (
|
||||
<ResourceTooltipContent consumable={consumable} resources={resources} />
|
||||
);
|
||||
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)],
|
||||
["每日消耗", formatDailyLoss(item)],
|
||||
["封禁记录", formatBanRecord(item)],
|
||||
["红皮", skinNames.slice(0, 2).join("、") || "-"],
|
||||
["登录方式", item.login_platform || "-"],
|
||||
["IP", regions[0] || item.server_region || "-"],
|
||||
const details: Array<{ label: string; value: ReactNode }> = [
|
||||
{ label: "哈夫币", value: formatCoin(item) },
|
||||
{ label: "保险格", value: readAssetString(item, "season_insurance") || "-" },
|
||||
{ label: "体力", value: readAssetString(item, "stamina_level") || "-" },
|
||||
{ label: "负重", value: readAssetString(item, "load_level") || "-" },
|
||||
{ label: "六头", value: String(getResourceQuantity(item, "helmet6") || "-") },
|
||||
{ label: "六甲", value: String(getResourceQuantity(item, "armor6") || "-") },
|
||||
{ label: "AW子弹", value: String(getResourceQuantity(item, "awmAmmo") || "-") },
|
||||
{ label: "绝密KD", value: String(readAssetNumber(item, "secret_kd") || "0.00") },
|
||||
{ label: "比例", value: formatRatio(item) },
|
||||
{ label: "每日消耗", value: formatDailyLoss(item) },
|
||||
{ label: "封禁记录", value: formatBanRecord(item) },
|
||||
{
|
||||
label: "红皮",
|
||||
value: <SkinDetailValue label="红皮" names={redSkins} />,
|
||||
},
|
||||
{
|
||||
label: "枪皮",
|
||||
value: <SkinDetailValue label="枪皮" names={weaponSkins} />,
|
||||
},
|
||||
{ label: "登录方式", value: item.login_platform || "-" },
|
||||
{ label: "IP", value: regions[0] || item.server_region || "-" },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -775,10 +784,14 @@ function ListingRow({ codeCopied, infoCopied, item, onCopyCode, onCopyInfo }: Li
|
||||
</div>
|
||||
|
||||
<div className="detail-grid">
|
||||
{details.map(([label, value]) => (
|
||||
<div key={`${code}-${label}`} className="detail-item">
|
||||
<span>{label}</span>
|
||||
<strong>{value}</strong>
|
||||
{details.map((itemDetail) => (
|
||||
<div key={`${code}-${itemDetail.label}`} className="detail-item">
|
||||
<span>{itemDetail.label}</span>
|
||||
{typeof itemDetail.value === "string" || typeof itemDetail.value === "number" ? (
|
||||
<strong>{itemDetail.value}</strong>
|
||||
) : (
|
||||
itemDetail.value
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -844,6 +857,65 @@ function ListingRow({ codeCopied, infoCopied, item, onCopyCode, onCopyInfo }: Li
|
||||
);
|
||||
}
|
||||
|
||||
const SKIN_PREVIEW_LIMIT = 2;
|
||||
|
||||
function SkinDetailValue({ label, names }: { label: string; names: string[] }) {
|
||||
if (!names.length) {
|
||||
return <strong>-</strong>;
|
||||
}
|
||||
|
||||
const preview = formatSkinPreview(names, SKIN_PREVIEW_LIMIT);
|
||||
const hasMore = names.length > SKIN_PREVIEW_LIMIT;
|
||||
|
||||
if (!hasMore) {
|
||||
return (
|
||||
<strong className="skin-value" title={names.join("、")}>
|
||||
{preview}
|
||||
</strong>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
arrow={false}
|
||||
placement="top"
|
||||
trigger={["hover", "click"]}
|
||||
rootClassName="skin-tooltip-root"
|
||||
styles={{
|
||||
container: {
|
||||
padding: 0,
|
||||
background: "#fff",
|
||||
color: "#172033",
|
||||
borderRadius: 12,
|
||||
boxShadow: "0 12px 32px rgba(21, 32, 43, 0.14)",
|
||||
},
|
||||
}}
|
||||
title={<SkinTooltipContent label={label} names={names} />}
|
||||
>
|
||||
<strong className="skin-value has-more" tabIndex={0}>
|
||||
{names.slice(0, SKIN_PREVIEW_LIMIT).join("、")}
|
||||
<em className="skin-more">+{names.length - SKIN_PREVIEW_LIMIT}</em>
|
||||
</strong>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function SkinTooltipContent({ label, names }: { label: string; names: string[] }) {
|
||||
return (
|
||||
<div className="skin-popover">
|
||||
<div className="skin-popover-head">
|
||||
<strong>{label}</strong>
|
||||
<span>共 {names.length} 个</span>
|
||||
</div>
|
||||
<ul className="skin-popover-list">
|
||||
{names.map((name) => (
|
||||
<li key={name}>{name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ResourceTooltipContent({
|
||||
consumable,
|
||||
resources,
|
||||
|
||||
+64
-7
@@ -138,7 +138,7 @@ export function formatResourceDetailLines(item: Listing) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 复制当前卡片展示的完整信息(非仅编号) */
|
||||
/** 复制当前卡片展示的完整信息(非仅编号);皮肤按分组完整输出 */
|
||||
export function formatListingInfoText(item: Listing) {
|
||||
const code = formatListingCode(item);
|
||||
const title = item.title?.trim() || `纯币${formatCoin(item)}资源号`;
|
||||
@@ -147,11 +147,14 @@ export function formatListingInfoText(item: Listing) {
|
||||
const consumable = getConsumablePrice(item);
|
||||
const total = Math.round((price + deposit) * 100) / 100;
|
||||
const regions = getRegions(item);
|
||||
const skinNames = getSkinNames(item);
|
||||
const remark = typeof item.asset_summary?.remark === "string" ? item.asset_summary.remark : "";
|
||||
const online = getOnlineTimeText(item);
|
||||
const fireLevel = readAssetNumber(item, "fire_level");
|
||||
const resourceLines = formatResourceDetailLines(item);
|
||||
const redSkins = getSkinGroup(item, "operatorRed");
|
||||
const weaponSkins = getSkinGroup(item, "weapon");
|
||||
const meleeSkins = getSkinGroup(item, "melee");
|
||||
const goldSkins = getSkinGroup(item, "operatorGold");
|
||||
|
||||
const lines = [
|
||||
`${title} 编号 ${code}`,
|
||||
@@ -164,7 +167,10 @@ export function formatListingInfoText(item: Listing) {
|
||||
`AW子弹:${getResourceQuantity(item, "awmAmmo") || "-"}`,
|
||||
`绝密KD:${readAssetNumber(item, "secret_kd") || "0.00"}`,
|
||||
`比例:${formatRatio(item)}`,
|
||||
`红皮:${skinNames.slice(0, 2).join("、") || "-"}`,
|
||||
`红皮:${formatSkinList(redSkins)}`,
|
||||
`枪皮:${formatSkinList(weaponSkins)}`,
|
||||
meleeSkins.length ? `刀皮:${formatSkinList(meleeSkins)}` : "",
|
||||
goldSkins.length ? `金皮:${formatSkinList(goldSkins)}` : "",
|
||||
`登录方式:${item.login_platform || "-"}`,
|
||||
`IP:${regions[0] || item.server_region || "-"}`,
|
||||
`封禁记录:${formatBanRecord(item)}`,
|
||||
@@ -199,12 +205,63 @@ export function readAssetNumber(item: Listing, key: string) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function getSkinNames(item: Listing) {
|
||||
export interface ListingSkinGroup {
|
||||
key: string;
|
||||
title: string;
|
||||
options: string[];
|
||||
}
|
||||
|
||||
const SKIN_GROUP_TITLES: Record<string, string> = {
|
||||
melee: "刀皮",
|
||||
operator: "干员",
|
||||
operatorGold: "金皮",
|
||||
operatorRed: "红皮",
|
||||
weapon: "枪皮",
|
||||
};
|
||||
|
||||
/** 读取某个皮肤分组(如 operatorRed / weapon / melee) */
|
||||
export function getSkinGroup(item: Listing, groupKey: string): string[] {
|
||||
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" && skin.trim() !== "");
|
||||
const value = (skinGroups as Record<string, unknown>)[groupKey];
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.filter((skin): skin is string => typeof skin === "string" && skin.trim() !== "");
|
||||
}
|
||||
|
||||
/** 全部有数据的皮肤分组 */
|
||||
export function getListingSkinGroups(item: Listing): ListingSkinGroup[] {
|
||||
const skinGroups = item.asset_summary?.skin_groups;
|
||||
if (typeof skinGroups !== "object" || skinGroups === null) return [];
|
||||
return Object.entries(skinGroups as Record<string, unknown>)
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
title: SKIN_GROUP_TITLES[key] || key,
|
||||
options: Array.isArray(value)
|
||||
? value.filter((skin): skin is string => typeof skin === "string" && skin.trim() !== "")
|
||||
: [],
|
||||
}))
|
||||
.filter((group) => group.options.length > 0);
|
||||
}
|
||||
|
||||
/** 扁平全部皮肤名(用于标签等) */
|
||||
export function getSkinNames(item: Listing) {
|
||||
return getListingSkinGroups(item).flatMap((group) => group.options);
|
||||
}
|
||||
|
||||
/** 皮肤列表文案:无则 `-`,有则顿号拼接全部 */
|
||||
export function formatSkinList(names: string[]) {
|
||||
if (!names.length) return "-";
|
||||
return names.join("、");
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表预览文案:超过 limit 时截断并附 `+N`
|
||||
* 例如 `蚀金玫瑰、水墨云图 +3`
|
||||
*/
|
||||
export function formatSkinPreview(names: string[], limit = 2) {
|
||||
if (!names.length) return "-";
|
||||
if (names.length <= limit) return names.join("、");
|
||||
return `${names.slice(0, limit).join("、")} +${names.length - limit}`;
|
||||
}
|
||||
|
||||
export function getRegions(item: Listing) {
|
||||
|
||||
@@ -724,6 +724,92 @@ input {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-item .skin-value {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
vertical-align: top;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.detail-item .skin-value.has-more {
|
||||
cursor: pointer;
|
||||
color: #1f2a37;
|
||||
}
|
||||
|
||||
.detail-item .skin-value.has-more:hover {
|
||||
color: var(--brand-dark);
|
||||
}
|
||||
|
||||
.detail-item .skin-more {
|
||||
margin-left: 4px;
|
||||
color: var(--brand);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
/* 红皮/枪皮完整列表悬浮卡片 */
|
||||
.skin-tooltip-root .ant-tooltip-container,
|
||||
.skin-tooltip-root .ant-tooltip-inner {
|
||||
width: 280px !important;
|
||||
max-width: min(280px, calc(100vw - 24px)) !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
border-radius: 12px !important;
|
||||
border: 1px solid #e8edf3 !important;
|
||||
background: #fff !important;
|
||||
color: var(--text-main) !important;
|
||||
box-shadow: 0 12px 32px rgba(21, 32, 43, 0.14) !important;
|
||||
}
|
||||
|
||||
.skin-popover {
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.skin-popover-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 11px 14px;
|
||||
background: linear-gradient(180deg, #fff8f1, #fff);
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
}
|
||||
|
||||
.skin-popover-head strong {
|
||||
color: var(--text-main);
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.skin-popover-head span {
|
||||
color: #98a2b3;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.skin-popover-list {
|
||||
margin: 0;
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
padding: 8px 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.skin-popover-list li {
|
||||
padding: 7px 14px;
|
||||
color: #1f2a37;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.skin-popover-list li:hover {
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.listing-meta {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user