diff --git a/src/App.tsx b/src/App.tsx
index 226c064..8054ba5 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -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 = (
);
- 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: ,
+ },
+ {
+ label: "枪皮",
+ value: ,
+ },
+ { 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
- {details.map(([label, value]) => (
-
-
{label}
-
{value}
+ {details.map((itemDetail) => (
+
+ {itemDetail.label}
+ {typeof itemDetail.value === "string" || typeof itemDetail.value === "number" ? (
+ {itemDetail.value}
+ ) : (
+ itemDetail.value
+ )}
))}
@@ -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
-;
+ }
+
+ const preview = formatSkinPreview(names, SKIN_PREVIEW_LIMIT);
+ const hasMore = names.length > SKIN_PREVIEW_LIMIT;
+
+ if (!hasMore) {
+ return (
+
+ {preview}
+
+ );
+ }
+
+ return (
+
}
+ >
+
+ {names.slice(0, SKIN_PREVIEW_LIMIT).join("、")}
+ +{names.length - SKIN_PREVIEW_LIMIT}
+
+
+ );
+}
+
+function SkinTooltipContent({ label, names }: { label: string; names: string[] }) {
+ return (
+
+
+ {label}
+ 共 {names.length} 个
+
+
+ {names.map((name) => (
+ - {name}
+ ))}
+
+
+ );
+}
+
function ResourceTooltipContent({
consumable,
resources,
diff --git a/src/listingDisplay.ts b/src/listingDisplay.ts
index 7b8ecb1..026cd5c 100644
--- a/src/listingDisplay.ts
+++ b/src/listingDisplay.ts
@@ -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
= {
+ 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)
- .flatMap((group) => (Array.isArray(group) ? group : []))
- .filter((skin): skin is string => typeof skin === "string" && skin.trim() !== "");
+ const value = (skinGroups as Record)[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)
+ .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) {
diff --git a/src/styles.css b/src/styles.css
index 57c216a..d0134a0 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -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;