From 3ea02661ea9bd754c56b1faebfd847f5b5bbf568 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 31 Oct 2026 12:37:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(listings):=20=E9=87=8D=E5=81=9A=E5=95=86?= =?UTF-8?q?=E5=93=81=E8=AF=A6=E6=83=85=E9=A1=B5=E5=B9=B6=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E5=88=86=E7=BB=84=E6=88=AA=E5=9B=BE=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/scripts/seed_demo_listings.sql | 16 +- .../features/listings/ListingDetailPage.tsx | 535 +++++++++++------- frontend/src/styles/global.css | 1 + .../src/styles/storefront-listing-detail.css | 477 ++++++++++++++++ frontend/src/styles/storefront-listings.css | 275 --------- 5 files changed, 825 insertions(+), 479 deletions(-) create mode 100644 frontend/src/styles/storefront-listing-detail.css diff --git a/backend/scripts/seed_demo_listings.sql b/backend/scripts/seed_demo_listings.sql index bba8f2a..d2d3c07 100644 --- a/backend/scripts/seed_demo_listings.sql +++ b/backend/scripts/seed_demo_listings.sql @@ -18,8 +18,8 @@ INSERT INTO game_accounts 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'); + 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-群星"]},"screenshot_groups":{"coin":["https://picsum.photos/seed/jieyou-1-coin/1200/750"],"gameId":["https://picsum.photos/seed/jieyou-1-game/1200/750"],"totalAsset":["https://picsum.photos/seed/jieyou-1-asset/1200/750"],"tencentSecurity":["https://picsum.photos/seed/jieyou-1-safe/1200/750"],"skin":["https://picsum.photos/seed/jieyou-1-skin/1200/750"]},"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-coin/1200/750","https://picsum.photos/seed/jieyou-1-game/1200/750","https://picsum.photos/seed/jieyou-1-asset/1200/750","https://picsum.photos/seed/jieyou-1-safe/1200/750","https://picsum.photos/seed/jieyou-1-skin/1200/750"]' 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); @@ -31,8 +31,8 @@ INSERT INTO game_accounts 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'); + 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-群星"]},"screenshot_groups":{"coin":["https://picsum.photos/seed/jieyou-2-coin/1200/750"],"gameId":["https://picsum.photos/seed/jieyou-2-game/1200/750"],"totalAsset":["https://picsum.photos/seed/jieyou-2-asset/1200/750"],"tencentSecurity":["https://picsum.photos/seed/jieyou-2-safe/1200/750"],"skin":["https://picsum.photos/seed/jieyou-2-skin/1200/750"]},"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-coin/1200/750","https://picsum.photos/seed/jieyou-2-game/1200/750","https://picsum.photos/seed/jieyou-2-asset/1200/750","https://picsum.photos/seed/jieyou-2-safe/1200/750","https://picsum.photos/seed/jieyou-2-skin/1200/750"]' 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); @@ -44,8 +44,8 @@ INSERT INTO game_accounts 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'); + 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-群星"]},"screenshot_groups":{"coin":["https://picsum.photos/seed/jieyou-3-coin/1200/750"],"gameId":["https://picsum.photos/seed/jieyou-3-game/1200/750"],"totalAsset":["https://picsum.photos/seed/jieyou-3-asset/1200/750"],"tencentSecurity":["https://picsum.photos/seed/jieyou-3-safe/1200/750"],"skin":["https://picsum.photos/seed/jieyou-3-skin/1200/750"]},"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-coin/1200/750","https://picsum.photos/seed/jieyou-3-game/1200/750","https://picsum.photos/seed/jieyou-3-asset/1200/750","https://picsum.photos/seed/jieyou-3-safe/1200/750","https://picsum.photos/seed/jieyou-3-skin/1200/750"]' 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); @@ -57,8 +57,8 @@ INSERT INTO game_accounts 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'); + 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-群星"]},"screenshot_groups":{"coin":["https://picsum.photos/seed/jieyou-4-coin/1200/750"],"gameId":["https://picsum.photos/seed/jieyou-4-game/1200/750"],"totalAsset":["https://picsum.photos/seed/jieyou-4-asset/1200/750"],"tencentSecurity":["https://picsum.photos/seed/jieyou-4-safe/1200/750"],"skin":["https://picsum.photos/seed/jieyou-4-skin/1200/750"]},"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-coin/1200/750","https://picsum.photos/seed/jieyou-4-game/1200/750","https://picsum.photos/seed/jieyou-4-asset/1200/750","https://picsum.photos/seed/jieyou-4-safe/1200/750","https://picsum.photos/seed/jieyou-4-skin/1200/750"]' 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/ListingDetailPage.tsx b/frontend/src/features/listings/ListingDetailPage.tsx index 23a4fd9..197c891 100644 --- a/frontend/src/features/listings/ListingDetailPage.tsx +++ b/frontend/src/features/listings/ListingDetailPage.tsx @@ -7,12 +7,9 @@ import { Button, Card, Checkbox, - Col, Descriptions, - Divider, Image, Modal, - Row, Space, Tag, Typography, @@ -20,10 +17,8 @@ import { import { ArrowLeftOutlined, SafetyCertificateOutlined, - ShoppingOutlined, StarFilled, StarOutlined, - WalletOutlined, } from '@ant-design/icons' import { Link, useNavigate, useParams } from 'react-router-dom' @@ -46,21 +41,39 @@ import { formatCoinM } from '@/shared/utils/format' import type { Listing } from '@/shared/api/types' function money(cents: number) { - return `¥${(cents / 100).toFixed(2)}` + return `¥${(cents / 100).toLocaleString('zh-CN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}` } -function assetValue(value: unknown) { - if (value && typeof value === 'object') { - const record = value as Record - if (typeof record.start === 'string' && typeof record.end === 'string') { - return `${record.start} – ${record.end}` - } - return Object.entries(record) - .map(([key, item]) => `${key}:${String(item)}`) - .join(',') - } - if (Array.isArray(value)) return value.map(String).join('、') - return value == null || value === '' ? '未填写' : String(value) +const skinGroupLabels: Record = { + melee: '近战皮肤', + operatorGold: '干员金皮', + operatorRed: '干员红皮', + weapon: '武器皮肤', +} + +const screenshotGroupLabels: Record = { + coin: '纯币截图', + gameId: '游戏ID截图', + totalAsset: '总资产截图', + tencentSecurity: '腾讯安全截图', + skin: '皮肤截图', +} + +function screenshotLabels(item: Listing) { + const map = new Map() + const groups = item.asset_summary?.screenshot_groups + if (!groups || typeof groups !== 'object') return map + Object.entries(groups as Record).forEach(([key, urls]) => { + if (!Array.isArray(urls)) return + const label = screenshotGroupLabels[key] ?? key + urls.forEach(url => { + if (typeof url === 'string' && url) map.set(url, label) + }) + }) + return map } function galleryImages(item: Listing) { @@ -68,6 +81,78 @@ function galleryImages(item: Listing) { return images.length > 0 ? Array.from(new Set(images)) : [] } +function textOf(value: unknown) { + return value == null || value === '' ? '' : String(value) +} + +function onlineTimeText(value: unknown) { + if (!value || typeof value !== 'object') return '' + const row = value as Record + const start = textOf(row.start) + const end = textOf(row.end) + if (!start && !end) return '' + return `${start} - ${end}` +} + +function skinGroupsOf(item: Listing) { + const groups = item.asset_summary?.skin_groups + if (!groups || typeof groups !== 'object') return [] + return Object.entries(groups as Record) + .map(([key, names]) => ({ + key, + label: skinGroupLabels[key] ?? key, + names: Array.isArray(names) ? names.filter((name): name is string => typeof name === 'string') : [], + })) + .filter(group => group.names.length > 0) +} + +function resourcesOf(item: Listing) { + const raw = item.asset_summary?.resources + if (!Array.isArray(raw)) return [] + return raw + .map(entry => { + const row = (entry ?? {}) as Record + return { + label: textOf(row.label) || textOf(row.key), + quantity: Number(row.quantity ?? 0), + mode: textOf(row.mode), + } + }) + .filter(row => row.label && row.quantity > 0) +} + +function ratioOf(item: Listing) { + const published = Number(item.asset_summary?.publish_ratio ?? 0) + if (published > 0) return `${published}w` + const priceYuan = item.price_cent / 100 + const coinM = item.haf_coin_amount / 1_000_000 + if (priceYuan <= 0 || coinM <= 0) return '' + return `${((coinM / priceYuan) * 100).toFixed(0)}w` +} + +function coreStats(item: Listing) { + const summary = item.asset_summary ?? {} + const dailyLoss = Number(summary.daily_loss_m ?? 0) + const secretKd = Number(summary.secret_kd ?? 0) + return [ + { label: '哈夫币', value: formatCoinM(item.haf_coin_amount) }, + { label: '保险格数', value: textOf(summary.season_insurance) }, + { label: '折算比', value: ratioOf(item) ? `1 元 ≈ ${ratioOf(item)}` : '' }, + { label: '火力等级', value: textOf(summary.fire_level) }, + { label: '体力', value: textOf(summary.stamina_level) }, + { label: '负重', value: textOf(summary.load_level) }, + { label: '日消耗', value: dailyLoss > 0 ? `${dailyLoss} M / 天` : '' }, + { label: '绝密 KD', value: secretKd > 0 ? String(secretKd) : '' }, + { label: '段位', value: item.rank_level }, + { label: '登录方式', value: item.login_platform }, + { label: '区服', value: item.server_region }, + { label: '在线时段', value: onlineTimeText(summary.online_time) }, + { label: '可改游戏名', value: textOf(summary.modifiable_name) }, + { label: '全英雄', value: textOf(summary.all_heroes) }, + { label: '凌晨响应', value: textOf(summary.early_morning_response) }, + ].filter(stat => stat.value) +} + export function ListingDetailPage() { const { id } = useParams() const navigate = useNavigate() @@ -91,6 +176,7 @@ export function ListingDetailPage() { const [agreementOpen, setAgreementOpen] = useState(false) const [agreementTitle, setAgreementTitle] = useState('') const [agreementContent, setAgreementContent] = useState('') + const [activeImage, setActiveImage] = useState('') useEffect(() => { if (!data) return @@ -103,6 +189,12 @@ export function ListingDetailPage() { recordListingView(data) }, [data, token, queryClient]) + useEffect(() => { + if (!data) return + const images = galleryImages(data) + setActiveImage(current => (current && images.includes(current) ? current : images[0] ?? '')) + }, [data]) + const showAgreement = (title: string, content?: string) => { setAgreementTitle(title) setAgreementContent(content || '暂无内容') @@ -161,7 +253,7 @@ export function ListingDetailPage() { if (isPending) { return ( -
+
) @@ -169,7 +261,7 @@ export function ListingDetailPage() { if (isError || !data) { return ( -
+
key !== 'online_time' - ) - const onlineTime = data.asset_summary?.online_time + const previewIndex = Math.max(images.indexOf(activeImage), 0) + const labels = screenshotLabels(data) + const activeLabel = labels.get(activeImage) ?? '' + const skins = skinGroupsOf(data) + const resources = resourcesOf(data) + const stats = coreStats(data) 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 toggleFavoriteState = async () => { if (token) { @@ -210,8 +305,8 @@ export function ListingDetailPage() { } return ( -
-
+
+
首页 }, @@ -224,200 +319,248 @@ export function ListingDetailPage() {
- - -
+
+
+
{images.length > 0 ? ( - -
- {data.title} -
- {images.length > 1 ? ( -
- {images.slice(0, 6).map(url => ( - {data.title} - ))} -
- ) : null} + + {data.title} ) : ( -
+
- 暂无商品图片 + 号主暂未上传截图
)} + + {data.in_transaction ? '租赁中' : '可租'} + + {images.length > 1 ? ( + + {activeLabel ? `${activeLabel} · ` : ''} + {previewIndex + 1} / {images.length} + + ) : null}
- - - -
- {data.listing_no} - - - {data.in_transaction ? '租赁中' : '可租'} - - - + {`${data.title} + + ))}
- - {data.title} - - - {data.server_region || '区服待定'} - {data.login_platform || '登录方式待定'} - {data.rank_level ? {data.rank_level} : null} + ) : null} +
+ + +
+ +
+ +
+ {stats.map(stat => ( +
+ {stat.label} + {stat.value} +
+ ))} +
+
+ + {skins.length > 0 ? ( + +
+ {skins.map(group => ( +
+ + {group.label} + {group.names.length} + +
+ {group.names.map(name => ( + {name} + ))} +
+
+ ))}
- - + ) : null} - - - - + {resources.length > 0 ? ( + +
+ {resources.map(row => ( +
+ {row.label} + {row.quantity} + {row.mode === '赠送' ? 赠送 : null} +
+ ))} +
+
+ ) : null} + +
+ + {data.description || '号主暂未填写补充说明。'} - - - {assetEntries.map(([key, value]) => ( - - {assetValue(value)} - - ))} - {assetValue(onlineTime)} - - - - -
- {[ - ['01', '确认下单', '核对租金、押金与账号信息,提交订单。'], - ['02', '完成支付', '支付成功后订单进入待交接。'], - ['03', '号主交接', '号主提交账号与登录说明,租客确认收号。'], - ['04', '使用与归还', '租期内按规则使用,结束时发起结账。'], - ['05', '确认结算', '双方确认或客服仲裁,完成押金与租金结算。'], - ].map(([no, title, desc]) => ( -
- {no} -
- {title} -

{desc}

-
+ +
    +
  1. + 01 +
    + 确认下单 +

    核对租金、押金与账号信息。

    - ))} -
+ +
  • + 02 +
    + 完成支付 +

    支付成功后进入待交接。

    +
    +
  • +
  • + 03 +
    + 号主交接 +

    提交账号与登录说明,租客确认。

    +
    +
  • +
  • + 04 +
    + 使用与归还 +

    租期内按规则使用,结束归还。

    +
    +
  • +
  • + 05 +
    + 确认结算 +

    双方确认或客服仲裁后结算。

    +
    +
  • + - - - - -
    - -
    - 实名与审核 - 发布、下单前均需身份核验。 -
    -
    - -
    - -
    - 全程留痕 - 交接、归还、结账都有记录。 -
    -
    - -
    - -
    - 账单透明 - 租金、押金、消耗逐项列明。 -
    -
    -
    - - +
    +
    @@ -475,7 +618,7 @@ export function ListingDetailPage() { } onCancel={() => setAgreementOpen(false)} > -
    {agreementContent}
    +
    {agreementContent}
    ) diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 347d2f5..b2d2680 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -4,6 +4,7 @@ @import './storefront-portal.css'; @import './storefront-sections.css'; @import './storefront-listings.css'; +@import './storefront-listing-detail.css'; @import './storefront-filters.css'; @import './storefront-content.css'; @import './storefront-help.css'; diff --git a/frontend/src/styles/storefront-listing-detail.css b/frontend/src/styles/storefront-listing-detail.css new file mode 100644 index 0000000..2708512 --- /dev/null +++ b/frontend/src/styles/storefront-listing-detail.css @@ -0,0 +1,477 @@ +/* 商品详情页:画廊 + 购买卡 + 资产分区 */ +.ld-page { + max-width: 1240px; + margin: 0 auto; + padding: 22px 24px 64px; +} + +.ld-crumb { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 14px; +} + +.ld-hero { + display: grid; + grid-template-columns: minmax(0, 1.12fr) minmax(0, 0.88fr); + gap: 20px; + align-items: start; +} + +/* 画廊 */ +.ld-gallery { + padding: 12px; + border: 1px solid var(--line); + border-radius: 18px; + background: var(--paper-warm); +} + +.ld-gallery__main { + position: relative; + aspect-ratio: 16 / 10; + overflow: hidden; + border-radius: 12px; + background: #eef1ea; +} + +.ld-gallery__main .ant-image, +.ld-gallery__main img { + width: 100%; + height: 100%; +} + +.ld-gallery__main img { + display: block; + object-fit: cover; +} + +.ld-status { + position: absolute; + top: 12px; + left: 12px; + padding: 3px 10px; + border-radius: 999px; + background: rgba(47, 125, 92, 0.92); + color: #fff; + font-size: 12px; + letter-spacing: 0.04em; +} + +.ld-status.is-busy { + background: rgba(94, 107, 100, 0.9); +} + +.ld-gallery__count { + position: absolute; + right: 12px; + bottom: 12px; + padding: 2px 8px; + border-radius: 999px; + background: rgba(21, 58, 50, 0.62); + color: #fff; + font-size: 11px; +} + +.ld-gallery__empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + height: 100%; + color: var(--ink-muted); + font-size: 12px; +} + +.ld-gallery__thumbs { + display: flex; + gap: 8px; + margin-top: 10px; + overflow-x: auto; + scrollbar-width: thin; +} + +.ld-thumb { + flex: none; + width: 84px; + height: 58px; + padding: 0; + overflow: hidden; + border: 2px solid transparent; + border-radius: 8px; + background: #eef1ea; + cursor: pointer; + opacity: 0.72; + transition: + opacity 160ms ease, + border-color 160ms ease; +} + +.ld-thumb img { + width: 100%; + height: 100%; + display: block; + object-fit: cover; +} + +.ld-thumb:hover { + opacity: 1; +} + +.ld-thumb.is-active { + border-color: var(--brand-seal); + opacity: 1; +} + +/* 购买卡 */ +.ld-buy { + position: sticky; + top: 92px; + padding: 20px 22px 18px; + border: 1px solid var(--line); + border-radius: 18px; + background: var(--paper-warm); +} + +.ld-buy__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.ld-no { + color: var(--ink-muted); + font-family: 'Inter', monospace; + font-size: 12px; + letter-spacing: 0.06em; +} + +.ld-title { + margin: 10px 0 10px !important; + color: var(--ink) !important; + font-size: clamp(20px, 2vw, 26px) !important; + line-height: 1.4 !important; +} + +.ld-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 14px; +} + +.ld-tags .ant-tag { + margin: 0; + border-radius: 4px; + background: #eef2ec; + color: var(--brand-ink); +} + +.ld-price { + padding: 14px 16px; + border-radius: 14px; + background: linear-gradient(135deg, rgba(166, 67, 50, 0.08), rgba(176, 138, 74, 0.1)); +} + +.ld-price > span { + color: var(--ink-muted); + font-size: 12px; +} + +.ld-price strong { + display: block; + margin: 2px 0 4px; + color: var(--brand-seal); + font-family: 'Inter', 'DIN Alternate', sans-serif; + font-size: 34px; + line-height: 1.15; +} + +.ld-price em { + color: var(--ink-secondary); + font-size: 12px; + font-style: normal; +} + +.ld-quick { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + margin: 12px 0 16px; +} + +.ld-quick div { + padding: 9px 12px; + border: 1px solid var(--line); + border-radius: 10px; + background: #fff; +} + +.ld-quick span { + display: block; + color: var(--ink-muted); + font-size: 11px; +} + +.ld-quick strong { + display: block; + margin-top: 2px; + color: var(--brand-ink); + font-size: 16px; +} + +.ld-cta { + height: 46px; + border-radius: 10px; + font-size: 16px; +} + +.ld-links { + display: flex; + flex-wrap: wrap; + gap: 6px 14px; + margin: 12px 0 14px; +} + +.ld-links button { + padding: 0; + border: 0; + background: transparent; + color: var(--ink-muted); + font-size: 12px; + cursor: pointer; +} + +.ld-links button:hover { + color: var(--brand-seal); +} + +.ld-owner { + display: flex; + align-items: center; + gap: 8px; + padding-top: 12px; + border-top: 1px dashed var(--line); + color: var(--ink-secondary); + font-size: 12px; +} + +.ld-owner .anticon { + color: var(--brand-ink); +} + +/* 资产区 */ +.ld-body { + display: flex; + flex-direction: column; + gap: 16px; + margin-top: 20px; +} + +.ld-panel { + border-color: var(--line); + border-radius: 16px; + background: var(--paper-warm); +} + +.ld-panel .ant-card-head { + min-height: 48px; + border-bottom-color: var(--line); +} + +.ld-panel .ant-card-head-title { + color: var(--brand-ink); + font-family: 'Noto Serif SC', 'Songti SC', serif; + font-size: 16px; +} + +.ld-stats { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 10px; +} + +.ld-stat { + padding: 10px 14px; + border: 1px solid var(--line); + border-radius: 10px; + background: #fff; +} + +.ld-stat span { + display: block; + color: var(--ink-muted); + font-size: 11px; +} + +.ld-stat strong { + display: block; + margin-top: 2px; + color: var(--ink); + font-size: 15px; + font-weight: 600; +} + +.ld-skin-groups { + display: flex; + flex-direction: column; + gap: 12px; +} + +.ld-skin-group { + display: flex; + align-items: flex-start; + gap: 14px; +} + +.ld-skin-group__label { + flex: none; + width: 84px; + padding-top: 5px; + color: var(--ink-secondary); + font-size: 12px; +} + +.ld-skin-group__label em { + margin-left: 4px; + color: var(--ink-muted); + font-style: normal; +} + +.ld-skin-group__items { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.ld-skin-group__items > span { + padding: 3px 10px; + border-radius: 999px; + background: rgba(166, 67, 50, 0.08); + color: var(--brand-seal); + font-size: 12px; +} + +.ld-resources { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.ld-resource { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border: 1px solid var(--line); + border-radius: 10px; + background: #fff; + font-size: 12px; + color: var(--ink-secondary); +} + +.ld-resource strong { + color: var(--ink); + font-size: 14px; +} + +.ld-resource em { + padding: 0 6px; + border-radius: 999px; + background: rgba(47, 125, 92, 0.12); + color: var(--success); + font-size: 10px; + font-style: normal; + line-height: 16px; +} + +.ld-split { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 16px; + align-items: start; +} + +.ld-description { + margin-bottom: 0 !important; + color: var(--ink-secondary); + font-size: 13px; + line-height: 1.9; +} + +.ld-steps { + display: flex; + flex-direction: column; + gap: 10px; + margin: 0; + padding: 0; + list-style: none; +} + +.ld-steps li { + display: flex; + gap: 12px; + padding-bottom: 10px; + border-bottom: 1px dashed var(--line); +} + +.ld-steps li:last-child { + padding-bottom: 0; + border-bottom: 0; +} + +.ld-steps li > span { + flex: none; + color: var(--brand-seal); + font-family: 'Inter', monospace; + font-size: 11px; + line-height: 20px; +} + +.ld-steps strong { + display: block; + color: var(--brand-ink); + font-size: 13px; +} + +.ld-steps p { + margin: 2px 0 0; + color: var(--ink-muted); + font-size: 12px; + line-height: 1.6; +} + +.ld-modal-notice { + margin: 14px 0 !important; +} + +.ld-agreement { + max-height: 60vh; + overflow-y: auto; + color: var(--ink-secondary); + font-size: 13px; + line-height: 1.9; + white-space: pre-wrap; +} + +@media (max-width: 1024px) { + .ld-hero { + grid-template-columns: minmax(0, 1fr); + } + + .ld-buy { + position: static; + } + + .ld-split { + grid-template-columns: minmax(0, 1fr); + } + + .ld-skin-group { + flex-direction: column; + gap: 6px; + } +} diff --git a/frontend/src/styles/storefront-listings.css b/frontend/src/styles/storefront-listings.css index 460cb69..02d00a6 100644 --- a/frontend/src/styles/storefront-listings.css +++ b/frontend/src/styles/storefront-listings.css @@ -117,281 +117,6 @@ color: inherit; } -.listing-detail-page { - max-width: 1240px; - margin: 0 auto; - padding: 26px 24px 64px; -} - -.listing-detail-page__crumb { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; -} - -.listing-detail-page__hero { - margin-top: 18px; -} - -.listing-detail-gallery { - padding: 14px; - border: 1px solid var(--line); - border-radius: 12px; - background: var(--paper-warm); -} - -.listing-detail-gallery__main { - aspect-ratio: 16 / 10; - overflow: hidden; - border-radius: 8px; - background: #eef1ea; -} - -.listing-detail-gallery__main .ant-image, -.listing-detail-gallery__main img { - width: 100%; - height: 100%; -} - -.listing-detail-gallery__main img { - object-fit: cover; -} - -.listing-detail-gallery__thumbs { - display: flex; - gap: 8px; - margin-top: 10px; - overflow-x: auto; -} - -.listing-detail-gallery__thumbs .ant-image { - flex: 0 0 auto; - overflow: hidden; - border: 1px solid var(--line); - border-radius: 6px; -} - -.listing-detail-gallery__thumbs img { - object-fit: cover; -} - -.listing-detail-gallery__empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 12px; - min-height: 360px; - border-radius: 8px; - background: var(--paper); - color: var(--brand-ink); - letter-spacing: 0.12em; -} - -.listing-buy-card { - position: sticky; - top: 84px; - border-color: var(--line); - background: var(--paper-warm); -} - -.listing-buy-card__top { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.listing-buy-card__title { - margin: 12px 0 10px !important; - color: var(--brand-ink) !important; - font-size: 26px !important; - line-height: 1.35 !important; -} - -.listing-buy-card__price { - display: flex; - align-items: baseline; - gap: 10px; - margin-top: 18px; - padding: 14px 16px; - border-radius: 8px; - background: var(--paper); -} - -.listing-buy-card__price span { - color: var(--ink-muted); - font-size: 12px; -} - -.listing-buy-card__price strong { - color: var(--brand-seal); - font-family: 'Inter', 'DIN Alternate', sans-serif; - font-size: 32px; -} - -.listing-buy-card__meta { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; - margin: 16px 0; -} - -.listing-buy-card__meta div { - display: flex; - flex-direction: column; - gap: 4px; - padding: 10px 12px; - border: 1px solid var(--line); - border-radius: 6px; - background: #fff; -} - -.listing-buy-card__meta span { - color: var(--ink-muted); - font-size: 11px; -} - -.listing-buy-card__meta strong { - color: var(--brand-ink); - font-family: 'Inter', monospace; - font-size: 13px; -} - -.listing-buy-card__notice { - margin-bottom: 16px; -} - -.listing-buy-card__links { - display: flex; - flex-wrap: wrap; - gap: 6px 12px; - margin-top: 12px; -} - -.listing-buy-card__links button { - padding: 0; - border: 0; - background: none; - color: var(--brand-seal); - font-size: 11px; - cursor: pointer; -} - -.listing-buy-card__links button:hover { - text-decoration: underline; - text-underline-offset: 3px; -} - -.listing-detail-page__body { - margin-top: 24px; -} - -.listing-detail-panel { - margin-bottom: 18px; - border-color: var(--line); - background: var(--paper-warm); -} - -.listing-detail-description { - margin-bottom: 0 !important; - color: var(--ink-secondary); - line-height: 1.85; - white-space: pre-wrap; -} - -.listing-process { - display: flex; - flex-direction: column; -} - -.listing-process__step { - display: flex; - gap: 16px; - padding: 14px 0; - border-bottom: 1px dashed var(--line); -} - -.listing-process__step:last-child { - border-bottom: 0; -} - -.listing-process__step > span { - color: var(--brand-seal); - font-family: 'Inter', monospace; - font-size: 12px; -} - -.listing-process__step strong { - color: var(--brand-ink); - font-size: 14px; -} - -.listing-process__step p { - margin: 6px 0 0; - color: var(--ink-muted); - font-size: 12px; -} - -.listing-trust-card { - border-color: var(--line); - background: var(--paper-warm); -} - -.listing-trust-card__item { - display: flex; - align-items: flex-start; - gap: 12px; -} - -.listing-trust-card__item > .anticon { - margin-top: 2px; - color: var(--brand-seal); - font-size: 18px; -} - -.listing-trust-card__item div { - display: flex; - flex-direction: column; - gap: 4px; -} - -.listing-trust-card__item strong { - color: var(--brand-ink); - font-size: 13px; -} - -.listing-trust-card__item span { - color: var(--ink-muted); - font-size: 11px; - line-height: 1.6; -} - -.listing-agreement-content { - max-height: 62vh; - overflow-y: auto; - color: var(--ink-secondary); - font-size: 12px; - line-height: 1.8; - white-space: pre-wrap; - word-break: break-word; -} - -@media (max-width: 991px) { - .listing-detail-page { - padding: 20px 16px 48px; - } - - .listing-buy-card { - position: static; - } - - .listing-buy-card__meta { - grid-template-columns: 1fr; - } -} - .pc-orders-page, .pc-order-detail-page { max-width: 1240px;