From 51991071e7eaa97931e33f0c239d30bbac3d385b Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 13 Jul 2026 23:54:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B1=95=E7=A4=BA=E9=A2=9D=E5=A4=96=E6=B6=88?= =?UTF-8?q?=E8=80=97=E5=93=81=E6=80=BB=E4=BB=B7=E4=B8=8E=E6=98=8E=E7=BB=86?= =?UTF-8?q?=E6=82=AC=E6=B5=AE=E5=8D=A1=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 租金改为「包含额外物品」;按 resources 收费项汇总额外消耗品金额,悬浮展示物品明细。 --- src/App.tsx | 73 ++++++++++++++++++- src/api.ts | 2 + src/listingDisplay.ts | 86 +++++++++++++++++------ src/styles.css | 160 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 300 insertions(+), 21 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index a76f9dd..a5622dd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,7 @@ 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 Tooltip from "antd/es/tooltip"; import { useEffect, useMemo, useState } from "react"; import { fetchListingsPage, @@ -26,12 +27,14 @@ import { formatListingInfoText, formatMoney, formatRatio, + getConsumablePrice, getListingDeposit, getListingPrice, getMainChips, getOnlineTimeText, getRegions, getResourceQuantity, + getResources, getSkinNames, readAssetNumber, readAssetString, @@ -618,12 +621,17 @@ function ListingRow({ codeCopied, infoCopied, item, onCopyCode, onCopyInfo }: Li const code = formatListingCode(item); const price = getListingPrice(item); const deposit = getListingDeposit(item); + const consumable = getConsumablePrice(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); const fireLevel = readAssetNumber(item, "fire_level"); + const resources = getResources(item); + const resourceTooltip = ( + + ); const details = [ ["哈夫币", formatCoin(item)], ["保险格", readAssetString(item, "season_insurance") || "-"], @@ -675,13 +683,33 @@ function ListingRow({ codeCopied, infoCopied, item, onCopyCode, onCopyInfo }: Li {chip.label} ))} + {resources.length ? 额外{resources.length}项 : null} {remark ? 备注:{remark} : null}
- 租金 ¥{formatMoney(price)} + 租金(包含额外物品) ¥{formatMoney(price)} + + + 额外消耗品 ¥{formatMoney(consumable)} + + 租期 {rentalDuration} @@ -700,6 +728,49 @@ function ListingRow({ codeCopied, infoCopied, item, onCopyCode, onCopyInfo }: Li ); } +function ResourceTooltipContent({ + consumable, + resources, +}: { + consumable: number; + resources: ReturnType; +}) { + if (!resources.length) { + return
暂无额外物品
; + } + + return ( +
+
+
+ 额外物品明细 + {resources.length} 项 +
+ 合计 ¥{formatMoney(consumable)} +
+
+ {resources.map((resource) => ( +
+
+ {resource.label} + ×{resource.quantity} + + {resource.mode || "-"} + +
+
+ {resource.price || "-"} + + {resource.mode === "收费" && resource.amount > 0 ? `¥${formatMoney(resource.amount)}` : "—"} + +
+
+ ))} +
+
+ ); +} + function buildQuery(nextPage: number, filters: Filters, sort: string): PublicListingQuery { return { page: nextPage, diff --git a/src/api.ts b/src/api.ts index b372815..5210562 100644 --- a/src/api.ts +++ b/src/api.ts @@ -10,6 +10,8 @@ export interface ListingResource { price: string; quantity: number; mode: string; + /** 收费项小计(元),赠送为 0 */ + amount: number; } export interface Listing { diff --git a/src/listingDisplay.ts b/src/listingDisplay.ts index cf131f0..d9bb691 100644 --- a/src/listingDisplay.ts +++ b/src/listingDisplay.ts @@ -73,18 +73,64 @@ export function formatRatio(item: Listing) { return `1:${formatRatioNumber(coinWan / price)}`; } +/** 额外消耗品明细(数量 > 0) */ +export function getResources(item: Listing): ListingResource[] { + const resources = item.asset_summary?.resources; + if (!Array.isArray(resources)) return []; + return resources + .filter((resource): resource is Record => typeof resource === "object" && resource !== null) + .map((resource) => { + const quantity = Number(resource.quantity || 0); + const mode = String(resource.mode || ""); + const price = String(resource.price || ""); + const amount = mode === "收费" ? roundMoney(quantity * readUnitPrice(price)) : 0; + return { + key: String(resource.key || ""), + label: String(resource.label || ""), + price, + quantity, + mode, + amount, + }; + }) + .filter((resource) => resource.key && resource.label && resource.quantity > 0); +} + +export function getResourceQuantity(item: Listing, key: string) { + return getResources(item).find((resource) => resource.key === key)?.quantity || 0; +} + +/** 额外消耗品总价(仅收费项合计,元) */ +export function getConsumablePrice(item: Listing) { + return roundMoney(getResources(item).reduce((sum, resource) => sum + resource.amount, 0)); +} + +/** 悬浮/复制用的额外物品明细文案 */ +export function formatResourceDetailLines(item: Listing) { + return getResources(item).map((resource) => { + const unit = resource.price || "-"; + const mode = resource.mode || "-"; + if (resource.mode === "收费" && resource.amount > 0) { + return `${resource.label} × ${resource.quantity}(${mode}) ${unit} = ¥${formatMoney(resource.amount)}`; + } + return `${resource.label} × ${resource.quantity}(${mode}) ${unit}`; + }); +} + /** 复制当前卡片展示的完整信息(非仅编号) */ export function formatListingInfoText(item: Listing) { const code = formatListingCode(item); const title = item.title?.trim() || `纯币${formatCoin(item)}资源号`; const price = getListingPrice(item); const deposit = getListingDeposit(item); + 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 lines = [ `${title} 编号 ${code}`, @@ -104,7 +150,9 @@ export function formatListingInfoText(item: Listing) { fireLevel ? `烽火:${fireLevel}` : "", item.rank_level ? `段位:${item.rank_level}` : "", remark ? `备注:${remark}` : "", - `租金:¥${formatMoney(price)}`, + `租金(包含额外物品):¥${formatMoney(price)}`, + `额外消耗品:¥${formatMoney(consumable)}`, + resourceLines.length ? `额外物品明细:\n${resourceLines.map((line) => ` - ${line}`).join("\n")}` : "", `租期:${formatEstimatedRentalDuration(item)}`, `押金:¥${formatMoney(deposit)}`, `合计:¥${formatMoney(total)}`, @@ -128,25 +176,6 @@ export function readAssetNumber(item: Listing, key: string) { return 0; } -export function getResources(item: Listing): ListingResource[] { - const resources = item.asset_summary?.resources; - if (!Array.isArray(resources)) return []; - return resources - .filter((resource): resource is Record => typeof resource === "object" && resource !== null) - .map((resource) => ({ - key: String(resource.key || ""), - label: String(resource.label || ""), - price: String(resource.price || ""), - quantity: Number(resource.quantity || 0), - mode: String(resource.mode || ""), - })) - .filter((resource) => resource.key && resource.label && resource.quantity > 0); -} - -export function getResourceQuantity(item: Listing, key: string) { - return getResources(item).find((resource) => resource.key === key)?.quantity || 0; -} - export function getSkinNames(item: Listing) { const skinGroups = item.asset_summary?.skin_groups; if (typeof skinGroups !== "object" || skinGroups === null) return []; @@ -188,3 +217,20 @@ function formatRatioNumber(value: number) { const rounded = Math.round(value * 10) / 10; return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1); } + +function roundMoney(value: number) { + return Math.round((Number.isFinite(value) ? value : 0) * 100) / 100; +} + +/** 解析「0.6元/发」「2.5元/个」「3元/张」等单价文案 */ +function readUnitPrice(priceText: string) { + const normalized = priceText.replace(/,/g, ",").trim(); + const fractionMatch = normalized.match(/(\d+(?:\.\d+)?)\s*元\s*\/\s*(\d+(?:\.\d+)?)/); + if (fractionMatch) { + const amount = Number(fractionMatch[1]); + const count = Number(fractionMatch[2]); + return count > 0 ? amount / count : 0; + } + const singleMatch = normalized.match(/(\d+(?:\.\d+)?)\s*元/); + return singleMatch ? Number(singleMatch[1]) : 0; +} diff --git a/src/styles.css b/src/styles.css index a5d15b0..ba87326 100644 --- a/src/styles.css +++ b/src/styles.css @@ -702,6 +702,166 @@ input { color: #ff5b66; } +.price-hint { + cursor: help; + border-bottom: 1px dotted #d0d5dd; + padding-bottom: 1px; + transition: border-color 0.15s ease, color 0.15s ease; +} + +.price-hint:hover { + border-bottom-color: var(--brand); +} + +.price-hint:hover strong { + color: var(--brand-dark); +} + +/* 额外物品悬浮卡片 */ +.resource-tooltip-root .ant-tooltip-container, +.resource-tooltip-root .ant-tooltip-inner { + width: 340px !important; + max-width: min(340px, 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; +} + +.resource-popover { + min-width: 0; + text-align: left; +} + +.resource-popover.empty { + padding: 14px 16px; + color: #98a2b3; + font-size: 13px; + font-weight: 700; +} + +.resource-popover-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 14px; + background: linear-gradient(180deg, #fff8f1, #fff); + border-bottom: 1px solid #f0f2f5; +} + +.resource-popover-head > div { + display: flex; + align-items: baseline; + gap: 8px; + min-width: 0; +} + +.resource-popover-head strong { + color: var(--text-main); + font-size: 13px; + font-weight: 900; +} + +.resource-popover-head span { + color: #98a2b3; + font-size: 12px; + font-weight: 700; +} + +.resource-popover-head em { + flex: none; + color: var(--brand); + font-size: 13px; + font-style: normal; + font-weight: 900; +} + +.resource-popover-list { + max-height: 280px; + overflow: auto; + padding: 6px 0; +} + +.resource-popover-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 8px 14px; +} + +.resource-popover-row:hover { + background: #fafbfc; +} + +.resource-popover-main { + min-width: 0; + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.resource-name { + color: #1f2a37; + font-size: 12px; + font-weight: 800; + white-space: nowrap; +} + +.resource-qty { + color: #667085; + font-size: 12px; + font-weight: 700; + white-space: nowrap; +} + +.resource-mode { + height: 18px; + padding: 0 6px; + border-radius: 999px; + font-size: 11px; + font-weight: 800; + line-height: 18px; + white-space: nowrap; +} + +.resource-mode.paid { + color: var(--brand-dark); + background: var(--brand-soft); +} + +.resource-mode.gift { + color: #027a48; + background: #ecfdf3; +} + +.resource-popover-side { + flex: none; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; +} + +.resource-unit { + color: #98a2b3; + font-size: 11px; + font-weight: 600; + white-space: nowrap; +} + +.resource-amount { + color: var(--brand); + font-size: 12px; + font-weight: 900; + white-space: nowrap; +} + .price-line .ant-btn { margin-left: auto; padding-right: 0;