From 96d87483b0e17646c97a39a00077b5c012d09ecc Mon Sep 17 00:00:00 2001 From: yml Date: Sat, 23 May 2026 01:24:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=A6=96=E9=A1=B5=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/systemconfig/publish_options.go | 2 +- backend/seeds/dev_orders.sql | 34 +- frontend/src/api/listingOptions.ts | 15 +- frontend/src/views/mobile/MobileHomeView.css | 551 ++++++++++++ frontend/src/views/mobile/MobileHomeView.vue | 826 ++---------------- .../views/mobile/MobileListingDetailView.vue | 300 +++++-- .../mobile/MobileSellerListingCreateView.vue | 4 +- frontend/src/views/mobile/listingDisplay.ts | 256 ++++++ .../src/views/mobile/mobileHomeFallback.ts | 177 ++++ 9 files changed, 1327 insertions(+), 838 deletions(-) create mode 100644 frontend/src/views/mobile/MobileHomeView.css create mode 100644 frontend/src/views/mobile/listingDisplay.ts create mode 100644 frontend/src/views/mobile/mobileHomeFallback.ts diff --git a/backend/internal/modules/systemconfig/publish_options.go b/backend/internal/modules/systemconfig/publish_options.go index befe5e5..87a4924 100644 --- a/backend/internal/modules/systemconfig/publish_options.go +++ b/backend/internal/modules/systemconfig/publish_options.go @@ -62,7 +62,7 @@ func DefaultPublishOptions() PublishOptionsDTO { {Key: "helmet6", Label: "6头数量", Price: "2元/个"}, {Key: "armor6", Label: "6甲数量", Price: "3元/个"}, {Key: "kit5", Label: "5级全装套数量", Price: "5元/个"}, - {Key: "armor6Ammo", Label: "6甲子弹", Price: "0.26元/发"}, + {Key: "level6Ammo", Label: "6级子弹", Price: "0.26元/发"}, {Key: "gridCard9", Label: "9格体验卡", Price: "5元/张", Placeholder: "如果是赛季9格,此处填0即可"}, }, ScreenshotSlots: []PublishScreenshotSlot{ diff --git a/backend/seeds/dev_orders.sql b/backend/seeds/dev_orders.sql index d582f89..b9b9481 100644 --- a/backend/seeds/dev_orders.sql +++ b/backend/seeds/dev_orders.sql @@ -18,7 +18,7 @@ DROP TEMPORARY TABLE IF EXISTS tmp_seed_orders; CREATE TEMPORARY TABLE IF NOT EXISTS tmp_seed_accounts AS SELECT id FROM game_accounts -WHERE title LIKE '测试发布-%' OR title LIKE '测试订单-%'; +WHERE title LIKE '测试发布-%' OR title LIKE '测试订单-%' OR title LIKE '纯币%'; DELETE FROM rental_listings WHERE account_id IN (SELECT id FROM tmp_seed_accounts); DELETE FROM game_accounts WHERE id IN (SELECT id FROM tmp_seed_accounts); @@ -55,26 +55,26 @@ ON DUPLICATE KEY UPDATE 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 - (@owner_a, '三角洲行动', 'QQ', '扫码', '测试发布-QQ 黑鹰 30000万哈夫币', + (@owner_a, '三角洲行动', 'QQ', '扫码', '纯币300M/6格/6体/6负/4六甲/6六头', '扫码交接,保险 2*3,30M 日损耗,适合高强度体验。', '黑鹰', 300000000, - JSON_OBJECT('haf_coin_wan', 30000, 'season_insurance', '2*3', 'stamina_level', '6级', 'load_level', '6级', 'daily_loss', '30M', 'ratio', 6, 'login_method', '扫码', 'common_regions', JSON_ARRAY('广东', '上海'), 'resources', JSON_ARRAY(JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 80, 'mode', '收费'), JSON_OBJECT('key', 'helmet6', 'label', '6头数量', 'quantity', 6, 'mode', '收费'), JSON_OBJECT('key', 'armor6', 'label', '6甲数量', 'quantity', 4, 'mode', '收费')), 'skin_groups', JSON_OBJECT('melee', JSON_ARRAY('龙牙', '信条'), 'operator', JSON_ARRAY('威龙-凌霄戍卫'), 'weapon', JSON_ARRAY('M4A1棱镜攻势', 'K416命运'))), - JSON_ARRAY(), 'published'), - (@owner_a, '三角洲行动', '微信', '账号密码', '测试发布-微信 钻石 12000万低押号', + JSON_OBJECT('haf_coin_wan', 30000, 'total_asset_wan', 33700, 'season_insurance', '2*3', 'stamina_level', '6级', 'load_level', '6级', 'daily_loss', '30M', 'ratio', 6, 'price', 5000, 'settlement_amount', 5000, 'rent_days', 7, 'login_method', '扫码', 'online_time', JSON_OBJECT('start', '10:00', 'end', '23:00'), 'common_regions', JSON_ARRAY('广东', '上海'), 'resources', JSON_ARRAY(JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 80, 'mode', '收费'), JSON_OBJECT('key', 'helmet6', 'label', '6头数量', 'quantity', 6, 'mode', '收费'), JSON_OBJECT('key', 'armor6', 'label', '6甲数量', 'quantity', 4, 'mode', '收费')), 'skin_groups', JSON_OBJECT('melee', JSON_ARRAY('龙牙', '信条'), 'operator', JSON_ARRAY('威龙-凌霄戍卫'), 'weapon', JSON_ARRAY('M4A1棱镜攻势', 'K416命运')), 'screenshots', JSON_OBJECT('coin', 'https://picsum.photos/seed/dev-account-1-coin/960/540', 'gameId', 'https://picsum.photos/seed/dev-account-1-game/960/540', 'totalAsset', 'https://picsum.photos/seed/dev-account-1-asset/960/540')), + JSON_ARRAY('https://picsum.photos/seed/dev-account-1-coin/960/540', 'https://picsum.photos/seed/dev-account-1-asset/960/540'), 'published'), + (@owner_a, '三角洲行动', '微信', '账号密码', '纯币120M/4格/5体/5负/2六头', '账密登录,低押金,保险 2*2,常用登录地江苏。', '钻石', 120000000, - JSON_OBJECT('haf_coin_wan', 12000, 'season_insurance', '2*2', 'stamina_level', '5级', 'load_level', '5级', 'daily_loss', '20M', 'ratio', 5, 'login_method', '账号密码', 'common_regions', JSON_ARRAY('江苏'), 'resources', JSON_ARRAY(JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 30, 'mode', '收费'), JSON_OBJECT('key', 'helmet6', 'label', '6头数量', 'quantity', 2, 'mode', '收费')), 'skin_groups', JSON_OBJECT('melee', JSON_ARRAY('怜悯'), 'operator', JSON_ARRAY('露娜-黑天际线'), 'weapon', JSON_ARRAY('SCAR-H电玩高手'))), - JSON_ARRAY(), 'published'), - (@owner_b, '三角洲行动', 'Steam', '扫码', '测试发布-Steam 铂金 6000万包夜号', + JSON_OBJECT('haf_coin_wan', 12000, 'total_asset_wan', 13800, 'season_insurance', '2*2', 'stamina_level', '5级', 'load_level', '5级', 'daily_loss', '20M', 'ratio', 5, 'price', 2400, 'settlement_amount', 2400, 'rent_days', 3, 'login_method', '账号密码', 'online_time', JSON_OBJECT('start', '12:00', 'end', '24:00'), 'common_regions', JSON_ARRAY('江苏'), 'resources', JSON_ARRAY(JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 30, 'mode', '收费'), JSON_OBJECT('key', 'helmet6', 'label', '6头数量', 'quantity', 2, 'mode', '收费')), 'skin_groups', JSON_OBJECT('melee', JSON_ARRAY('怜悯'), 'operator', JSON_ARRAY('露娜-黑天际线'), 'weapon', JSON_ARRAY('SCAR-H电玩高手')), 'screenshots', JSON_OBJECT('coin', 'https://picsum.photos/seed/dev-account-2-coin/960/540', 'gameId', 'https://picsum.photos/seed/dev-account-2-game/960/540', 'totalAsset', 'https://picsum.photos/seed/dev-account-2-asset/960/540')), + JSON_ARRAY('https://picsum.photos/seed/dev-account-2-coin/960/540', 'https://picsum.photos/seed/dev-account-2-asset/960/540'), 'published'), + (@owner_b, '三角洲行动', 'Steam', '扫码', '纯币60M/2格/4体/4负/1体验卡', '可包夜,保险 2*1,适合短租搬砖。', '铂金', 60000000, - JSON_OBJECT('haf_coin_wan', 6000, 'season_insurance', '2*1', 'stamina_level', '4级', 'load_level', '4级', 'daily_loss', '10M', 'ratio', 4, 'login_method', '扫码', 'common_regions', JSON_ARRAY('四川', '重庆'), 'resources', JSON_ARRAY(JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 12, 'mode', '收费'), JSON_OBJECT('key', 'gridCard9', 'label', '9格体验卡', 'quantity', 1, 'mode', '收费')), 'skin_groups', JSON_OBJECT('melee', JSON_ARRAY('暗星'), 'operator', JSON_ARRAY('赛伊德'), 'weapon', JSON_ARRAY('Vector美杜莎'))), - JSON_ARRAY(), 'published'), - (@owner_b, '三角洲行动', 'QQ', '账号密码', '测试发布-QQ 三角洲巅峰 50000万成品号', + JSON_OBJECT('haf_coin_wan', 6000, 'total_asset_wan', 7200, 'season_insurance', '2*1', 'stamina_level', '4级', 'load_level', '4级', 'daily_loss', '10M', 'ratio', 4, 'price', 1500, 'settlement_amount', 1500, 'rent_days', 1, 'login_method', '扫码', 'online_time', JSON_OBJECT('start', '18:00', 'end', '02:00'), 'common_regions', JSON_ARRAY('四川', '重庆'), 'resources', JSON_ARRAY(JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 12, 'mode', '收费'), JSON_OBJECT('key', 'gridCard9', 'label', '9格体验卡', 'quantity', 1, 'mode', '收费')), 'skin_groups', JSON_OBJECT('melee', JSON_ARRAY('暗星'), 'operator', JSON_ARRAY('赛伊德'), 'weapon', JSON_ARRAY('Vector美杜莎')), 'screenshots', JSON_OBJECT('coin', 'https://picsum.photos/seed/dev-account-3-coin/960/540', 'gameId', 'https://picsum.photos/seed/dev-account-3-game/960/540', 'totalAsset', 'https://picsum.photos/seed/dev-account-3-asset/960/540')), + JSON_ARRAY('https://picsum.photos/seed/dev-account-3-coin/960/540', 'https://picsum.photos/seed/dev-account-3-asset/960/540'), 'published'), + (@owner_b, '三角洲行动', 'QQ', '账号密码', '纯币500M/9格/7体/7负/10六头', '账密交接,高段位,保险 3*3,皮肤资源较多。', '三角洲巅峰', 500000000, - JSON_OBJECT('haf_coin_wan', 50000, 'season_insurance', '3*3', 'stamina_level', '7级', 'load_level', '7级', 'daily_loss', '30M', 'ratio', 7, 'login_method', '账号密码', 'common_regions', JSON_ARRAY('北京', '浙江'), 'resources', JSON_ARRAY(JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 120, 'mode', '收费'), JSON_OBJECT('key', 'helmet6', 'label', '6头数量', 'quantity', 10, 'mode', '收费'), JSON_OBJECT('key', 'armor6Ammo', 'label', '6甲子弹', 'quantity', 300, 'mode', '收费')), 'skin_groups', JSON_OBJECT('melee', JSON_ARRAY('坠星者', '处刑者'), 'operator', JSON_ARRAY('威龙-铁面判官', '红狼-蚀金玫瑰'), 'weapon', JSON_ARRAY('M7棱镜攻势S2', 'KC17-造物纪元'))), - JSON_ARRAY(), 'published'), + JSON_OBJECT('haf_coin_wan', 50000, 'total_asset_wan', 58600, 'season_insurance', '3*3', 'stamina_level', '7级', 'load_level', '7级', 'daily_loss', '30M', 'ratio', 7, 'price', 7142.86, 'settlement_amount', 7142.86, 'rent_days', 7, 'login_method', '账号密码', 'online_time', JSON_OBJECT('start', '09:00', 'end', '23:30'), 'common_regions', JSON_ARRAY('北京', '浙江'), 'resources', JSON_ARRAY(JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 120, 'mode', '收费'), JSON_OBJECT('key', 'helmet6', 'label', '6头数量', 'quantity', 10, 'mode', '收费'), JSON_OBJECT('key', 'level6Ammo', 'label', '6级子弹', 'quantity', 300, 'mode', '收费')), 'skin_groups', JSON_OBJECT('melee', JSON_ARRAY('坠星者', '处刑者'), 'operator', JSON_ARRAY('威龙-铁面判官', '红狼-蚀金玫瑰'), 'weapon', JSON_ARRAY('M7棱镜攻势S2', 'KC17-造物纪元')), 'screenshots', JSON_OBJECT('coin', 'https://picsum.photos/seed/dev-account-4-coin/960/540', 'gameId', 'https://picsum.photos/seed/dev-account-4-game/960/540', 'totalAsset', 'https://picsum.photos/seed/dev-account-4-asset/960/540')), + JSON_ARRAY('https://picsum.photos/seed/dev-account-4-coin/960/540', 'https://picsum.photos/seed/dev-account-4-asset/960/540'), 'published'), (@owner_a, '三角洲行动', '微信', '扫码', '测试订单-待交接 微信 黑鹰 20000万', '测试订单关联账号:待交接。', '黑鹰', 200000000, @@ -91,10 +91,10 @@ VALUES JSON_OBJECT('haf_coin_wan', 5000, 'season_insurance', '2*1', 'stamina_level', '3级', 'load_level', '3级', 'daily_loss', '10M', 'ratio', 4, 'login_method', '扫码', 'common_regions', JSON_ARRAY('湖北'), 'resources', JSON_ARRAY(JSON_OBJECT('key', 'gridCard9', 'label', '9格体验卡', 'quantity', 1, 'mode', '收费')), 'skin_groups', JSON_OBJECT('melee', JSON_ARRAY('北极星'), 'operator', JSON_ARRAY('蜂医-送葬人'), 'weapon', JSON_ARRAY('MP7电玩高手S2'))), JSON_ARRAY(), 'rented'); -SELECT id INTO @account_pub_1 FROM game_accounts WHERE title = '测试发布-QQ 黑鹰 30000万哈夫币' ORDER BY id DESC LIMIT 1; -SELECT id INTO @account_pub_2 FROM game_accounts WHERE title = '测试发布-微信 钻石 12000万低押号' ORDER BY id DESC LIMIT 1; -SELECT id INTO @account_pub_3 FROM game_accounts WHERE title = '测试发布-Steam 铂金 6000万包夜号' ORDER BY id DESC LIMIT 1; -SELECT id INTO @account_pub_4 FROM game_accounts WHERE title = '测试发布-QQ 三角洲巅峰 50000万成品号' ORDER BY id DESC LIMIT 1; +SELECT id INTO @account_pub_1 FROM game_accounts WHERE title = '纯币300M/6格/6体/6负/4六甲/6六头' ORDER BY id DESC LIMIT 1; +SELECT id INTO @account_pub_2 FROM game_accounts WHERE title = '纯币120M/4格/5体/5负/2六头' ORDER BY id DESC LIMIT 1; +SELECT id INTO @account_pub_3 FROM game_accounts WHERE title = '纯币60M/2格/4体/4负/1体验卡' ORDER BY id DESC LIMIT 1; +SELECT id INTO @account_pub_4 FROM game_accounts WHERE title = '纯币500M/9格/7体/7负/10六头' ORDER BY id DESC LIMIT 1; SELECT id INTO @account_order_1 FROM game_accounts WHERE title = '测试订单-待交接 微信 黑鹰 20000万' ORDER BY id DESC LIMIT 1; SELECT id INTO @account_order_2 FROM game_accounts WHERE title = '测试订单-租赁中 QQ 钻石 10000万' ORDER BY id DESC LIMIT 1; SELECT id INTO @account_order_3 FROM game_accounts WHERE title = '测试订单-待归还 Steam 黄金 5000万' ORDER BY id DESC LIMIT 1; diff --git a/frontend/src/api/listingOptions.ts b/frontend/src/api/listingOptions.ts index 4960d9b..4dabb23 100644 --- a/frontend/src/api/listingOptions.ts +++ b/frontend/src/api/listingOptions.ts @@ -7,7 +7,7 @@ export type QuantityKey = | 'helmet6' | 'armor6' | 'kit5' - | 'armor6Ammo' + | 'level6Ammo' | 'gridCard9' export type ScreenshotKey = 'coin' | 'gameId' | 'totalAsset' | 'tencentSecurity' | 'skin' @@ -100,7 +100,7 @@ export const defaultListingPublishOptions: ListingPublishOptions = { { key: 'helmet6', label: '6头数量', price: '2元/个' }, { key: 'armor6', label: '6甲数量', price: '3元/个' }, { key: 'kit5', label: '5级全装套数量', price: '5元/个' }, - { key: 'armor6Ammo', label: '6甲子弹', price: '0.26元/发' }, + { key: 'level6Ammo', label: '6级子弹', price: '0.26元/发' }, { key: 'gridCard9', label: '9格体验卡', price: '5元/张', placeholder: '如果是赛季9格,此处填0即可' }, ], screenshot_slots: [ @@ -127,7 +127,16 @@ export function mergeListingPublishOptions(options?: Partial ({ + ...item, + key: String(item.key) === 'armor6Ammo' ? 'level6Ammo' : item.key, + })) +} diff --git a/frontend/src/views/mobile/MobileHomeView.css b/frontend/src/views/mobile/MobileHomeView.css new file mode 100644 index 0000000..83cc0dc --- /dev/null +++ b/frontend/src/views/mobile/MobileHomeView.css @@ -0,0 +1,551 @@ +.mobile-shell { + min-height: 100vh; + background: #f5f7fa; + color: #17233d; + padding-bottom: 58px; +} + +/* ========== Hero 区域:纯色蓝色头部 ========== */ +.mobile-hero { + overflow: hidden; + border-radius: 0 0 24px 24px; + background: #1479ff; + padding: 14px 14px 18px; +} + +.mobile-topbar, +.mobile-brand { + display: flex; + align-items: center; +} + +.mobile-topbar { + justify-content: space-between; + color: #ffffff; +} + +.mobile-brand { + gap: 10px; +} + +.mobile-logo { + display: grid; + width: 38px; + height: 38px; + place-items: center; + border-radius: 12px; + background: #ffffff; + color: #1479ff; + font-weight: 900; +} + +.mobile-brand strong, +.mobile-brand small { + display: block; +} + +.mobile-brand small { + margin-top: 2px; + color: rgba(255, 255, 255, 0.78); + font-size: 12px; +} + +.mobile-service { + border: 0; + border-radius: 999px; + background: rgba(255, 255, 255, 0.18); + color: #ffffff; + padding: 7px 12px; +} + +/* ========== van-search 覆盖样式 ========== */ +.home-search { + margin-top: 12px; + padding: 0; + background: transparent; +} + +.home-search :deep(.van-search__content) { + border-radius: 999px; + background: #ffffff; + padding: 8px 14px; +} + +.home-search :deep(.van-search__content .van-field__control) { + color: #17233d; + font-size: 14px; +} + +/* ========== 防骗提示卡片 ========== */ +.fraud-tip { + display: flex; + align-items: center; + gap: 8px; + margin-top: 12px; + border-radius: 12px; + background: #ffffff; + padding: 10px 14px; + font-size: 12px; + line-height: 1.45; + color: #5c3b00; +} + +.fraud-dot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: #f0a500; + flex-shrink: 0; +} + +/* ========== Content 区 ========== */ +.mobile-content { + padding: 14px; +} + +/* ========== Banner 轮播 ========== */ +.banner-swipe { + border-radius: 16px; +} + +.banner-swipe :deep(.van-swipe__indicators) { + bottom: 10px; +} + +.banner-swipe :deep(.van-swipe__indicator) { + width: 5px; + height: 5px; + background: rgba(23, 35, 61, 0.24); + opacity: 1; +} + +.banner-swipe :deep(.van-swipe__indicator--active) { + width: 16px; + border-radius: 999px; + background: #1479ff; +} + +.mobile-banner { + display: flex; + justify-content: space-between; + gap: 14px; + min-height: 132px; + border-radius: 16px; + background: #ffffff; + padding: 18px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); +} + +.mobile-banner.tone-green { + background: linear-gradient(135deg, #ffffff 0%, #f0fdf4 100%); +} + +.mobile-banner.tone-orange { + background: linear-gradient(135deg, #ffffff 0%, #fff7ed 100%); +} + +.mobile-banner p, +.mobile-banner h1 { + margin: 0; +} + +.mobile-banner p { + color: #1479ff; + font-size: 13px; + font-weight: 800; +} + +.mobile-banner h1 { + margin-top: 6px; + font-size: 22px; + line-height: 1.22; +} + +.mobile-banner span { + display: inline-flex; + margin-top: 12px; + border-radius: 999px; + background: #ffefb5; + color: #7a4b00; + padding: 6px 10px; + font-size: 12px; + font-weight: 700; +} + +.banner-badge { + align-self: flex-start; + border-radius: 12px; + background: #ff6a00; + color: #ffffff; + padding: 8px 9px; + font-size: 12px; + font-weight: 900; + transform: rotate(8deg); +} + +.tone-green .banner-badge { + background: #16a34a; +} + +.tone-orange .banner-badge { + background: #f59e0b; +} + +/* ========== 列表工具栏 ========== */ +.list-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 12px; + border-radius: 14px; + background: #fff; + padding: 14px 16px; + color: #536173; + font-size: 12px; +} + +.sort-entry, +.filter-entry { + border: 0; + background: transparent; + display: flex; + align-items: center; + cursor: pointer; +} + +.sort-entry { + gap: 4px; + color: #17233d; + padding: 0; + font-size: 15px; + font-weight: 900; +} + +.filter-entry { + position: relative; + border-radius: 999px; + color: #17233d; + padding: 0; + gap: 5px; + font-size: 14px; + font-weight: 800; +} + +.filter-entry em { + min-width: 16px; + height: 16px; + border-radius: 999px; + background: #fff; + color: #1477ff; + display: grid; + place-items: center; + font-size: 10px; + font-style: normal; +} + +.sort-panel { + margin-top: 6px; + border-radius: 0 0 14px 14px; + background: #fff; + padding: 4px 16px 14px; +} + +.sort-panel button { + display: flex; + width: 100%; + min-height: 48px; + align-items: center; + justify-content: space-between; + border: 0; + background: transparent; + color: #2d3748; + padding: 0; + font-size: 14px; + font-weight: 700; +} + +.sort-panel button.active { + color: #ff7a00; + font-weight: 900; +} + +.result-count { + margin-top: 8px; + color: #536173; + font-size: 12px; +} + +.result-count strong { + margin-right: 3px; + color: #1477ff; + font-size: 15px; + font-weight: 900; +} + +/* ========== 加载状态 ========== */ +.state-loading { + display: flex; + justify-content: center; + padding: 20px 0; + color: #6b7a90; + font-size: 13px; +} + +/* ========== 卡片列表 ========== */ +.mobile-list { + display: grid; + gap: 12px; + margin-top: 12px; +} + +.mobile-card { + display: grid; + grid-template-columns: 132px minmax(0, 1fr); + grid-template-rows: 132px 62px; + gap: 10px 12px; + min-height: 230px; + border-radius: 16px; + background: #ffffff; + padding: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); + text-decoration: none; + color: inherit; +} + +.card-cover { + position: relative; + display: grid; + width: 132px; + height: 132px; + place-items: center; + overflow: hidden; + align-self: center; + border-radius: 14px; + background: #d8d8d8; + color: #9aa4b2; + font-size: 22px; + font-weight: 900; +} + +.card-cover img { + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; +} + +.card-cover em { + position: absolute; + top: 7px; + right: 7px; + border-radius: 999px; + background: #ffd21e; + color: #1f2937; + padding: 3px 7px; + font-size: 11px; + font-style: normal; + font-weight: 800; +} + +.card-main { + display: flex; + min-height: 132px; + min-width: 0; + flex-direction: column; + justify-content: center; +} + +.card-title-row { + display: flex; + align-items: flex-start; +} + +.card-title-row h2 { + overflow: hidden; + margin: 0; + color: #182238; + display: -webkit-box; + font-size: 15.5px; + font-weight: 800; + line-height: 1.42; + min-height: 44px; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + min-width: 0; +} + +.card-subtitle { + margin: 5px 0 0; + color: #6b7280; + font-size: 12px; + font-weight: 600; + line-height: 1.35; +} + +.card-badges-row, +.card-chip-row { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.card-badges-row { + margin-top: 9px; +} + +.trust-badge, +.server-badge { + display: inline-flex; + align-items: center; + min-height: 25px; + border-radius: 6px; + padding: 0 7px; + font-size: 12px; + font-weight: 800; +} + +.trust-badge { + background: #fff4dc; + color: #ff7900; +} + +.server-badge { + background: #eaf4ff; + color: #1477ff; +} + +.card-footer { + display: flex; + align-items: flex-end; + justify-content: flex-start; + gap: 10px; + margin-top: 11px; +} + +.price-col { + display: flex; + align-items: baseline; + gap: 8px; + min-width: 0; +} + +.card-footer strong { + color: #ff1f35; + font-size: 21px; + line-height: 1; +} + +.card-footer .rent-sub { + color: #8a96a8; + font-size: 12px; + font-weight: 700; + white-space: nowrap; +} + +.card-chip-row { + grid-column: 1 / -1; + align-content: flex-start; + margin-top: 0; + min-height: 62px; +} + +.card-chip-row span { + display: inline-flex; + align-items: center; + min-height: 25px; + border: 1px solid #ff8a1f; + border-radius: 4px; + color: #ff7900; + padding: 0 5px; + font-size: 11.5px; + font-weight: 700; +} + +@media (max-width: 374px) { + .mobile-card { + grid-template-columns: 112px minmax(0, 1fr); + grid-template-rows: 112px 62px; + min-height: 210px; + } + + .card-cover { + width: 112px; + height: 112px; + } + + .card-main { + min-height: 112px; + } + + .card-title-row h2 { + font-size: 14.5px; + } + + .card-footer strong { + font-size: 20px; + } +} + +/* ========== 底部导航:原生App风格 ========== */ +.bottom-nav { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 100; + display: flex; + height: 50px; + background: #fff; + border-top: 1px solid #eee; +} + +.nav-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; + color: #999; + font-size: 10px; + text-decoration: none; +} + +.nav-item.active { + color: #1477ff; +} + +.nav-item span { + font-weight: 600; +} + +.publish-pill { + width: 36px; + height: 26px; + display: grid; + place-items: center; + border-radius: 13px; + background: #ff6a00; + color: #fff; + font-size: 18px; + font-weight: 900; +} + +.nav-publish span { + color: #ff6a00; +} + +/* ========== 响应式适配 ========== */ +@media (min-width: 520px) { + .mobile-shell { + max-width: 430px; + margin: 0 auto; + box-shadow: 0 0 0 1px rgba(23, 35, 61, 0.08); + } + + .bottom-nav { + left: calc((100vw - 430px) / 2); + right: calc((100vw - 430px) / 2); + } +} diff --git a/frontend/src/views/mobile/MobileHomeView.vue b/frontend/src/views/mobile/MobileHomeView.vue index 0228822..a2a02e2 100644 --- a/frontend/src/views/mobile/MobileHomeView.vue +++ b/frontend/src/views/mobile/MobileHomeView.vue @@ -12,6 +12,26 @@ import { fetchListings, type Listing } from "@/api/listings"; import MobileHomeFilterSheet, { type FilterSection, } from "./MobileHomeFilterSheet.vue"; +import { buildFallbackListings } from "./mobileHomeFallback"; +import { + assetRegions, + formatHafCoinM, + getCoinM, + getCoinWan, + getListingChips, + getListingDisplayPrice, + getListingSubtitle, + getListingTitle, + getLoginMethod, + getResourceQuantity, + getServerRegion, + getSkinGroup, + hasGiftResources, + normalizeLoginMethod, + normalizeServerRegion, + readAssetNumber, + readAssetString, +} from "./listingDisplay"; const route = useRoute(); const loading = ref(false); @@ -35,12 +55,11 @@ const sortOptions = [ const rangePresets: Record> = { coin: [ - { label: "10-50", min: "10", max: "50" }, { label: "50-100", min: "50", max: "100" }, - { label: "100-150", min: "100", max: "150" }, - { label: "150-200", min: "150", max: "200" }, + { label: "100-200", min: "100", max: "200" }, { label: "200-300", min: "200", max: "300" }, - { label: "300以上", min: "300", max: "" }, + { label: "300-500", min: "300", max: "500" }, + { label: "500以上", min: "500", max: "" }, ], resource_awmAmmo: [ { label: "0-20", min: "0", max: "20" }, @@ -75,141 +94,10 @@ const bannerSlides = [ }, ]; -const fallbackListings: Listing[] = [ - { - id: 9001, - account_id: 0, - owner_id: 0, - title: "QQ区 黑鹰 30000万哈夫币账号", - description: "支持扫码上号,号主在线交接,保险 2*3,适合高强度体验。", - game_name: "三角洲行动", - server_region: "QQ", - login_platform: "扫码", - rank_level: "黑鹰", - haf_coin_amount: 300000000, - asset_summary: { - haf_coin_wan: 30000, - season_insurance: "2*3", - stamina_level: "6级", - load_level: "6级", - daily_loss: "30M", - ratio: 6, - login_method: "扫码", - common_regions: ["广东", "上海"], - resources: [ - { key: "awmAmmo", label: "AWM子弹", quantity: 80, mode: "收费" }, - { key: "helmet6", label: "6头数量", quantity: 6, mode: "收费" }, - { key: "armor6", label: "6甲数量", quantity: 4, mode: "收费" }, - ], - skin_groups: { - melee: ["龙牙", "信条"], - operator: ["威龙-凌霄戍卫"], - weapon: ["M4A1棱镜攻势", "K416命运"], - }, - }, - screenshot_urls: [], - price_hourly: 8.8, - price_daily: 88, - price_weekly: 520, - deposit_amount: 120, - min_rent_hours: 24, - max_rent_hours: 168, - status: "published", - review_status: "approved", - review_reason: "", - created_at: "", - updated_at: "", - }, - { - id: 9002, - account_id: 0, - owner_id: 0, - title: "微信 钻石 12000万哈夫币低押号", - description: "账密登录,低押金,皮肤截图齐全,常用登录地江苏。", - game_name: "三角洲行动", - server_region: "微信", - login_platform: "账号密码", - rank_level: "钻石", - haf_coin_amount: 120000000, - asset_summary: { - haf_coin_wan: 12000, - season_insurance: "2*2", - stamina_level: "5级", - load_level: "5级", - daily_loss: "20M", - ratio: 5, - login_method: "账号密码", - common_regions: ["江苏"], - resources: [ - { key: "awmAmmo", label: "AWM子弹", quantity: 30, mode: "收费" }, - { key: "helmet6", label: "6头数量", quantity: 2, mode: "收费" }, - ], - skin_groups: { - melee: ["怜悯"], - operator: ["露娜-黑天际线"], - weapon: ["SCAR-H电玩高手"], - }, - }, - screenshot_urls: [], - price_hourly: 5.5, - price_daily: 55, - price_weekly: 330, - deposit_amount: 60, - min_rent_hours: 24, - max_rent_hours: 72, - status: "published", - review_status: "approved", - review_reason: "", - created_at: "", - updated_at: "", - }, - { - id: 9003, - account_id: 0, - owner_id: 0, - title: "Steam 铂金 6000万哈夫币包夜号", - description: "可包夜,保险 2*1,适合短租搬砖体验。", - game_name: "三角洲行动", - server_region: "Steam", - login_platform: "扫码", - rank_level: "铂金", - haf_coin_amount: 60000000, - asset_summary: { - haf_coin_wan: 6000, - season_insurance: "2*1", - stamina_level: "4级", - load_level: "4级", - daily_loss: "10M", - ratio: 4, - login_method: "扫码", - common_regions: ["四川", "重庆"], - resources: [ - { key: "awmAmmo", label: "AWM子弹", quantity: 12, mode: "收费" }, - { key: "gridCard9", label: "9格体验卡", quantity: 1, mode: "收费" }, - ], - skin_groups: { - melee: ["暗星"], - operator: ["赛伊德"], - weapon: ["Vector美杜莎"], - }, - }, - screenshot_urls: [], - price_hourly: 3.9, - price_daily: 39, - price_weekly: 220, - deposit_amount: 80, - min_rent_hours: 8, - max_rent_hours: 24, - status: "published", - review_status: "approved", - review_reason: "", - created_at: "", - updated_at: "", - }, -]; +const fallbackListings = computed(() => buildFallbackListings(publishOptions.value)); const sourceListings = computed(() => - loadFailed.value ? fallbackListings : listings.value + loadFailed.value ? fallbackListings.value : listings.value ); const activeSortLabel = computed( @@ -235,8 +123,8 @@ const loginMethodFilterOptions = computed(() => ); const filterSections = computed(() => [ - { key: "price", title: "价格区间", type: "range", unit: "元/小时", minPlaceholder: "最低价", maxPlaceholder: "最高价" }, - { key: "coin", title: "哈夫币数量", type: "range", unit: "万", minPlaceholder: "最低", maxPlaceholder: "最高" }, + { key: "price", title: "价格区间", type: "range", unit: "元", minPlaceholder: "最低价", maxPlaceholder: "最高价" }, + { key: "coin", title: "哈夫币数量", type: "range", unit: "M", minPlaceholder: "最低", maxPlaceholder: "最高" }, { key: "server", title: "区服", type: "chips", options: serverFilterOptions.value }, { key: "login", title: "上号方式", type: "chips", options: loginMethodFilterOptions.value }, { key: "insurance", title: "保险", type: "chips", options: publishOptions.value.insurance_options }, @@ -357,12 +245,12 @@ function sortListings(items: Listing[]) { } if (activeSort.value === "priceAsc") { return sorted.sort( - (a, b) => Number(a.price_hourly || 0) - Number(b.price_hourly || 0) + (a, b) => getListingDisplayPrice(a) - getListingDisplayPrice(b) ); } if (activeSort.value === "priceDesc") { return sorted.sort( - (a, b) => Number(b.price_hourly || 0) - Number(a.price_hourly || 0) + (a, b) => getListingDisplayPrice(b) - getListingDisplayPrice(a) ); } return sorted.sort((a, b) => { @@ -391,8 +279,8 @@ function matchesFilters(item: Listing) { return Object.entries(rangeFilters.value).every(([key, range]) => { if (!range.min && !range.max) return true; let value = 0; - if (key === "price") value = Number(item.price_hourly || 0); - if (key === "coin") value = getCoinWan(item); + if (key === "price") value = getListingDisplayPrice(item); + if (key === "coin") value = getCoinM(item); if (key === "secretKd") value = readAssetNumber(item, "secret_kd"); if (key === "deposit") value = Number(item.deposit_amount || 0); if (key.startsWith("resource_")) { @@ -420,7 +308,7 @@ function searchText(item: Listing) { readAssetString(item, "season_insurance"), readAssetString(item, "stamina_level"), readAssetString(item, "load_level"), - formatCoinWan(getCoinWan(item)), + formatHafCoinM(getCoinWan(item)), assetRegions(item).join(" "), ...publishOptions.value.skin_groups.map((group) => getSkinGroup(item, group.key).join(" ") @@ -430,39 +318,6 @@ function searchText(item: Listing) { .toLowerCase(); } -function getCoinWan(item: Listing) { - const assetCoin = readAssetNumber(item, "haf_coin_wan"); - if (assetCoin > 0) return assetCoin; - return Math.round(Number(item.haf_coin_amount || 0) / 10000); -} - -function getLoginMethod(item: Listing) { - return normalizeLoginMethod( - readAssetString(item, "login_method") || item.login_platform - ); -} - -function getServerRegion(item: Listing) { - return normalizeServerRegion(item.server_region); -} - -function normalizeServerRegion(value: string) { - const text = value.trim(); - const upper = text.toUpperCase(); - if (upper.includes("STEAM")) return "Steam"; - if (upper.includes("QQ")) return "QQ"; - if (text.includes("微信")) return "微信"; - return ""; -} - -function normalizeLoginMethod(value: string) { - const text = value.trim(); - if (!text) return ""; - if (text.includes("扫码")) return "扫码"; - if (text.includes("账密") || text.includes("账号密码")) return "账号密码"; - return ""; -} - function isSkinGroupKey(key: string) { return publishOptions.value.skin_groups.some((group) => group.key === key); } @@ -476,80 +331,6 @@ function uniqueOptions(values: string[]) { return [...new Set(values.map((item) => item.trim()).filter(Boolean))]; } -function getRatio(item: Listing) { - const ratio = readAssetNumber(item, "ratio"); - if (ratio > 0) return ratio; - const price = Number(item.price_hourly || 0); - if (price <= 0) return 0; - return getCoinWan(item) / price; -} - -function assetRegions(item: Listing) { - const regions = item.asset_summary?.common_regions; - return Array.isArray(regions) - ? regions.filter((region): region is string => typeof region === "string") - : []; -} - -function getResourceQuantity(item: Listing, resourceKey: string) { - const resources = item.asset_summary?.resources; - if (!Array.isArray(resources)) return 0; - const found = resources.find((resource) => { - return ( - typeof resource === "object" && - resource !== null && - "key" in resource && - (resource as { key?: unknown }).key === resourceKey - ); - }) as { quantity?: unknown } | undefined; - const quantity = found?.quantity; - if (typeof quantity === "number") return quantity; - if (typeof quantity === "string" && quantity.trim()) return Number(quantity) || 0; - return 0; -} - -function getSkinGroup(item: Listing, groupKey: string) { - const skinGroups = item.asset_summary?.skin_groups; - if ( - typeof skinGroups !== "object" || - skinGroups === null || - !Array.isArray((skinGroups as Record)[groupKey]) - ) { - return []; - } - return ((skinGroups as Record)[groupKey] as unknown[]).filter( - (skin): skin is string => typeof skin === "string" - ); -} - -function readAssetString(item: Listing, key: string) { - const value = item.asset_summary?.[key]; - return typeof value === "string" ? value : ""; -} - -function readAssetNumber(item: Listing, key: string) { - const value = item.asset_summary?.[key]; - if (typeof value === "number") return value; - if (typeof value === "string" && value.trim()) { - const parsed = Number(value); - return Number.isFinite(parsed) ? parsed : 0; - } - return 0; -} - -function formatCoinWan(amountWan: number) { - if (amountWan >= 10000) { - const yi = amountWan / 10000; - return `${Number.isInteger(yi) ? yi : yi.toFixed(1)}亿`; - } - return `${amountWan}万`; -} - -function formatRatio(item: Listing) { - const ratio = getRatio(item); - return ratio > 0 ? `1:${Math.round(ratio)}` : "--"; -} - - + diff --git a/frontend/src/views/mobile/MobileListingDetailView.vue b/frontend/src/views/mobile/MobileListingDetailView.vue index 7093f68..763a39c 100644 --- a/frontend/src/views/mobile/MobileListingDetailView.vue +++ b/frontend/src/views/mobile/MobileListingDetailView.vue @@ -4,8 +4,27 @@ import { useRoute, useRouter } from "vue-router"; import { showToast, showDialog } from "vant"; import { fetchListing, type Listing } from "@/api/listings"; +import { defaultListingPublishOptions } from "@/api/listingOptions"; import { createOrder } from "@/api/orders"; import { useSessionStore } from "@/stores/session"; +import { buildFallbackListings } from "./mobileHomeFallback"; +import { + assetRegions, + formatHafCoinM, + formatRatio, + getCoinWan, + getListingChips, + getListingDisplayPrice, + getListingResources, + getListingSubtitle, + getListingTitle, + getLoginMethod, + getRentDays, + getScreenshotMap, + getServerRegion, + readAssetNumber, + readAssetString, +} from "./listingDisplay"; const route = useRoute(); const router = useRouter(); @@ -19,8 +38,17 @@ onMounted(async () => { loading.value = true; try { listing.value = await fetchListing(String(route.params.id)); + rentHours.value = Math.max(listing.value.min_rent_hours || 1, 1); } catch { - showToast({ message: "加载失败", icon: "warning-o" }); + const fallback = buildFallbackListings(defaultListingPublishOptions).find( + (item) => String(item.id) === String(route.params.id) + ); + if (fallback) { + listing.value = fallback; + rentHours.value = Math.max(fallback.min_rent_hours || 1, 1); + } else { + showToast({ message: "加载失败", icon: "warning-o" }); + } } finally { loading.value = false; } @@ -32,6 +60,61 @@ const totalRent = computed(() => { return (listing.value.price_hourly * rentHours.value).toFixed(2); }); +const detailMetrics = computed(() => { + if (!listing.value) return []; + const totalAssetWan = readAssetNumber(listing.value, "total_asset_wan"); + return [ + { label: "纯币", value: formatHafCoinM(getCoinWan(listing.value)), tone: "coin" }, + { + label: "总资产", + value: totalAssetWan > 0 ? formatHafCoinM(totalAssetWan) : "--", + tone: "coin", + }, + { label: "价格", value: `¥${getListingDisplayPrice(listing.value)}`, tone: "price" }, + { label: "押金", value: `¥${listing.value.deposit_amount}`, tone: "" }, + ]; +}); + +const detailScreenshots = computed(() => { + if (!listing.value) return []; + const screenshotMap = getScreenshotMap(listing.value); + const labels: Record = { + coin: "纯币截图", + gameId: "游戏ID", + totalAsset: "总资产", + tencentSecurity: "腾讯安全中心", + skin: "皮肤截图", + }; + const mapped = Object.entries(screenshotMap) + .filter(([, url]) => typeof url === "string" && url) + .map(([key, url]) => ({ label: labels[key] || "账号截图", url: String(url) })); + if (mapped.length) return mapped; + return (listing.value.screenshot_urls || []).map((url, index) => ({ + label: `账号截图${index + 1}`, + url, + })); +}); + +const detailSkinGroups = computed(() => { + if (!listing.value) return []; + const groups = listing.value.asset_summary?.skin_groups; + if (typeof groups !== "object" || groups === null) return []; + const titles: Record = { + melee: "近战皮肤", + operator: "干员皮肤", + weapon: "武器皮肤", + }; + return Object.entries(groups as Record) + .map(([key, value]) => ({ + key, + title: titles[key] || key, + options: Array.isArray(value) + ? value.filter((skin): skin is string => typeof skin === "string") + : [], + })) + .filter((group) => group.options.length); +}); + /* 下单 */ async function handleCreateOrder() { if (!listing.value) return; @@ -125,9 +208,9 @@ function isNavActive(path: string) {
@@ -136,10 +219,10 @@ function isNavActive(path: string) {
- {{ listing.screenshot_urls?.length }}张 + {{ detailScreenshots.length }}张
@@ -147,71 +230,102 @@ function isNavActive(path: string) {
{{ - listing.server_region - }} - {{ - listing.login_platform + getServerRegion(listing) }} + + {{ getLoginMethod(listing) }} + {{ listing.rank_level }} + + 损耗 {{ readAssetString(listing, "daily_loss") }} +
-

{{ listing.title }}

-

- {{ listing.description || "号主暂未填写详细说明。" }} -

+

{{ getListingTitle(listing) }}

+

{{ getListingSubtitle(listing) }}

-
- 哈夫币 - {{ - listing.haf_coin_amount - }} -
-
- 时租 - ¥{{ listing.price_hourly }} -
-
- 日租 - ¥{{ listing.price_daily }} -
-
- 押金 - ¥{{ listing.deposit_amount }} +
+ {{ metric.label }} + {{ metric.value }}
- +
-
- 最短租期 - {{ listing.min_rent_hours }}小时 +

账号资料

+
+ + {{ chip.label }}:{{ chip.value }} +
- 最长租期 - {{ listing.max_rent_hours }}小时 + 比例 + {{ formatRatio(listing) }}
- 状态 - {{ listing.status }} + 租期 + {{ getRentDays(listing) }}天
+
+ 常用登录地 + {{ assetRegions(listing).join("、") || "--" }} +
+
+ 封禁记录 + {{ readAssetString(listing, "ban_record") }} +
+
+ +
+

资源道具

+
+
+ {{ resource.label }} + {{ resource.quantity }} + {{ resource.mode }} +
+
+
+ +
+

皮肤

+
+

{{ group.title }}

+
+ {{ skin }} +
+
+
+ +
+

备注

+

{{ listing.description }}

-
+

账号截图

- +
+ +
{{ shot.label }}
+
@@ -380,10 +494,10 @@ function isNavActive(path: string) { .detail-title { margin: 0 0 6px; - font-size: 17px; + font-size: 20px; font-weight: 700; color: #1a1a1a; - line-height: 1.3; + line-height: 1.35; } .detail-desc { @@ -418,8 +532,11 @@ function isNavActive(path: string) { .metric-value { display: block; + overflow: hidden; font-size: 15px; color: #1a1a1a; + text-overflow: ellipsis; + white-space: nowrap; } .metric-value.coin { @@ -459,20 +576,97 @@ function isNavActive(path: string) { font-weight: 700; } +.detail-chip-row { + display: flex; + flex-wrap: wrap; + gap: 7px; + margin-bottom: 10px; +} + +.detail-chip-row span { + display: inline-flex; + align-items: center; + min-height: 28px; + border: 1px solid #ff8a1f; + border-radius: 5px; + color: #ff7900; + padding: 0 7px; + font-size: 12px; + font-weight: 800; +} + +.resource-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.resource-pill { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 4px 8px; + border-radius: 8px; + background: #f7f9fc; + padding: 10px; +} + +.resource-pill span { + min-width: 0; + color: #5f6b7a; + font-size: 12px; + font-weight: 700; +} + +.resource-pill strong { + color: #17233d; + font-size: 14px; +} + +.resource-pill em { + grid-column: 1 / -1; + color: #ff7900; + font-size: 11px; + font-style: normal; + font-weight: 800; +} + +.skin-detail-group + .skin-detail-group { + margin-top: 12px; +} + +.skin-detail-group p { + margin: 0 0 8px; + color: #5f6b7a; + font-size: 12px; + font-weight: 900; +} + .screenshot-grid { display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 6px; + grid-template-columns: repeat(2, 1fr); + gap: 8px; +} + +.screenshot-item { + margin: 0; } .screenshot-thumb { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; - border-radius: 6px; + border-radius: 8px; cursor: pointer; } +.screenshot-item figcaption { + margin-top: 4px; + color: #6b7280; + font-size: 11px; + font-weight: 700; + text-align: center; +} + /* ========== 底部留白 ========== */ .bottom-spacer { height: 20px; diff --git a/frontend/src/views/mobile/MobileSellerListingCreateView.vue b/frontend/src/views/mobile/MobileSellerListingCreateView.vue index e565145..2356286 100644 --- a/frontend/src/views/mobile/MobileSellerListingCreateView.vue +++ b/frontend/src/views/mobile/MobileSellerListingCreateView.vue @@ -54,7 +54,7 @@ const quantityValues = reactive>({ helmet6: 0, armor6: 0, kit5: 0, - armor6Ammo: 0, + level6Ammo: 0, gridCard9: 0, }); @@ -63,7 +63,7 @@ const quantityModes = reactive>({ helmet6: "收费", armor6: "收费", kit5: "收费", - armor6Ammo: "收费", + level6Ammo: "收费", gridCard9: "收费", }); diff --git a/frontend/src/views/mobile/listingDisplay.ts b/frontend/src/views/mobile/listingDisplay.ts new file mode 100644 index 0000000..b03fe06 --- /dev/null +++ b/frontend/src/views/mobile/listingDisplay.ts @@ -0,0 +1,256 @@ +import type { Listing } from "@/api/listings"; + +export interface ListingDisplayChip { + label: string; + value: string; +} + +export interface ListingDisplayResource { + key: string; + label: string; + quantity: number; + mode: string; +} + +export function getCoinWan(item: Listing) { + const assetCoin = readAssetNumber(item, "haf_coin_wan"); + if (assetCoin > 0) return assetCoin; + return Math.round(Number(item.haf_coin_amount || 0) / 10000); +} + +export function getCoinM(item: Listing) { + return getCoinWan(item) / 100; +} + +export function formatHafCoinM(amountWan: number) { + const amountM = amountWan / 100; + const rounded = Math.round(amountM * 10) / 10; + return `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)}M`; +} + +export function getListingDisplayPrice(item: Listing) { + const assetPrice = readAssetNumber(item, "price"); + if (assetPrice > 0) return assetPrice; + const rentDays = getRentDays(item); + if (rentDays > 0 && item.price_daily > 0) { + return roundMoney(item.price_daily * rentDays); + } + return Number(item.price_daily || item.price_hourly || 0); +} + +export function getRentDays(item: Listing) { + const assetDays = readAssetNumber(item, "rent_days"); + if (assetDays > 0) return assetDays; + if (item.max_rent_hours >= 24) return Math.max(Math.round(item.max_rent_hours / 24), 1); + return 1; +} + +export function getRatioValue(item: Listing) { + const ratio = readAssetNumber(item, "ratio"); + if (ratio > 0) return ratio; + const price = getListingDisplayPrice(item); + if (price <= 0) return 0; + return getCoinWan(item) / price; +} + +export function formatRatio(item: Listing) { + const ratio = getRatioValue(item); + return ratio > 0 ? `1:${Math.round(ratio)}` : "--"; +} + +export function getValuePerYuanText(item: Listing) { + const price = getListingDisplayPrice(item); + if (price <= 0) return ""; + return `1元=${Math.round(getCoinWan(item) / price)}w哈夫币`; +} + +export function getLoginMethod(item: Listing) { + return normalizeLoginMethod(readAssetString(item, "login_method") || item.login_platform); +} + +export function getServerRegion(item: Listing) { + return normalizeServerRegion(item.server_region); +} + +export function getListingTitle(item: Listing) { + const parts = [ + `纯币${formatHafCoinM(getCoinWan(item))}`, + formatInsuranceSlotText(readAssetString(item, "season_insurance")), + formatLevelShort(readAssetString(item, "stamina_level"), "体"), + formatLevelShort(readAssetString(item, "load_level"), "负"), + formatResourceShort(item, "armor6", "六甲"), + formatResourceShort(item, "helmet6", "六头"), + ...getSkinNames(item).slice(0, 4), + ].filter(Boolean); + return parts.join("/"); +} + +export function getListingSubtitle(item: Listing) { + return [getValuePerYuanText(item), `租期${getRentDays(item)}天`] + .filter(Boolean) + .join(" "); +} + +export function getListingChips(item: Listing): ListingDisplayChip[] { + const totalAsset = readAssetNumber(item, "total_asset_wan"); + const chips: ListingDisplayChip[] = [ + { label: "哈夫币", value: formatHafCoinM(getCoinWan(item)) }, + { label: "保险格数", value: readAssetString(item, "season_insurance") }, + { label: "体力", value: readAssetString(item, "stamina_level") }, + { label: "负重", value: readAssetString(item, "load_level") }, + { label: "段位", value: item.rank_level }, + ]; + const awmAmmo = getResourceQuantity(item, "awmAmmo"); + if (awmAmmo > 0) { + chips.push({ label: "AWM", value: `${awmAmmo}发` }); + } + if (totalAsset > 0) { + chips.push({ label: "总资产", value: formatHafCoinM(totalAsset) }); + } + const online = getOnlineTimeText(item); + if (online) { + chips.push({ label: "方便上号", value: online }); + } + return chips.filter((chip) => chip.value); +} + +export function getListingResources(item: Listing): ListingDisplayResource[] { + const resources = item.asset_summary?.resources; + if (!Array.isArray(resources)) return []; + return resources + .map((resource) => { + if (typeof resource !== "object" || resource === null) return null; + const row = resource as Record; + return { + key: typeof row.key === "string" ? row.key : "", + label: typeof row.label === "string" ? row.label : "", + quantity: readUnknownNumber(row.quantity), + mode: typeof row.mode === "string" ? row.mode : "", + }; + }) + .filter((resource): resource is ListingDisplayResource => { + return Boolean(resource?.key && resource.label && resource.quantity > 0); + }); +} + +export function getResourceQuantity(item: Listing, resourceKey: string) { + const keys = getResourceKeyAliases(resourceKey); + return getListingResources(item).find((resource) => keys.includes(resource.key))?.quantity || 0; +} + +export function hasGiftResources(item: Listing) { + return getListingResources(item).some((resource) => resource.mode === "赠送"); +} + +export function getSkinGroup(item: Listing, groupKey: string) { + const skinGroups = item.asset_summary?.skin_groups; + if ( + typeof skinGroups !== "object" || + skinGroups === null || + !Array.isArray((skinGroups as Record)[groupKey]) + ) { + return []; + } + return ((skinGroups as Record)[groupKey] as unknown[]).filter( + (skin): skin is string => typeof skin === "string" + ); +} + +export function getSkinNames(item: Listing) { + const skinGroups = item.asset_summary?.skin_groups; + if (typeof skinGroups === "object" && skinGroups !== null) { + return Object.values(skinGroups as Record) + .flatMap((group) => (Array.isArray(group) ? group : [])) + .filter((skin): skin is string => typeof skin === "string"); + } + const skins = item.asset_summary?.skins; + return Array.isArray(skins) ? skins.filter((skin): skin is string => typeof skin === "string") : []; +} + +export function assetRegions(item: Listing) { + const regions = item.asset_summary?.common_regions; + return Array.isArray(regions) + ? regions.filter((region): region is string => typeof region === "string") + : []; +} + +export function getScreenshotMap(item: Listing) { + const screenshots = item.asset_summary?.screenshots; + return typeof screenshots === "object" && screenshots !== null + ? (screenshots as Record) + : {}; +} + +export function getOnlineTimeText(item: Listing) { + const onlineTime = item.asset_summary?.online_time; + if (typeof onlineTime !== "object" || onlineTime === null) return ""; + const start = (onlineTime as Record).start; + const end = (onlineTime as Record).end; + if (typeof start !== "string" || typeof end !== "string" || !start || !end) return ""; + return `${start.replace(":00", "")}-${end.replace(":00", "")}点`; +} + +export function readAssetString(item: Listing, key: string) { + const value = item.asset_summary?.[key]; + return typeof value === "string" ? value : ""; +} + +export function readAssetNumber(item: Listing, key: string) { + return readUnknownNumber(item.asset_summary?.[key]); +} + +function readUnknownNumber(value: unknown) { + if (typeof value === "number") return value; + if (typeof value === "string" && value.trim()) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; + } + return 0; +} + +export function normalizeServerRegion(value: string) { + const text = value.trim(); + const upper = text.toUpperCase(); + if (upper.includes("STEAM")) return "Steam"; + if (upper.includes("QQ")) return "QQ"; + if (text.includes("微信")) return "微信"; + return text; +} + +export function normalizeLoginMethod(value: string) { + const text = value.trim(); + if (!text) return ""; + if (text.includes("扫码")) return "扫码"; + if (text.includes("账密") || text.includes("账号密码")) return "账号密码"; + return text; +} + +function formatInsuranceSlotText(value: string) { + const parts = value.split("*").map((item) => Number(item)); + const rows = parts[0] || 0; + const cols = parts[1] || 0; + if (!Number.isFinite(rows) || !Number.isFinite(cols) || rows <= 0 || cols <= 0) { + return value; + } + return `${rows * cols}格`; +} + +function formatLevelShort(value: string, suffix: string) { + const level = value.match(/\d+/)?.[0]; + return level ? `${level}${suffix}` : value; +} + +function formatResourceShort(item: Listing, key: string, label: string) { + const quantity = getResourceQuantity(item, key); + return quantity > 0 ? `${quantity}${label}` : ""; +} + +function getResourceKeyAliases(key: string) { + if (key === "level6Ammo") return ["level6Ammo", "armor6Ammo"]; + if (key === "armor6Ammo") return ["armor6Ammo", "level6Ammo"]; + return [key]; +} + +function roundMoney(value: number) { + return Math.round(value * 100) / 100; +} diff --git a/frontend/src/views/mobile/mobileHomeFallback.ts b/frontend/src/views/mobile/mobileHomeFallback.ts new file mode 100644 index 0000000..58a50ec --- /dev/null +++ b/frontend/src/views/mobile/mobileHomeFallback.ts @@ -0,0 +1,177 @@ +import { + defaultListingPublishOptions, + type ListingPublishOptions, +} from "@/api/listingOptions"; +import type { Listing } from "@/api/listings"; + +const fallbackRegionOptions = ["广东", "上海", "江苏", "四川", "北京", "浙江"]; + +const coinWanOptions = [ + 1800, 3200, 5800, 9600, 12000, 18000, 26000, 36000, 52000, 68000, +]; +const depositOptions = [60, 80, 100, 120, 160, 200]; +const secretKdOptions = [1.2, 1.6, 2.1, 2.8, 3.4, 4.2]; +const onlineWindows = [ + ["09:00", "23:30"], + ["10:30", "01:00"], + ["12:00", "24:00"], + ["18:00", "02:00"], +] as const; +const resourceQuantityPresets: Record = { + awmAmmo: [0, 20, 48, 80, 120, 180], + helmet6: [0, 2, 4, 6, 10, 16], + armor6: [0, 1, 3, 5, 8, 12], + kit5: [0, 3, 6, 10, 15, 24], + level6Ammo: [0, 120, 240, 360, 520, 800], + gridCard9: [0, 1, 2, 4, 6, 10], +}; + +function pick(items: readonly T[], index: number, fallback: T): T { + if (!items.length) return fallback; + return items[index % items.length] ?? fallback; +} + +function pickMany(items: string[], start: number, count: number) { + if (!items.length) return []; + const fallback = items[0] || ""; + return Array.from({ length: Math.min(count, items.length) }, (_, idx) => + pick(items, start + idx, fallback) + ).filter(Boolean); +} + +function buildResources(options: ListingPublishOptions, seed: number) { + return options.quantity_items.map((item, index) => ({ + key: item.key, + label: item.label, + quantity: pick( + resourceQuantityPresets[item.key] || [0, 1, 3, 6], + seed + index, + 0 + ), + mode: "收费", + })); +} + +function buildSkinGroups(options: ListingPublishOptions, seed: number) { + return options.skin_groups.reduce>((groups, group, index) => { + const count = group.key === "melee" ? 2 : index % 2 === 0 ? 3 : 2; + groups[group.key] = pickMany(group.options, seed + index * 2, count); + return groups; + }, {}); +} + +function buildScreenshotMap(options: ListingPublishOptions, seed: number) { + return options.screenshot_slots.reduce>((screenshots, slot, index) => { + screenshots[slot.key] = `https://picsum.photos/seed/hfb-${slot.key}-${seed + index}/960/540`; + return screenshots; + }, {}); +} + +function flattenSkinGroups(groups: Record) { + return Object.values(groups).flat(); +} + +function calculateRentDays(coinWan: number) { + return coinWan >= 30000 ? 7 : coinWan >= 10000 ? 3 : 1; +} + +function calculateDailyLoss(coinWan: number) { + return coinWan >= 30000 ? "30M" : coinWan >= 10000 ? "20M" : "10M"; +} + +function roundMoney(value: number) { + return Math.round(value * 100) / 100; +} + +function formatHafCoinM(coinWan: number) { + const coinM = Math.round((coinWan / 100) * 10) / 10; + return `${Number.isInteger(coinM) ? coinM : coinM.toFixed(1)}M`; +} + +export function buildFallbackListings( + options: ListingPublishOptions = defaultListingPublishOptions +): Listing[] { + return Array.from({ length: 10 }, (_, index) => { + const seed = index + 1; + const server = pick(options.server_options, index, "QQ"); + const loginMethod = pick(options.login_method_options, index + 1, "账号密码"); + const rank = pick(options.rank_options, index + 3, "钻石"); + const insurance = pick(options.insurance_options, index + 1, "2*2"); + const stamina = pick(options.level_options, index + 4, "5级"); + const load = pick(options.level_options, index + 5, "5级"); + const faceOwner = pick(options.face_options, index, "是"); + const coinWan = pick(coinWanOptions, index, 12000); + const ratio = 5 + (index % 4); + const rentDays = calculateRentDays(coinWan); + const finalPrice = roundMoney(coinWan / ratio); + const settlementAmount = finalPrice; + const dailyPrice = roundMoney(finalPrice / rentDays); + const hourlyPrice = Math.max(roundMoney(dailyPrice / 24), 0.01); + const deposit = pick(depositOptions, index, 100); + const skinGroups = buildSkinGroups(options, seed); + const screenshots = buildScreenshotMap(options, seed); + const commonRegions = pickMany( + options.region_options.length ? options.region_options : fallbackRegionOptions, + index * 2, + 2 + ); + const [onlineStart, onlineEnd] = pick(onlineWindows, index, onlineWindows[0]); + + return { + id: 9100 + seed, + account_id: 0, + owner_id: 0, + title: `${server} ${rank} ${formatHafCoinM(coinWan)}哈夫币`, + description: `${loginMethod}交接,${insurance}保险,体力${stamina}、负重${load},示例数据按发布表单随机生成。`, + game_name: "三角洲行动", + server_region: server, + login_platform: loginMethod, + rank_level: rank, + haf_coin_amount: coinWan * 10000, + asset_summary: { + face_owner: faceOwner, + haf_coin_wan: coinWan, + total_asset_wan: Math.round(coinWan * (1.08 + (index % 4) * 0.04)), + secret_kd: pick(secretKdOptions, index, 1.6), + fire_level: 38 + seed * 3, + season_insurance: insurance, + stamina_level: stamina, + load_level: load, + daily_loss: calculateDailyLoss(coinWan), + ratio, + login_method: loginMethod, + common_regions: commonRegions, + resources: buildResources(options, seed), + skins: flattenSkinGroups(skinGroups), + skin_groups: skinGroups, + online_time: { + start: onlineStart, + end: onlineEnd, + }, + ban_record: index % 4 === 0 ? "无封禁记录,近期登录稳定" : "", + screenshots, + deposit_amount: deposit, + price: finalPrice, + settlement_amount: settlementAmount, + rent_days: rentDays, + remark: + index % 3 === 0 + ? "部分仓库材料请勿使用,下单后会在群聊再次提醒。" + : "", + }, + screenshot_urls: Object.values(screenshots), + price_hourly: hourlyPrice, + price_daily: dailyPrice, + price_weekly: Math.round(dailyPrice * 7 * 10) / 10, + deposit_amount: deposit, + min_rent_hours: coinWan >= 10000 ? 24 : 8, + max_rent_hours: coinWan >= 30000 ? 168 : 72, + status: "published", + review_status: "approved", + review_reason: "", + published_at: new Date(Date.now() - seed * 18 * 60 * 1000).toISOString(), + created_at: new Date(Date.now() - seed * 20 * 60 * 1000).toISOString(), + updated_at: new Date(Date.now() - seed * 10 * 60 * 1000).toISOString(), + }; + }); +}