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));