展示枪皮与红皮,多皮肤支持悬浮查看完整列表

列表按分组展示红皮/枪皮,超出预览数时悬浮显示全部;复制信息输出完整皮肤名单。
This commit is contained in:
yml2213
2026-07-18 21:56:30 +08:00
parent 2ecc6a3770
commit f9007fdd08
3 changed files with 244 additions and 29 deletions
+94 -22
View File
@@ -13,7 +13,7 @@ import Pagination from "antd/es/pagination";
import Spin from "antd/es/spin"; import Spin from "antd/es/spin";
import Tag from "antd/es/tag"; import Tag from "antd/es/tag";
import Tooltip from "antd/es/tooltip"; import Tooltip from "antd/es/tooltip";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState, type ReactNode } from "react";
import { import {
fetchListingsPage, fetchListingsPage,
fetchMobileHomeConfig, fetchMobileHomeConfig,
@@ -30,6 +30,7 @@ import {
formatListingInfoText, formatListingInfoText,
formatMoney, formatMoney,
formatRatio, formatRatio,
formatSkinPreview,
getConsumablePrice, getConsumablePrice,
getListingDeposit, getListingDeposit,
getListingPrice, getListingPrice,
@@ -38,7 +39,7 @@ import {
getRegions, getRegions,
getResourceQuantity, getResourceQuantity,
getResources, getResources,
getSkinNames, getSkinGroup,
readAssetNumber, readAssetNumber,
readAssetString, readAssetString,
} from "./listingDisplay"; } from "./listingDisplay";
@@ -730,27 +731,35 @@ function ListingRow({ codeCopied, infoCopied, item, onCopyCode, onCopyInfo }: Li
const rentalDuration = formatEstimatedRentalDuration(item); const rentalDuration = formatEstimatedRentalDuration(item);
const remark = getAssetText(item, "remark"); const remark = getAssetText(item, "remark");
const regions = getRegions(item); 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 fireLevel = readAssetNumber(item, "fire_level");
const resources = getResources(item); const resources = getResources(item);
const resourceTooltip = ( const resourceTooltip = (
<ResourceTooltipContent consumable={consumable} resources={resources} /> <ResourceTooltipContent consumable={consumable} resources={resources} />
); );
const details = [ const details: Array<{ label: string; value: ReactNode }> = [
["哈夫币", formatCoin(item)], { label: "哈夫币", value: formatCoin(item) },
["保险格", readAssetString(item, "season_insurance") || "-"], { label: "保险格", value: readAssetString(item, "season_insurance") || "-" },
["体力", readAssetString(item, "stamina_level") || "-"], { label: "体力", value: readAssetString(item, "stamina_level") || "-" },
["负重", readAssetString(item, "load_level") || "-"], { label: "负重", value: readAssetString(item, "load_level") || "-" },
["六头", String(getResourceQuantity(item, "helmet6") || "-")], { label: "六头", value: String(getResourceQuantity(item, "helmet6") || "-") },
["六甲", String(getResourceQuantity(item, "armor6") || "-")], { label: "六甲", value: String(getResourceQuantity(item, "armor6") || "-") },
["AW子弹", String(getResourceQuantity(item, "awmAmmo") || "-")], { label: "AW子弹", value: String(getResourceQuantity(item, "awmAmmo") || "-") },
["绝密KD", String(readAssetNumber(item, "secret_kd") || "0.00")], { label: "绝密KD", value: String(readAssetNumber(item, "secret_kd") || "0.00") },
["比例", formatRatio(item)], { label: "比例", value: formatRatio(item) },
["每日消耗", formatDailyLoss(item)], { label: "每日消耗", value: formatDailyLoss(item) },
["封禁记录", formatBanRecord(item)], { label: "封禁记录", value: formatBanRecord(item) },
["红皮", skinNames.slice(0, 2).join("、") || "-"], {
["登录方式", item.login_platform || "-"], label: "红皮",
["IP", regions[0] || item.server_region || "-"], 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 ( return (
@@ -775,10 +784,14 @@ function ListingRow({ codeCopied, infoCopied, item, onCopyCode, onCopyInfo }: Li
</div> </div>
<div className="detail-grid"> <div className="detail-grid">
{details.map(([label, value]) => ( {details.map((itemDetail) => (
<div key={`${code}-${label}`} className="detail-item"> <div key={`${code}-${itemDetail.label}`} className="detail-item">
<span>{label}</span> <span>{itemDetail.label}</span>
<strong>{value}</strong> {typeof itemDetail.value === "string" || typeof itemDetail.value === "number" ? (
<strong>{itemDetail.value}</strong>
) : (
itemDetail.value
)}
</div> </div>
))} ))}
</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({ function ResourceTooltipContent({
consumable, consumable,
resources, resources,
+64 -7
View File
@@ -138,7 +138,7 @@ export function formatResourceDetailLines(item: Listing) {
}); });
} }
/** 复制当前卡片展示的完整信息(非仅编号) */ /** 复制当前卡片展示的完整信息(非仅编号);皮肤按分组完整输出 */
export function formatListingInfoText(item: Listing) { export function formatListingInfoText(item: Listing) {
const code = formatListingCode(item); const code = formatListingCode(item);
const title = item.title?.trim() || `纯币${formatCoin(item)}资源号`; const title = item.title?.trim() || `纯币${formatCoin(item)}资源号`;
@@ -147,11 +147,14 @@ export function formatListingInfoText(item: Listing) {
const consumable = getConsumablePrice(item); const consumable = getConsumablePrice(item);
const total = Math.round((price + deposit) * 100) / 100; const total = Math.round((price + deposit) * 100) / 100;
const regions = getRegions(item); const regions = getRegions(item);
const skinNames = getSkinNames(item);
const remark = typeof item.asset_summary?.remark === "string" ? item.asset_summary.remark : ""; const remark = typeof item.asset_summary?.remark === "string" ? item.asset_summary.remark : "";
const online = getOnlineTimeText(item); const online = getOnlineTimeText(item);
const fireLevel = readAssetNumber(item, "fire_level"); const fireLevel = readAssetNumber(item, "fire_level");
const resourceLines = formatResourceDetailLines(item); 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 = [ const lines = [
`${title} 编号 ${code}`, `${title} 编号 ${code}`,
@@ -164,7 +167,10 @@ export function formatListingInfoText(item: Listing) {
`AW子弹:${getResourceQuantity(item, "awmAmmo") || "-"}`, `AW子弹:${getResourceQuantity(item, "awmAmmo") || "-"}`,
`绝密KD${readAssetNumber(item, "secret_kd") || "0.00"}`, `绝密KD${readAssetNumber(item, "secret_kd") || "0.00"}`,
`比例:${formatRatio(item)}`, `比例:${formatRatio(item)}`,
`红皮:${skinNames.slice(0, 2).join("、") || "-"}`, `红皮:${formatSkinList(redSkins)}`,
`枪皮:${formatSkinList(weaponSkins)}`,
meleeSkins.length ? `刀皮:${formatSkinList(meleeSkins)}` : "",
goldSkins.length ? `金皮:${formatSkinList(goldSkins)}` : "",
`登录方式:${item.login_platform || "-"}`, `登录方式:${item.login_platform || "-"}`,
`IP${regions[0] || item.server_region || "-"}`, `IP${regions[0] || item.server_region || "-"}`,
`封禁记录:${formatBanRecord(item)}`, `封禁记录:${formatBanRecord(item)}`,
@@ -199,12 +205,63 @@ export function readAssetNumber(item: Listing, key: string) {
return 0; 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; const skinGroups = item.asset_summary?.skin_groups;
if (typeof skinGroups !== "object" || skinGroups === null) return []; if (typeof skinGroups !== "object" || skinGroups === null) return [];
return Object.values(skinGroups as Record<string, unknown>) const value = (skinGroups as Record<string, unknown>)[groupKey];
.flatMap((group) => (Array.isArray(group) ? group : [])) if (!Array.isArray(value)) return [];
.filter((skin): skin is string => typeof skin === "string" && skin.trim() !== ""); 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) { export function getRegions(item: Listing) {
+86
View File
@@ -724,6 +724,92 @@ input {
white-space: nowrap; 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 { .listing-meta {
min-width: 0; min-width: 0;
display: flex; display: flex;