展示额外消耗品总价与明细悬浮卡片
租金改为「包含额外物品」;按 resources 收费项汇总额外消耗品金额,悬浮展示物品明细。
This commit is contained in:
+72
-1
@@ -11,6 +11,7 @@ import Pagination from "antd/es/pagination";
|
|||||||
import Select from "antd/es/select";
|
import Select from "antd/es/select";
|
||||||
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 { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
fetchListingsPage,
|
fetchListingsPage,
|
||||||
@@ -26,12 +27,14 @@ import {
|
|||||||
formatListingInfoText,
|
formatListingInfoText,
|
||||||
formatMoney,
|
formatMoney,
|
||||||
formatRatio,
|
formatRatio,
|
||||||
|
getConsumablePrice,
|
||||||
getListingDeposit,
|
getListingDeposit,
|
||||||
getListingPrice,
|
getListingPrice,
|
||||||
getMainChips,
|
getMainChips,
|
||||||
getOnlineTimeText,
|
getOnlineTimeText,
|
||||||
getRegions,
|
getRegions,
|
||||||
getResourceQuantity,
|
getResourceQuantity,
|
||||||
|
getResources,
|
||||||
getSkinNames,
|
getSkinNames,
|
||||||
readAssetNumber,
|
readAssetNumber,
|
||||||
readAssetString,
|
readAssetString,
|
||||||
@@ -618,12 +621,17 @@ function ListingRow({ codeCopied, infoCopied, item, onCopyCode, onCopyInfo }: Li
|
|||||||
const code = formatListingCode(item);
|
const code = formatListingCode(item);
|
||||||
const price = getListingPrice(item);
|
const price = getListingPrice(item);
|
||||||
const deposit = getListingDeposit(item);
|
const deposit = getListingDeposit(item);
|
||||||
|
const consumable = getConsumablePrice(item);
|
||||||
const totalPrice = Math.round((price + deposit) * 100) / 100;
|
const totalPrice = Math.round((price + deposit) * 100) / 100;
|
||||||
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 skinNames = getSkinNames(item);
|
||||||
const fireLevel = readAssetNumber(item, "fire_level");
|
const fireLevel = readAssetNumber(item, "fire_level");
|
||||||
|
const resources = getResources(item);
|
||||||
|
const resourceTooltip = (
|
||||||
|
<ResourceTooltipContent consumable={consumable} resources={resources} />
|
||||||
|
);
|
||||||
const details = [
|
const details = [
|
||||||
["哈夫币", formatCoin(item)],
|
["哈夫币", formatCoin(item)],
|
||||||
["保险格", readAssetString(item, "season_insurance") || "-"],
|
["保险格", readAssetString(item, "season_insurance") || "-"],
|
||||||
@@ -675,13 +683,33 @@ function ListingRow({ codeCopied, infoCopied, item, onCopyCode, onCopyInfo }: Li
|
|||||||
{chip.label}
|
{chip.label}
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
|
{resources.length ? <Tag color="orange">额外{resources.length}项</Tag> : null}
|
||||||
{remark ? <span className="remark">备注:{remark}</span> : null}
|
{remark ? <span className="remark">备注:{remark}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="price-line">
|
<div className="price-line">
|
||||||
<span>
|
<span>
|
||||||
租金 <strong>¥{formatMoney(price)}</strong>
|
租金(包含额外物品) <strong>¥{formatMoney(price)}</strong>
|
||||||
</span>
|
</span>
|
||||||
|
<Tooltip
|
||||||
|
arrow={false}
|
||||||
|
placement="top"
|
||||||
|
rootClassName="resource-tooltip-root"
|
||||||
|
styles={{
|
||||||
|
container: {
|
||||||
|
padding: 0,
|
||||||
|
background: "#fff",
|
||||||
|
color: "#172033",
|
||||||
|
borderRadius: 12,
|
||||||
|
boxShadow: "0 12px 32px rgba(21, 32, 43, 0.14)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
title={resourceTooltip}
|
||||||
|
>
|
||||||
|
<span className="price-hint">
|
||||||
|
额外消耗品 <strong>¥{formatMoney(consumable)}</strong>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
<span>
|
<span>
|
||||||
租期 <strong className="rental-days">{rentalDuration}</strong>
|
租期 <strong className="rental-days">{rentalDuration}</strong>
|
||||||
</span>
|
</span>
|
||||||
@@ -700,6 +728,49 @@ function ListingRow({ codeCopied, infoCopied, item, onCopyCode, onCopyInfo }: Li
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ResourceTooltipContent({
|
||||||
|
consumable,
|
||||||
|
resources,
|
||||||
|
}: {
|
||||||
|
consumable: number;
|
||||||
|
resources: ReturnType<typeof getResources>;
|
||||||
|
}) {
|
||||||
|
if (!resources.length) {
|
||||||
|
return <div className="resource-popover empty">暂无额外物品</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="resource-popover">
|
||||||
|
<div className="resource-popover-head">
|
||||||
|
<div>
|
||||||
|
<strong>额外物品明细</strong>
|
||||||
|
<span>{resources.length} 项</span>
|
||||||
|
</div>
|
||||||
|
<em>合计 ¥{formatMoney(consumable)}</em>
|
||||||
|
</div>
|
||||||
|
<div className="resource-popover-list">
|
||||||
|
{resources.map((resource) => (
|
||||||
|
<div key={resource.key} className="resource-popover-row">
|
||||||
|
<div className="resource-popover-main">
|
||||||
|
<span className="resource-name">{resource.label}</span>
|
||||||
|
<span className="resource-qty">×{resource.quantity}</span>
|
||||||
|
<span className={resource.mode === "收费" ? "resource-mode paid" : "resource-mode gift"}>
|
||||||
|
{resource.mode || "-"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="resource-popover-side">
|
||||||
|
<span className="resource-unit">{resource.price || "-"}</span>
|
||||||
|
<strong className="resource-amount">
|
||||||
|
{resource.mode === "收费" && resource.amount > 0 ? `¥${formatMoney(resource.amount)}` : "—"}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function buildQuery(nextPage: number, filters: Filters, sort: string): PublicListingQuery {
|
function buildQuery(nextPage: number, filters: Filters, sort: string): PublicListingQuery {
|
||||||
return {
|
return {
|
||||||
page: nextPage,
|
page: nextPage,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export interface ListingResource {
|
|||||||
price: string;
|
price: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
mode: string;
|
mode: string;
|
||||||
|
/** 收费项小计(元),赠送为 0 */
|
||||||
|
amount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Listing {
|
export interface Listing {
|
||||||
|
|||||||
+66
-20
@@ -73,18 +73,64 @@ export function formatRatio(item: Listing) {
|
|||||||
return `1:${formatRatioNumber(coinWan / price)}`;
|
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<string, unknown> => 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) {
|
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)}资源号`;
|
||||||
const price = getListingPrice(item);
|
const price = getListingPrice(item);
|
||||||
const deposit = getListingDeposit(item);
|
const deposit = getListingDeposit(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 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 lines = [
|
const lines = [
|
||||||
`${title} 编号 ${code}`,
|
`${title} 编号 ${code}`,
|
||||||
@@ -104,7 +150,9 @@ export function formatListingInfoText(item: Listing) {
|
|||||||
fireLevel ? `烽火:${fireLevel}` : "",
|
fireLevel ? `烽火:${fireLevel}` : "",
|
||||||
item.rank_level ? `段位:${item.rank_level}` : "",
|
item.rank_level ? `段位:${item.rank_level}` : "",
|
||||||
remark ? `备注:${remark}` : "",
|
remark ? `备注:${remark}` : "",
|
||||||
`租金:¥${formatMoney(price)}`,
|
`租金(包含额外物品):¥${formatMoney(price)}`,
|
||||||
|
`额外消耗品:¥${formatMoney(consumable)}`,
|
||||||
|
resourceLines.length ? `额外物品明细:\n${resourceLines.map((line) => ` - ${line}`).join("\n")}` : "",
|
||||||
`租期:${formatEstimatedRentalDuration(item)}`,
|
`租期:${formatEstimatedRentalDuration(item)}`,
|
||||||
`押金:¥${formatMoney(deposit)}`,
|
`押金:¥${formatMoney(deposit)}`,
|
||||||
`合计:¥${formatMoney(total)}`,
|
`合计:¥${formatMoney(total)}`,
|
||||||
@@ -128,25 +176,6 @@ export function readAssetNumber(item: Listing, key: string) {
|
|||||||
return 0;
|
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<string, unknown> => 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) {
|
export function getSkinNames(item: Listing) {
|
||||||
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 [];
|
||||||
@@ -188,3 +217,20 @@ function formatRatioNumber(value: number) {
|
|||||||
const rounded = Math.round(value * 10) / 10;
|
const rounded = Math.round(value * 10) / 10;
|
||||||
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
+160
@@ -702,6 +702,166 @@ input {
|
|||||||
color: #ff5b66;
|
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 {
|
.price-line .ant-btn {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user