展示枪皮与红皮,多皮肤支持悬浮查看完整列表
列表按分组展示红皮/枪皮,超出预览数时悬浮显示全部;复制信息输出完整皮肤名单。
This commit is contained in:
+94
-22
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user