feat(listings): 详情页价格拆分收敛为一行展示

This commit is contained in:
2026-11-12 14:03:05 +08:00
parent f1c0cb3730
commit 835a7f8d5b
6 changed files with 101 additions and 3 deletions
@@ -78,12 +78,28 @@ func applyPublicListingURLs(item *ListingDTO) {
rawScreenshotURLs := item.ScreenshotURLS
item.ScreenshotURLS = publicScreenshotURLs(item.ID, item.ScreenshotURLS, item.Status, item.ReviewStatus)
if item.AssetSummary != nil {
applyPublicPriceBreakdown(item.AssetSummary)
delete(item.AssetSummary, "import_meta")
delete(item.AssetSummary, "price_breakdown")
applyPublicScreenshotGroups(item.ID, item.AssetSummary, rawScreenshotURLs, item.ScreenshotURLS)
}
}
// applyPublicPriceBreakdown 把买家侧的纯币租金和额外物资摊平到资产摘要顶层。
// 公共接口随后会删除完整的 price_breakdown,避免暴露卖家成本和平台加价。
func applyPublicPriceBreakdown(summary map[string]any) {
breakdown, ok := summary["price_breakdown"].(map[string]any)
if !ok {
return
}
if pureCoinCent := summaryMoneyCent(breakdown, "buyer_coin_base_price_cent", "buyer_coin_base_price"); pureCoinCent > 0 {
summary["pure_coin_price_cent"] = pureCoinCent
}
if extraItemCent := summaryMoneyCent(breakdown, "consumable_price_cent", "consumable_price"); extraItemCent > 0 {
summary["extra_item_price_cent"] = extraItemCent
}
}
func applyPublicScreenshotGroups(listingID uint64, assetSummary map[string]any, rawURLs []string, publicURLs []string) {
rawGroups, ok := assetSummary["screenshot_groups"].(map[string]any)
if !ok {
@@ -28,6 +28,7 @@ import {
banRecordText,
faceOwnerText,
gameNameChangeText,
listingPriceDetail,
listingRentalDays,
} from '@/features/listings/labels'
import { recordListingView, useListingCollections } from '@/features/listings/collections'
@@ -294,6 +295,7 @@ export function ListingDetailPage() {
const isOwner = user?.id === data.owner_id
const favorited = token ? Boolean(favoriteIDsQuery.data?.has(data.id)) : isFavorite(data.id)
const totalCent = data.price_cent + data.deposit_amount_cent
const priceDetail = listingPriceDetail(data)
const toggleFavoriteState = async () => {
if (token) {
@@ -396,7 +398,10 @@ export function ListingDetailPage() {
<div className="ld-price">
<span>总租金</span>
<strong>{money(data.price_cent)}</strong>
<strong>
{money(data.price_cent)}
{priceDetail ? <small className="ld-price__detail">{priceDetail}</small> : null}
</strong>
<em>
押金 {money(data.deposit_amount_cent)} · 合计 {money(totalCent)}
</em>
@@ -589,7 +594,10 @@ export function ListingDetailPage() {
<Descriptions.Item label="商品" span={2}>
{data.title}
</Descriptions.Item>
<Descriptions.Item label="租金">{money(data.price_cent)}</Descriptions.Item>
<Descriptions.Item label="租金">
{money(data.price_cent)}
{priceDetail ? <small className="ld-modal-detail">{priceDetail}</small> : null}
</Descriptions.Item>
<Descriptions.Item label="押金">{money(data.deposit_amount_cent)}</Descriptions.Item>
<Descriptions.Item label="哈夫币">{formatCoinM(data.haf_coin_amount)}</Descriptions.Item>
<Descriptions.Item label="号主">
+40
View File
@@ -115,6 +115,46 @@ export function gameNameChangeText(summary?: Record<string, unknown> | null): st
return text
}
export function listingPriceBreakdown(item: {
price_cent: number
asset_summary?: Record<string, unknown>
}) {
const summary = item.asset_summary ?? {}
const pureCoinCent = Number(summary.pure_coin_price_cent ?? 0)
const extraItemCent = Number(summary.extra_item_price_cent ?? 0)
return {
pureCoinCent: Number.isFinite(pureCoinCent) ? pureCoinCent : 0,
extraItemCent: Number.isFinite(extraItemCent) ? extraItemCent : 0,
hasBreakdown: pureCoinCent > 0 || extraItemCent > 0,
}
}
export function listingPriceDetail(
item: {
price_cent: number
asset_summary?: Record<string, unknown>
},
options?: { compact?: boolean }
): string {
const { pureCoinCent, extraItemCent } = listingPriceBreakdown(item)
const pureLabel = options?.compact ? '纯币' : '纯币租金'
const extraLabel = options?.compact ? '物资' : '额外物资'
const parts = [
pureCoinCent > 0 ? `${pureLabel} ¥${formatCentAmount(pureCoinCent)}` : '',
extraItemCent > 0 ? `${extraLabel} ¥${formatCentAmount(extraItemCent)}` : '',
].filter(Boolean)
if (!parts.length) return ''
const text = parts.join(' + ')
return options?.compact ? text : `(${text})`
}
function formatCentAmount(cent: number) {
return (cent / 100).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
}
export function listingRentalDays(item: {
haf_coin_amount: number
asset_summary?: Record<string, unknown>
@@ -9,6 +9,7 @@ import {
banRecordText,
faceOwnerText,
gameNameChangeText,
listingPriceDetail,
listingRentalDays,
} from '@/features/listings/labels'
import { ensureSupportConversation } from '@/features/messages/api'
@@ -154,12 +155,15 @@ export function MobileListingDetailPage() {
key !== 'face_owner' &&
key !== 'ban_record' &&
key !== 'can_change_game_name' &&
key !== 'modifiable_name'
key !== 'modifiable_name' &&
key !== 'pure_coin_price_cent' &&
key !== 'extra_item_price_cent'
)
const rentalDays = listingRentalDays(data)
const faceOwner = faceOwnerText(data.asset_summary)
const banRecord = banRecordText(data.asset_summary)
const gameNameChange = gameNameChangeText(data.asset_summary)
const priceDetail = listingPriceDetail(data, { compact: true })
const isOwner = user?.id === data.owner_id
return (
@@ -193,6 +197,9 @@ export function MobileListingDetailPage() {
<div>
<span>租金</span>
<strong>{money(data.price_cent)}</strong>
{priceDetail ? (
<small className="mobile-detail__price-detail">{priceDetail}</small>
) : null}
</div>
<div>
<span>押金</span>
+9
View File
@@ -102,6 +102,15 @@
font-size: 18px;
}
.mobile-detail__price-detail {
display: block;
margin-top: 3px;
color: var(--ink-muted);
font-size: 9px;
font-weight: 400;
line-height: 1.4;
}
.mobile-detail__section {
padding: 20px 0 0;
}
@@ -196,6 +196,24 @@
font-style: normal;
}
.ld-price__detail {
display: inline-block;
margin-left: 8px;
color: var(--ink-muted);
font-family: inherit;
font-size: 12px;
font-weight: 400;
letter-spacing: 0;
vertical-align: baseline;
}
.ld-modal-detail {
display: block;
margin-top: 2px;
color: var(--ink-muted);
font-size: 12px;
}
.ld-quick {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));