From 5f9ad08347db53b2b00926ac5dabefe6b59af2ec Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 31 Oct 2026 11:45:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(listings):=20=E6=BC=94=E7=A4=BA=E5=95=86?= =?UTF-8?q?=E5=93=81=E6=95=B0=E6=8D=AE=E4=B8=8E=E5=93=88=E5=A4=AB=E5=B8=81?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/scripts/seed_demo_listings.sql | 64 +++++++++++++++++++ .../listings/ListingCollectionsPage.tsx | 4 +- .../features/listings/ListingDetailPage.tsx | 5 +- .../src/features/listings/ListingListPage.tsx | 59 +++++++++++++++-- .../src/features/mobile/MobileHomePage.tsx | 3 +- .../mobile/MobileListingDetailPage.tsx | 5 +- frontend/src/shared/utils/format.ts | 8 +++ frontend/src/styles/storefront-filters.css | 32 ++++++++++ 8 files changed, 169 insertions(+), 11 deletions(-) create mode 100644 backend/scripts/seed_demo_listings.sql diff --git a/backend/scripts/seed_demo_listings.sql b/backend/scripts/seed_demo_listings.sql new file mode 100644 index 0000000..bba8f2a --- /dev/null +++ b/backend/scripts/seed_demo_listings.sql @@ -0,0 +1,64 @@ +-- 本地演示数据:4 条已上架商品(仅用于开发环境,禁止在生产执行)。 +-- 依赖:MySQL 8;phone 前缀 138000090 用于重复执行时清理。 + +DELETE l FROM rental_listings l JOIN users u ON u.id = l.owner_id WHERE u.phone LIKE '138000090%'; +DELETE ga FROM game_accounts ga JOIN users u ON u.id = ga.owner_id WHERE u.phone LIKE '138000090%'; +DELETE FROM users WHERE phone LIKE '138000090%'; + +INSERT INTO users (phone, password_hash, nickname, realname_status, status) VALUES +('13800009001', '$2a$10$demo.demo.demo.demo.demo.demo.demo.demo.demo.demo', '号主·老白', 'verified', 'active'), +('13800009002', '$2a$10$demo.demo.demo.demo.demo.demo.demo.demo.demo.demo', '号主·阿岚', 'verified', 'active'), +('13800009003', '$2a$10$demo.demo.demo.demo.demo.demo.demo.demo.demo.demo', '号主·北屿', 'verified', 'active'), +('13800009004', '$2a$10$demo.demo.demo.demo.demo.demo.demo.demo.demo.demo', '号主·临江', 'verified', 'active'); + +-- 1) 53M / 3*3 / 黑鹰 +SET @u1 = (SELECT id FROM users WHERE phone = '13800009001'); +INSERT INTO game_accounts + (owner_id, game_name, server_region, login_platform, title, description, rank_level, haf_coin_amount, asset_summary, screenshot_urls, status) +VALUES + (@u1, 'delta_force', 'QQ', '扫码', '解忧柜选 · 三角洲行动 高资产账号', + '哈夫币 53M,保险 3*3,刀皮坠星者,红皮四件,适合稳定上分与搬砖。', '黑鹰', 53000000, + CAST('{"season_insurance":"3*3","stamina_level":"7级","load_level":"7级","fire_level":60,"daily_loss_m":10,"secret_kd":3.2,"early_morning_response":"是","modifiable_name":"是","all_heroes":"是","online_time":{"start":"12点","end":"次日02点"},"skin_groups":{"melee":["坠星者"],"operatorRed":["红狼-电锯惊魂","蜂医-危险物质","蜂医-迷葬人","无名-夜鹰"],"operatorGold":["蚀金玫瑰"],"weapon":["AKM-群星"]},"publish_ratio":35,"resources":[{"key":"level6Ammo","label":"6级弹","quantity":99,"mode":"付费"},{"key":"awmAmmo","label":"AWM子弹","quantity":6,"mode":"付费"},{"key":"armor6","label":"6甲","quantity":8,"mode":"付费"},{"key":"helmet6","label":"6头","quantity":8,"mode":"付费"},{"key":"coffeeBean","label":"高级咖啡豆","quantity":23,"mode":"付费"}]}' AS JSON), + CAST('["https://picsum.photos/seed/jieyou-1/960/600"]' AS JSON), 'published'); +SET @a1 = LAST_INSERT_ID(); +INSERT INTO rental_listings (listing_no, account_id, owner_id, price_cent, deposit_amount_cent, status, review_status, published_at) +VALUES ('202610310001', @a1, @u1, 25580, 75000, 'published', 'approved', NOW() - INTERVAL 6 MINUTE); + +-- 2) 800M / 2*3 / 铂金(含赠送物资,进入赠送专区) +SET @u2 = (SELECT id FROM users WHERE phone = '13800009002'); +INSERT INTO game_accounts + (owner_id, game_name, server_region, login_platform, title, description, rank_level, haf_coin_amount, asset_summary, screenshot_urls, status) +VALUES + (@u2, 'delta_force', '微信', '扫码', '解忧柜选 · 800M 顶配红皮账号', + '哈夫币 800M,红皮与金皮齐全,适合高强度对局与直播。', '铂金', 800000000, + CAST('{"season_insurance":"2*3","stamina_level":"7级","load_level":"7级","fire_level":62,"daily_loss_m":20,"secret_kd":4.1,"early_morning_response":"是","modifiable_name":"否","all_heroes":"是","online_time":{"start":"13点","end":"次日01点"},"skin_groups":{"melee":["暗星"],"operatorRed":["红狼-电锯惊魂","蜂医-危险物质","威龙-吴彦祖","疾风-西部往事"],"operatorGold":["露娜-金牌射手"],"weapon":["AKM-群星"]},"publish_ratio":46,"resources":[{"key":"level6Ammo","label":"6级弹","quantity":217,"mode":"付费"},{"key":"armor6","label":"6甲","quantity":6,"mode":"付费"},{"key":"helmet6","label":"6头","quantity":5,"mode":"付费"},{"key":"gridCard9","label":"9格体验卡","quantity":2,"mode":"赠送"}]}' AS JSON), + CAST('["https://picsum.photos/seed/jieyou-2/960/600"]' AS JSON), 'published'); +SET @a2 = LAST_INSERT_ID(); +INSERT INTO rental_listings (listing_no, account_id, owner_id, price_cent, deposit_amount_cent, status, review_status, published_at) +VALUES ('202610310002', @a2, @u2, 191350, 30000, 'published', 'approved', NOW() - INTERVAL 5 MINUTE); + +-- 3) 220M / 3*3 / 黑鹰 +SET @u3 = (SELECT id FROM users WHERE phone = '13800009003'); +INSERT INTO game_accounts + (owner_id, game_name, server_region, login_platform, title, description, rank_level, haf_coin_amount, asset_summary, screenshot_urls, status) +VALUES + (@u3, 'delta_force', 'QQ', '扫码', '解忧柜选 · 220M 性价比账号', + '哈夫币 220M,AWM 子弹充足,适合狙击位玩家。', '黑鹰', 220000000, + CAST('{"season_insurance":"3*3","stamina_level":"7级","load_level":"7级","fire_level":58,"daily_loss_m":15,"secret_kd":2.8,"early_morning_response":"是","modifiable_name":"否","all_heroes":"是","online_time":{"start":"08点","end":"次日01点"},"skin_groups":{"melee":["北极星"],"operatorRed":["红狼-电锯惊魂","维什戴尔"],"weapon":["AKM-群星"]},"publish_ratio":40,"resources":[{"key":"awmAmmo","label":"AWM子弹","quantity":77,"mode":"付费"},{"key":"armor6","label":"6甲","quantity":1,"mode":"付费"},{"key":"helmet6","label":"6头","quantity":2,"mode":"付费"}]}' AS JSON), + CAST('["https://picsum.photos/seed/jieyou-3/960/600"]' AS JSON), 'published'); +SET @a3 = LAST_INSERT_ID(); +INSERT INTO rental_listings (listing_no, account_id, owner_id, price_cent, deposit_amount_cent, status, review_status, published_at) +VALUES ('202610310003', @a3, @u3, 60940, 50000, 'published', 'approved', NOW() - INTERVAL 4 MINUTE); + +-- 4) 508M / 3*3 / 黑鹰 / 账密登录 +SET @u4 = (SELECT id FROM users WHERE phone = '13800009004'); +INSERT INTO game_accounts + (owner_id, game_name, server_region, login_platform, title, description, rank_level, haf_coin_amount, asset_summary, screenshot_urls, status) +VALUES + (@u4, 'delta_force', 'Steam', '账号密码', '解忧柜选 · 508M 账密直登账号', + '哈夫币 508M,支持账密直登,仓库物资齐全。', '黑鹰', 508000000, + CAST('{"season_insurance":"3*3","stamina_level":"7级","load_level":"7级","fire_level":60,"daily_loss_m":12,"secret_kd":3.6,"early_morning_response":"是","modifiable_name":"是","all_heroes":"是","online_time":{"start":"全天","end":"全天"},"skin_groups":{"melee":["重塑"],"operatorRed":["红狼-电锯惊魂"],"weapon":["AKM-群星"]},"publish_ratio":46,"resources":[{"key":"awmAmmo","label":"AWM子弹","quantity":50,"mode":"付费"},{"key":"armor6","label":"6甲","quantity":7,"mode":"付费"},{"key":"helmet6","label":"6头","quantity":5,"mode":"付费"},{"key":"coffeeBean","label":"高级咖啡豆","quantity":3,"mode":"付费"}]}' AS JSON), + CAST('["https://picsum.photos/seed/jieyou-4/960/600"]' AS JSON), 'published'); +SET @a4 = LAST_INSERT_ID(); +INSERT INTO rental_listings (listing_no, account_id, owner_id, price_cent, deposit_amount_cent, status, review_status, published_at) +VALUES ('202610310004', @a4, @u4, 117180, 60000, 'published', 'approved', NOW() - INTERVAL 3 MINUTE); diff --git a/frontend/src/features/listings/ListingCollectionsPage.tsx b/frontend/src/features/listings/ListingCollectionsPage.tsx index 376e21c..66a9472 100644 --- a/frontend/src/features/listings/ListingCollectionsPage.tsx +++ b/frontend/src/features/listings/ListingCollectionsPage.tsx @@ -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() {
- 哈夫币 {item.listing.haf_coin_amount?.toLocaleString() ?? '—'} + 哈夫币 {formatCoinM(item.listing.haf_coin_amount)} 押金 {formatMoney(item.listing.deposit_amount_cent)}
diff --git a/frontend/src/features/listings/ListingDetailPage.tsx b/frontend/src/features/listings/ListingDetailPage.tsx index 254d841..23a4fd9 100644 --- a/frontend/src/features/listings/ListingDetailPage.tsx +++ b/frontend/src/features/listings/ListingDetailPage.tsx @@ -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() {
哈夫币 - {data.haf_coin_amount.toLocaleString()} M + {formatCoinM(data.haf_coin_amount)}
号主 @@ -444,7 +445,7 @@ export function ListingDetailPage() { {money(data.price_cent)} {money(data.deposit_amount_cent)} - {data.haf_coin_amount.toLocaleString()} M + {formatCoinM(data.haf_coin_amount)} {data.owner_nickname || `#${data.owner_id}`} diff --git a/frontend/src/features/listings/ListingListPage.tsx b/frontend/src/features/listings/ListingListPage.tsx index 9ad0d82..1644b49 100644 --- a/frontend/src/features/listings/ListingListPage.tsx +++ b/frontend/src/features/listings/ListingListPage.tsx @@ -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).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 + 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 (
@@ -145,6 +176,26 @@ function ListingRow({ item }: { item: Listing }) { ))}
+ + {skins.length > 0 ? ( +
+ 精选皮肤 + {skins.map(name => ( + {name} + ))} +
+ ) : null} + + {resources.length > 0 ? ( +
+ 仓库物资 + {resources.map(row => ( + + {row.label} {row.quantity} + + ))} +
+ ) : null}
@@ -194,7 +245,7 @@ function ListingGridCard({ item }: { item: Listing }) { {item.rank_level ? {item.rank_level} : null}
- 哈夫币 {item.haf_coin_amount.toLocaleString()} + 哈夫币 {formatCoinM(item.haf_coin_amount)} {item.owner_nickname || '商行认证号主'}
diff --git a/frontend/src/features/mobile/MobileHomePage.tsx b/frontend/src/features/mobile/MobileHomePage.tsx index 94fc10a..f584604 100644 --- a/frontend/src/features/mobile/MobileHomePage.tsx +++ b/frontend/src/features/mobile/MobileHomePage.tsx @@ -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 }) {
{item.server_region || '区服待定'} {item.rank_level || '段位待定'} - 哈夫币 {item.haf_coin_amount.toLocaleString()} + 哈夫币 {formatCoinM(item.haf_coin_amount)} {specs.map(spec => ( {spec} ))} diff --git a/frontend/src/features/mobile/MobileListingDetailPage.tsx b/frontend/src/features/mobile/MobileListingDetailPage.tsx index 5a7e326..b9152ec 100644 --- a/frontend/src/features/mobile/MobileListingDetailPage.tsx +++ b/frontend/src/features/mobile/MobileListingDetailPage.tsx @@ -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() {
哈夫币 - {data.haf_coin_amount.toLocaleString()} + {formatCoinM(data.haf_coin_amount)}
@@ -267,7 +268,7 @@ export function MobileListingDetailPage() {
哈夫币 - {data.haf_coin_amount.toLocaleString()} M + {formatCoinM(data.haf_coin_amount)}
号主 diff --git a/frontend/src/shared/utils/format.ts b/frontend/src/shared/utils/format.ts index 9ee2820..7a36ec6 100644 --- a/frontend/src/shared/utils/format.ts +++ b/frontend/src/shared/utils/format.ts @@ -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') diff --git a/frontend/src/styles/storefront-filters.css b/frontend/src/styles/storefront-filters.css index f4c620a..9b46cd9 100644 --- a/frontend/src/styles/storefront-filters.css +++ b/frontend/src/styles/storefront-filters.css @@ -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;