feat(listings): 详情页价格拆分收敛为一行展示
This commit is contained in:
@@ -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="号主">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user