From 835a7f8d5b5d6f02cd0982b8c326bbac898581fd Mon Sep 17 00:00:00 2001
From: zhaoxing <2558691827@qq.com>
Date: Thu, 12 Nov 2026 14:03:05 +0800
Subject: [PATCH] =?UTF-8?q?feat(listings):=20=E8=AF=A6=E6=83=85=E9=A1=B5?=
=?UTF-8?q?=E4=BB=B7=E6=A0=BC=E6=8B=86=E5=88=86=E6=94=B6=E6=95=9B=E4=B8=BA?=
=?UTF-8?q?=E4=B8=80=E8=A1=8C=E5=B1=95=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/internal/modules/listing/presenter.go | 16 ++++++++
.../features/listings/ListingDetailPage.tsx | 12 +++++-
frontend/src/features/listings/labels.ts | 40 +++++++++++++++++++
.../mobile/MobileListingDetailPage.tsx | 9 ++++-
frontend/src/styles/mobile-listing.css | 9 +++++
.../src/styles/storefront-listing-detail.css | 18 +++++++++
6 files changed, 101 insertions(+), 3 deletions(-)
diff --git a/backend/internal/modules/listing/presenter.go b/backend/internal/modules/listing/presenter.go
index 9a07f8b..3a28afb 100644
--- a/backend/internal/modules/listing/presenter.go
+++ b/backend/internal/modules/listing/presenter.go
@@ -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 {
diff --git a/frontend/src/features/listings/ListingDetailPage.tsx b/frontend/src/features/listings/ListingDetailPage.tsx
index 93b8633..e3f65d0 100644
--- a/frontend/src/features/listings/ListingDetailPage.tsx
+++ b/frontend/src/features/listings/ListingDetailPage.tsx
@@ -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() {
总租金
-
{money(data.price_cent)}
+
+ {money(data.price_cent)}
+ {priceDetail ? {priceDetail} : null}
+
押金 {money(data.deposit_amount_cent)} · 合计 {money(totalCent)}
@@ -589,7 +594,10 @@ export function ListingDetailPage() {
{data.title}
-
{money(data.price_cent)}
+
+ {money(data.price_cent)}
+ {priceDetail ? {priceDetail} : null}
+
{money(data.deposit_amount_cent)}
{formatCoinM(data.haf_coin_amount)}
diff --git a/frontend/src/features/listings/labels.ts b/frontend/src/features/listings/labels.ts
index ba13093..76343ec 100644
--- a/frontend/src/features/listings/labels.ts
+++ b/frontend/src/features/listings/labels.ts
@@ -115,6 +115,46 @@ export function gameNameChangeText(summary?: Record | null): st
return text
}
+export function listingPriceBreakdown(item: {
+ price_cent: number
+ asset_summary?: Record
+}) {
+ 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
+ },
+ 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
diff --git a/frontend/src/features/mobile/MobileListingDetailPage.tsx b/frontend/src/features/mobile/MobileListingDetailPage.tsx
index cca41d8..92d00ba 100644
--- a/frontend/src/features/mobile/MobileListingDetailPage.tsx
+++ b/frontend/src/features/mobile/MobileListingDetailPage.tsx
@@ -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() {
租金
{money(data.price_cent)}
+ {priceDetail ? (
+ {priceDetail}
+ ) : null}
押金
diff --git a/frontend/src/styles/mobile-listing.css b/frontend/src/styles/mobile-listing.css
index a663c20..058b920 100644
--- a/frontend/src/styles/mobile-listing.css
+++ b/frontend/src/styles/mobile-listing.css
@@ -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;
}
diff --git a/frontend/src/styles/storefront-listing-detail.css b/frontend/src/styles/storefront-listing-detail.css
index 2708512..b2dd298 100644
--- a/frontend/src/styles/storefront-listing-detail.css
+++ b/frontend/src/styles/storefront-listing-detail.css
@@ -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));