feat(listings): 演示商品数据与哈夫币展示

This commit is contained in:
yml2213
2026-10-31 11:52:39 +08:00
parent cfafc21014
commit 5f9ad08347
8 changed files with 169 additions and 11 deletions
@@ -13,7 +13,7 @@ import {
} from '@/features/listings/collectionsApi'
import { useListingCollections } from '@/features/listings/collections'
import { useSessionStore } from '@/stores/session'
import { formatDateTime, formatMoney } from '@/shared/utils/format'
import { formatCoinM, formatDateTime, formatMoney } from '@/shared/utils/format'
type TabKey = 'history' | 'favorite'
@@ -218,7 +218,7 @@ export function ListingCollectionsPage() {
</Tag>
</Space>
<div className="collections-item__meta">
<span>哈夫币 {item.listing.haf_coin_amount?.toLocaleString() ?? '—'}</span>
<span>哈夫币 {formatCoinM(item.listing.haf_coin_amount)}</span>
<span>押金 {formatMoney(item.listing.deposit_amount_cent)}</span>
</div>
</div>
@@ -42,6 +42,7 @@ import {
usePostRentalNotice,
} from '@/features/orders/api'
import { useSessionStore } from '@/stores/session'
import { formatCoinM } from '@/shared/utils/format'
import type { Listing } from '@/shared/api/types'
function money(cents: number) {
@@ -285,7 +286,7 @@ export function ListingDetailPage() {
</div>
<div>
<span>哈夫币</span>
<strong>{data.haf_coin_amount.toLocaleString()} M</strong>
<strong>{formatCoinM(data.haf_coin_amount)}</strong>
</div>
<div>
<span>号主</span>
@@ -444,7 +445,7 @@ export function ListingDetailPage() {
<Descriptions.Item label="租金">{money(data.price_cent)}</Descriptions.Item>
<Descriptions.Item label="押金">{money(data.deposit_amount_cent)}</Descriptions.Item>
<Descriptions.Item label="哈夫币">
{data.haf_coin_amount.toLocaleString()} M
{formatCoinM(data.haf_coin_amount)}
</Descriptions.Item>
<Descriptions.Item label="号主">
{data.owner_nickname || `#${data.owner_id}`}
@@ -42,6 +42,7 @@ import {
type ListingFilterState,
} from '@/features/listings/ListingFilterPanel'
import { fetchListingPublishOptions } from '@/features/seller/options'
import { formatCoinM } from '@/shared/utils/format'
import type { Listing } from '@/shared/api/types'
const PAGE_SIZE = 20
@@ -79,9 +80,37 @@ function assetText(item: Listing, key: string) {
}
function ratioText(item: Listing) {
const published = Number(item.asset_summary?.publish_ratio ?? 0)
if (published > 0) return `${published}w`
const priceYuan = item.price_cent / 100
if (priceYuan <= 0 || !item.haf_coin_amount) return ''
return `${((item.haf_coin_amount / priceYuan) * 100).toFixed(0)}w`
const coinM = item.haf_coin_amount / 1_000_000
if (priceYuan <= 0 || coinM <= 0) return ''
return `${((coinM / priceYuan) * 100).toFixed(0)}w`
}
function skinNames(item: Listing): string[] {
const groups = item.asset_summary?.skin_groups
if (!groups || typeof groups !== 'object') return []
const names: string[] = []
Object.values(groups as Record<string, unknown>).forEach(value => {
if (!Array.isArray(value)) return
value.forEach(name => {
if (typeof name === 'string' && name) names.push(name)
})
})
return Array.from(new Set(names)).slice(0, 8)
}
function resourceItems(item: Listing): { label: string; quantity: number }[] {
const raw = item.asset_summary?.resources
if (!Array.isArray(raw)) return []
return raw
.map(entry => {
const row = (entry ?? {}) as Record<string, unknown>
return { label: String(row.label ?? row.key ?? ''), quantity: Number(row.quantity ?? 0) }
})
.filter(row => row.label && row.quantity > 0)
.slice(0, 6)
}
function ListingCover({ item }: { item: Listing }) {
@@ -102,7 +131,7 @@ function ListingCover({ item }: { item: Listing }) {
function ListingRow({ item }: { item: Listing }) {
const specs = [
{ label: '哈夫币', value: `${item.haf_coin_amount.toLocaleString()}M` },
{ label: '哈夫币', value: formatCoinM(item.haf_coin_amount) },
{ label: '保险', value: assetText(item, 'season_insurance') },
{ label: '体力', value: assetText(item, 'stamina_level') },
{ label: '负重', value: assetText(item, 'load_level') },
@@ -111,6 +140,8 @@ function ListingRow({ item }: { item: Listing }) {
{ label: '区服', value: item.server_region },
{ label: '折算比', value: ratioText(item) ? `1元≈${ratioText(item)}` : '' },
].filter(spec => spec.value)
const skins = skinNames(item)
const resources = resourceItems(item)
return (
<article className="listing-row">
@@ -145,6 +176,26 @@ function ListingRow({ item }: { item: Listing }) {
</span>
))}
</div>
{skins.length > 0 ? (
<div className="listing-row__skins">
<em>精选皮肤</em>
{skins.map(name => (
<span key={name}>{name}</span>
))}
</div>
) : null}
{resources.length > 0 ? (
<div className="listing-row__resources">
<em>仓库物资</em>
{resources.map(row => (
<span key={row.label}>
{row.label} {row.quantity}
</span>
))}
</div>
) : null}
</div>
<div className="listing-row__side">
@@ -194,7 +245,7 @@ function ListingGridCard({ item }: { item: Listing }) {
{item.rank_level ? <Tag bordered={false}>{item.rank_level}</Tag> : null}
</div>
<div className="listing-card__meta">
<span>哈夫币 {item.haf_coin_amount.toLocaleString()}</span>
<span>哈夫币 {formatCoinM(item.haf_coin_amount)}</span>
<span>{item.owner_nickname || '商行认证号主'}</span>
</div>
<div className="listing-card__price-row">
@@ -14,6 +14,7 @@ import {
type ListingFilterState,
} from '@/features/listings/ListingFilterPanel'
import { fetchListingPublishOptions } from '@/features/seller/options'
import { formatCoinM } from '@/shared/utils/format'
import type { Listing } from '@/shared/api/types'
const zoneTabs = [
@@ -104,7 +105,7 @@ function MobileListingCard({ item }: { item: Listing }) {
<div className="mobile-listing-card__tags">
<span>{item.server_region || '区服待定'}</span>
<span>{item.rank_level || '段位待定'}</span>
<span>哈夫币 {item.haf_coin_amount.toLocaleString()}</span>
<span>哈夫币 {formatCoinM(item.haf_coin_amount)}</span>
{specs.map(spec => (
<span key={spec}>{spec}</span>
))}
@@ -12,6 +12,7 @@ import {
usePostRentalNotice,
} from '@/features/orders/api'
import { useSessionStore } from '@/stores/session'
import { formatCoinM } from '@/shared/utils/format'
function money(cents: number) {
return `¥${(cents / 100).toFixed(2)}`
@@ -184,7 +185,7 @@ export function MobileListingDetailPage() {
</div>
<div>
<span>哈夫币</span>
<strong>{data.haf_coin_amount.toLocaleString()}</strong>
<strong>{formatCoinM(data.haf_coin_amount)}</strong>
</div>
</section>
@@ -267,7 +268,7 @@ export function MobileListingDetailPage() {
</div>
<div>
<span>哈夫币</span>
<strong>{data.haf_coin_amount.toLocaleString()} M</strong>
<strong>{formatCoinM(data.haf_coin_amount)}</strong>
</div>
<div>
<span>号主</span>
+8
View File
@@ -11,6 +11,14 @@ export function formatMoney(cents?: number | null) {
})}`
}
/** 哈夫币以原始数量入库,展示时统一换算为 M(百万)。 */
export function formatCoinM(amount?: number | null) {
if (amount == null || Number.isNaN(amount)) return EMPTY
const m = amount / 1_000_000
const rounded = Math.round(m * 10) / 10
return `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)}M`
}
export function formatDateTime(value?: string | null) {
if (!value) return EMPTY
return dayjs(value).format('YYYY-MM-DD HH:mm')
@@ -263,6 +263,38 @@
font-style: normal;
}
.listing-row__skins,
.listing-row__resources {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 10px;
margin-top: 8px;
font-size: 12px;
color: var(--ink-secondary);
}
.listing-row__skins em,
.listing-row__resources em {
flex: none;
color: var(--ink-muted);
font-style: normal;
}
.listing-row__skins span {
padding: 2px 8px;
border-radius: 999px;
background: rgba(166, 67, 50, 0.08);
color: var(--brand-seal);
}
.listing-row__resources span {
padding: 2px 8px;
border-radius: 6px;
background: #f1f0e9;
color: var(--ink-secondary);
}
.listing-row__side {
display: flex;
flex-direction: column;