diff --git a/README.md b/README.md
index b9a3595..27d01f3 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@
docker compose -f deploy/docker-compose.dev.yml up -d
docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < backend/migrations/000001_init.sql
+docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < backend/seeds/dev_orders.sql
cd backend
cp .env.example .env
diff --git a/backend/seeds/dev_orders.sql b/backend/seeds/dev_orders.sql
new file mode 100644
index 0000000..d582f89
--- /dev/null
+++ b/backend/seeds/dev_orders.sql
@@ -0,0 +1,146 @@
+-- Development seed: refresh demo listings and rental orders for the mobile app.
+-- Usage:
+-- docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < backend/seeds/dev_orders.sql
+
+SET NAMES utf8mb4;
+
+CREATE TEMPORARY TABLE IF NOT EXISTS tmp_seed_orders AS
+SELECT id FROM rental_orders;
+
+DELETE FROM disputes WHERE order_id IN (SELECT id FROM tmp_seed_orders);
+DELETE FROM handoff_records WHERE order_id IN (SELECT id FROM tmp_seed_orders);
+DELETE FROM wallet_ledger WHERE order_id IN (SELECT id FROM tmp_seed_orders);
+DELETE FROM notifications
+WHERE biz_type = 'order' OR biz_id IN (SELECT id FROM tmp_seed_orders);
+DELETE FROM rental_orders;
+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 '测试订单-%';
+
+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);
+DROP TEMPORARY TABLE IF EXISTS tmp_seed_accounts;
+
+INSERT INTO users (phone, nickname, realname_status, risk_status, credit_score, status)
+VALUES
+ ('13800000001', '测试号主A', 'verified', 'normal', 100, 'active'),
+ ('13800000002', '测试号主B', 'verified', 'normal', 100, 'active'),
+ ('13900000001', '测试租客A', 'verified', 'normal', 100, 'active')
+ON DUPLICATE KEY UPDATE
+ nickname = VALUES(nickname),
+ realname_status = VALUES(realname_status),
+ risk_status = VALUES(risk_status),
+ credit_score = VALUES(credit_score),
+ status = VALUES(status),
+ updated_at = CURRENT_TIMESTAMP;
+
+SELECT id INTO @owner_a FROM users WHERE phone = '13800000001';
+SELECT id INTO @owner_b FROM users WHERE phone = '13800000002';
+SELECT id INTO @renter_a FROM users WHERE phone = '13900000001';
+
+INSERT INTO wallet_accounts (user_id, available_balance, frozen_balance, status)
+VALUES
+ (@owner_a, 0, 0, 'active'),
+ (@owner_b, 0, 0, 'active'),
+ (@renter_a, 2000, 0, 'active')
+ON DUPLICATE KEY UPDATE
+ available_balance = VALUES(available_balance),
+ frozen_balance = VALUES(frozen_balance),
+ status = VALUES(status),
+ updated_at = CURRENT_TIMESTAMP;
+
+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万哈夫币',
+ '扫码交接,保险 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万低押号',
+ '账密登录,低押金,保险 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万包夜号',
+ '可包夜,保险 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万成品号',
+ '账密交接,高段位,保险 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'),
+ (@owner_a, '三角洲行动', '微信', '扫码', '测试订单-待交接 微信 黑鹰 20000万',
+ '测试订单关联账号:待交接。',
+ '黑鹰', 200000000,
+ JSON_OBJECT('haf_coin_wan', 20000, 'season_insurance', '2*3', 'stamina_level', '6级', 'load_level', '5级', 'daily_loss', '20M', 'ratio', 6, 'login_method', '扫码', 'common_regions', JSON_ARRAY('广东'), 'resources', JSON_ARRAY(JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 50, 'mode', '收费')), 'skin_groups', JSON_OBJECT('melee', JSON_ARRAY('影锋'), 'operator', JSON_ARRAY('威龙-吴彦祖'), 'weapon', JSON_ARRAY('QBZ95王牌之剑'))),
+ JSON_ARRAY(), 'rented'),
+ (@owner_b, '三角洲行动', 'QQ', '账号密码', '测试订单-租赁中 QQ 钻石 10000万',
+ '测试订单关联账号:租赁中。',
+ '钻石', 100000000,
+ JSON_OBJECT('haf_coin_wan', 10000, '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', 20, 'mode', '收费')), 'skin_groups', JSON_OBJECT('melee', JSON_ARRAY('黑海'), 'operator', JSON_ARRAY('骇爪-水墨云图'), 'weapon', JSON_ARRAY('AUG气象感应'))),
+ JSON_ARRAY(), 'rented'),
+ (@owner_a, '三角洲行动', 'Steam', '扫码', '测试订单-待归还 Steam 黄金 5000万',
+ '测试订单关联账号:待归还确认。',
+ '黄金', 50000000,
+ 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_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;
+
+INSERT INTO rental_listings
+ (account_id, owner_id, price_hourly, price_daily, price_weekly, deposit_amount, min_rent_hours, max_rent_hours, status, review_status, published_at)
+VALUES
+ (@account_pub_1, @owner_a, 8.80, 88.00, 520.00, 120.00, 24, 168, 'published', 'approved', NOW() - INTERVAL 10 MINUTE),
+ (@account_pub_2, @owner_a, 5.50, 55.00, 330.00, 60.00, 24, 72, 'published', 'approved', NOW() - INTERVAL 20 MINUTE),
+ (@account_pub_3, @owner_b, 3.90, 39.00, 220.00, 80.00, 8, 24, 'published', 'approved', NOW() - INTERVAL 30 MINUTE),
+ (@account_pub_4, @owner_b, 12.80, 128.00, 760.00, 180.00, 24, 168, 'published', 'approved', NOW() - INTERVAL 40 MINUTE),
+ (@account_order_1, @owner_a, 7.00, 70.00, 420.00, 100.00, 24, 72, 'rented', 'approved', NOW() - INTERVAL 50 MINUTE),
+ (@account_order_2, @owner_b, 6.00, 60.00, 360.00, 80.00, 24, 168, 'rented', 'approved', NOW() - INTERVAL 60 MINUTE),
+ (@account_order_3, @owner_a, 3.00, 30.00, 180.00, 50.00, 8, 24, 'rented', 'approved', NOW() - INTERVAL 70 MINUTE);
+
+SELECT id INTO @listing_order_1 FROM rental_listings WHERE account_id = @account_order_1 ORDER BY id DESC LIMIT 1;
+SELECT id INTO @listing_order_2 FROM rental_listings WHERE account_id = @account_order_2 ORDER BY id DESC LIMIT 1;
+SELECT id INTO @listing_order_3 FROM rental_listings WHERE account_id = @account_order_3 ORDER BY id DESC LIMIT 1;
+
+INSERT INTO rental_orders
+ (order_no, listing_id, account_id, owner_id, renter_id, rent_start_at, rent_end_at, rent_hours, rent_amount, deposit_amount, platform_fee, account_snapshot, status, handoff_status, settlement_status, created_at, updated_at)
+VALUES
+ ('TEST202605230001', @listing_order_1, @account_order_1, @owner_a, @renter_a, NULL, NULL, 24, 70.00, 100.00, 7.00,
+ JSON_OBJECT('title', '测试订单-待交接 微信 黑鹰 20000万', 'server_region', '微信', 'login_platform', '扫码', 'haf_coin_amount', 200000000),
+ 'pending_handoff', 'pending_owner', 'unsettled', NOW(), NOW()),
+ ('TEST202605230002', @listing_order_2, @account_order_2, @owner_b, @renter_a, NOW() - INTERVAL 2 HOUR, NOW() + INTERVAL 22 HOUR, 24, 60.00, 80.00, 6.00,
+ JSON_OBJECT('title', '测试订单-租赁中 QQ 钻石 10000万', 'server_region', 'QQ', 'login_platform', '账号密码', 'haf_coin_amount', 100000000),
+ 'renting', 'received', 'unsettled', NOW() - INTERVAL 2 HOUR, NOW() - INTERVAL 90 MINUTE),
+ ('TEST202605230003', @listing_order_3, @account_order_3, @owner_a, @renter_a, NOW() - INTERVAL 10 HOUR, NOW() - INTERVAL 2 HOUR, 8, 30.00, 50.00, 3.00,
+ JSON_OBJECT('title', '测试订单-待归还 Steam 黄金 5000万', 'server_region', 'Steam', 'login_platform', '扫码', 'haf_coin_amount', 50000000),
+ 'pending_return_confirm', 'pending_owner_return_confirm', 'unsettled', NOW() - INTERVAL 10 HOUR, NOW() - INTERVAL 15 MINUTE);
+
+SELECT id INTO @order_1 FROM rental_orders WHERE order_no = 'TEST202605230001';
+SELECT id INTO @order_2 FROM rental_orders WHERE order_no = 'TEST202605230002';
+SELECT id INTO @order_3 FROM rental_orders WHERE order_no = 'TEST202605230003';
+
+INSERT INTO handoff_records
+ (order_id, from_user_id, to_user_id, type, content, attachment_urls, confirmed_by_renter_at, confirmed_by_owner_at)
+VALUES
+ (@order_2, @owner_b, @renter_a, 'owner_submit', '账密已通过群聊交接,请按约定时间使用。', JSON_ARRAY(), NOW() - INTERVAL 90 MINUTE, NULL),
+ (@order_3, @owner_a, @renter_a, 'owner_submit', '扫码已完成,账号可正常进入游戏。', JSON_ARRAY(), NOW() - INTERVAL 9 HOUR, NULL),
+ (@order_3, @renter_a, @owner_a, 'renter_return', '已退出账号,未消耗保留物资。', JSON_ARRAY(), NULL, NULL);
+
+INSERT INTO notifications (user_id, type, title, content, biz_type, biz_id, created_at)
+VALUES
+ (@owner_a, 'order', '收到新的租号订单', '测试订单 TEST202605230001 等待你提交交接说明。', 'order', @order_1, NOW()),
+ (@renter_a, 'order', '订单已创建', '测试订单 TEST202605230001 已创建,请等待号主交接。', 'order', @order_1, NOW()),
+ (@renter_a, 'order', '订单租赁中', '测试订单 TEST202605230002 已进入租赁中。', 'order', @order_2, NOW() - INTERVAL 90 MINUTE),
+ (@owner_a, 'order', '买家已提交归还', '测试订单 TEST202605230003 等待你确认归还。', 'order', @order_3, NOW() - INTERVAL 15 MINUTE);
diff --git a/frontend/components.d.ts b/frontend/components.d.ts
index 3785345..e2383c8 100644
--- a/frontend/components.d.ts
+++ b/frontend/components.d.ts
@@ -25,6 +25,8 @@ declare module 'vue' {
VanPopup: typeof import('vant/es')['Popup']
VanPullRefresh: typeof import('vant/es')['PullRefresh']
VanSearch: typeof import('vant/es')['Search']
+ VanSwipe: typeof import('vant/es')['Swipe']
+ VanSwipeItem: typeof import('vant/es')['SwipeItem']
VanTag: typeof import('vant/es')['Tag']
}
}
diff --git a/frontend/src/views/mobile/MobileHomeFilterSheet.vue b/frontend/src/views/mobile/MobileHomeFilterSheet.vue
new file mode 100644
index 0000000..2078433
--- /dev/null
+++ b/frontend/src/views/mobile/MobileHomeFilterSheet.vue
@@ -0,0 +1,448 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/views/mobile/MobileHomeView.vue b/frontend/src/views/mobile/MobileHomeView.vue
index 07c9cda..0228822 100644
--- a/frontend/src/views/mobile/MobileHomeView.vue
+++ b/frontend/src/views/mobile/MobileHomeView.vue
@@ -3,36 +3,76 @@ import { computed, onMounted, ref } from "vue";
import { RouterLink, useRoute } from "vue-router";
import { showToast } from "vant";
+import {
+ defaultListingPublishOptions,
+ fetchListingPublishOptions,
+ type ListingPublishOptions,
+} from "@/api/listingOptions";
import { fetchListings, type Listing } from "@/api/listings";
+import MobileHomeFilterSheet, {
+ type FilterSection,
+} from "./MobileHomeFilterSheet.vue";
const route = useRoute();
const loading = ref(false);
const loadFailed = ref(false);
const listings = ref
([]);
-const activeSort = ref(0);
-const activeFilter = ref([]);
-const activeChannel = ref(0);
+const publishOptions = ref(defaultListingPublishOptions);
+const sortOpen = ref(false);
+const activeSort = ref("comprehensive");
+const filterOpen = ref(false);
+const selectedFilters = ref>({});
+const rangeFilters = ref>({});
const refreshing = ref(false);
const searchValue = ref("");
-const channels = [
- { label: "全部", icon: "🔥" },
- { label: "高哈夫币", icon: "💰" },
- { label: "低押金", icon: "🛡️" },
- { label: "扫码号", icon: "📱" },
- { label: "包夜", icon: "🌙" },
-];
const sortOptions = [
- { text: "默认", value: "default" },
- { text: "价格", value: "price" },
- { text: "比例", value: "ratio" },
- { text: "哈夫币", value: "coin" },
+ { key: "comprehensive", label: "综合排序" },
+ { key: "published", label: "发布时间" },
+ { key: "priceAsc", label: "价格最低" },
+ { key: "priceDesc", label: "价格最高" },
];
-const quickFilters = [
- { text: "三角洲行动", value: "a" },
- { text: "安卓/扫码", value: "b" },
- { text: "高段位", value: "c" },
- { text: "可包夜", value: "d" },
+
+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: "200-300", min: "200", max: "300" },
+ { label: "300以上", min: "300", max: "" },
+ ],
+ resource_awmAmmo: [
+ { label: "0-20", min: "0", max: "20" },
+ { label: "20-50", min: "20", max: "50" },
+ { label: "50-100", min: "50", max: "100" },
+ { label: "100-200", min: "100", max: "200" },
+ { label: "200以上", min: "200", max: "" },
+ ],
+};
+
+const bannerSlides = [
+ {
+ eyebrow: "三角洲行动账号专区",
+ title: "高哈夫币 · 安全交接 · 随租随玩",
+ badge: "HOT",
+ pill: "新人领券最高减 20 元",
+ tone: "blue",
+ },
+ {
+ eyebrow: "平台担保交易",
+ title: "交接全程留痕,拒绝私下转账",
+ badge: "SAFE",
+ pill: "租号前先验实名与资料",
+ tone: "green",
+ },
+ {
+ eyebrow: "高效筛选",
+ title: "按区服、段位、哈夫币快速找号",
+ badge: "FAST",
+ pill: "支持扫码号与账密号",
+ tone: "orange",
+ },
];
const fallbackListings: Listing[] = [
@@ -40,19 +80,39 @@ const fallbackListings: Listing[] = [
id: 9001,
account_id: 0,
owner_id: 0,
- title: "烽火地带 · 80W 哈夫币安全号",
- description: "支持扫码上号,号主在线交接,适合快速体验高资产账号。",
+ title: "QQ区 黑鹰 30000万哈夫币账号",
+ description: "支持扫码上号,号主在线交接,保险 2*3,适合高强度体验。",
game_name: "三角洲行动",
- server_region: "微信区",
- login_platform: "安卓扫码",
- rank_level: "机密行动 王牌",
- haf_coin_amount: 800000,
+ 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: 6.8,
- price_daily: 58,
- price_weekly: 288,
- deposit_amount: 200,
- min_rent_hours: 2,
+ 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",
@@ -64,19 +124,38 @@ const fallbackListings: Listing[] = [
id: 9002,
account_id: 0,
owner_id: 0,
- title: "航天基地 · 高配皮肤成品号",
- description: "热门枪皮齐全,低押金,新手友好。",
+ title: "微信 钻石 12000万哈夫币低押号",
+ description: "账密登录,低押金,皮肤截图齐全,常用登录地江苏。",
game_name: "三角洲行动",
- server_region: "QQ区",
- login_platform: "iOS扫码",
+ server_region: "微信",
+ login_platform: "账号密码",
rank_level: "钻石",
- haf_coin_amount: 520000,
+ 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: 4.5,
- price_daily: 39,
- price_weekly: 198,
- deposit_amount: 120,
- min_rent_hours: 1,
+ 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",
@@ -88,20 +167,39 @@ const fallbackListings: Listing[] = [
id: 9003,
account_id: 0,
owner_id: 0,
- title: "零号大坝 · 搬砖专用稳定号",
- description: "可长租,支持包周,交接说明完整。",
+ title: "Steam 铂金 6000万哈夫币包夜号",
+ description: "可包夜,保险 2*1,适合短租搬砖体验。",
game_name: "三角洲行动",
- server_region: "微信区",
- login_platform: "安卓/模拟器",
+ server_region: "Steam",
+ login_platform: "扫码",
rank_level: "铂金",
- haf_coin_amount: 360000,
+ 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: 32,
- price_weekly: 168,
+ price_daily: 39,
+ price_weekly: 220,
deposit_amount: 80,
- min_rent_hours: 3,
- max_rent_hours: 168,
+ min_rent_hours: 8,
+ max_rent_hours: 24,
status: "published",
review_status: "approved",
review_reason: "",
@@ -110,25 +208,78 @@ const fallbackListings: Listing[] = [
},
];
-const displayListings = computed(() =>
- listings.value.length > 0 ? listings.value : fallbackListings
+const sourceListings = computed(() =>
+ loadFailed.value ? fallbackListings : listings.value
);
-const platformStats = computed(() => {
- const items = displayListings.value;
- const totalCoin = items.reduce(
- (sum, item) => sum + Number(item.haf_coin_amount || 0),
+const activeSortLabel = computed(
+ () =>
+ sortOptions.find((option) => option.key === activeSort.value)?.label ||
+ "综合排序"
+);
+
+const serverFilterOptions = computed(() =>
+ uniqueOptions(
+ publishOptions.value.server_options
+ .map((item) => normalizeServerRegion(item))
+ .filter(Boolean)
+ )
+);
+
+const loginMethodFilterOptions = computed(() =>
+ uniqueOptions(
+ publishOptions.value.login_method_options
+ .map((item) => normalizeLoginMethod(item))
+ .filter(Boolean)
+ )
+);
+
+const filterSections = computed(() => [
+ { key: "price", title: "价格区间", type: "range", unit: "元/小时", minPlaceholder: "最低价", maxPlaceholder: "最高价" },
+ { key: "coin", title: "哈夫币数量", type: "range", unit: "万", 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 },
+ { key: "stamina", title: "体力", type: "chips", options: publishOptions.value.level_options },
+ { key: "load", title: "负重", type: "chips", options: publishOptions.value.level_options },
+ ...publishOptions.value.quantity_items.map((item) => ({
+ key: `resource_${item.key}`,
+ title: item.label,
+ type: "range" as const,
+ unit: parseQuantityUnit(item.price),
+ minPlaceholder: "最低",
+ maxPlaceholder: "最高",
+ })),
+ ...publishOptions.value.skin_groups.map((group) => ({
+ key: group.key,
+ title: group.title,
+ type: "chips" as const,
+ options: group.options,
+ })),
+ { key: "secretKd", title: "绝密KD", type: "range", minPlaceholder: "最低", maxPlaceholder: "最高" },
+ { key: "rank", title: "段位", type: "chips", options: publishOptions.value.rank_options },
+ { key: "deposit", title: "押金", type: "range", unit: "元", minPlaceholder: "最低", maxPlaceholder: "最高" },
+]);
+
+const activeFilterCount = computed(() => {
+ const chipCount = Object.values(selectedFilters.value).reduce(
+ (sum, values) => sum + values.length,
0
);
- const lowDeposit = items.filter(
- (item) => Number(item.deposit_amount || 0) <= 120
+ const rangeCount = Object.values(rangeFilters.value).filter(
+ (range) => range.min || range.max
).length;
+ return chipCount + rangeCount;
+});
- return [
- { value: `${items.length}+`, label: "在线租号", icon: "👥" },
- { value: formatCoin(totalCoin), label: "哈夫币总量", icon: "💰" },
- { value: `${lowDeposit}个`, label: "低押账号", icon: "🛡️" },
- ];
+const displayListings = computed(() => {
+ const keyword = searchValue.value.trim().toLowerCase();
+ const filtered = sourceListings.value.filter((item) => {
+ if (!matchesFilters(item)) return false;
+ if (!keyword) return true;
+ return searchText(item).includes(keyword);
+ });
+ return sortListings(filtered);
});
/** 判断当前底部导航是否激活 */
@@ -137,7 +288,10 @@ function isNavActive(path: string) {
return route.path.startsWith(path);
}
-onMounted(loadListings);
+onMounted(() => {
+ loadListings();
+ loadPublishOptions();
+});
async function loadListings() {
loading.value = true;
@@ -151,10 +305,23 @@ async function loadListings() {
}
}
+async function loadPublishOptions() {
+ try {
+ publishOptions.value = await fetchListingPublishOptions();
+ } catch {
+ publishOptions.value = defaultListingPublishOptions;
+ }
+}
+
async function onRefresh() {
refreshing.value = true;
try {
- listings.value = await fetchListings();
+ const [nextListings, nextOptions] = await Promise.all([
+ fetchListings(),
+ fetchListingPublishOptions(),
+ ]);
+ listings.value = nextListings;
+ publishOptions.value = nextOptions;
showToast({ message: "刷新成功", icon: "passed" });
} catch {
// 静默处理
@@ -163,28 +330,226 @@ async function onRefresh() {
}
}
-function toggleFilter(value: string) {
- const idx = activeFilter.value.indexOf(value);
- if (idx >= 0) {
- activeFilter.value.splice(idx, 1);
- } else {
- activeFilter.value.push(value);
- }
+function openFilters() {
+ sortOpen.value = false;
+ filterOpen.value = true;
}
-function formatCoin(amount: number) {
- if (amount >= 10000) {
- return `${Math.round(amount / 10000)}W`;
+function toggleSortPanel() {
+ sortOpen.value = !sortOpen.value;
+}
+
+function selectSort(sortKey: string) {
+ activeSort.value = sortKey;
+ sortOpen.value = false;
+}
+
+function clearFilters() {
+ selectedFilters.value = {};
+ rangeFilters.value = {};
+ searchValue.value = "";
+}
+
+function sortListings(items: Listing[]) {
+ const sorted = [...items];
+ if (activeSort.value === "comprehensive") {
+ return sorted;
}
- return `${amount}`;
+ if (activeSort.value === "priceAsc") {
+ return sorted.sort(
+ (a, b) => Number(a.price_hourly || 0) - Number(b.price_hourly || 0)
+ );
+ }
+ if (activeSort.value === "priceDesc") {
+ return sorted.sort(
+ (a, b) => Number(b.price_hourly || 0) - Number(a.price_hourly || 0)
+ );
+ }
+ return sorted.sort((a, b) => {
+ const bTime = Date.parse(b.published_at || b.created_at || "") || 0;
+ const aTime = Date.parse(a.published_at || a.created_at || "") || 0;
+ return bTime - aTime || b.id - a.id;
+ });
+}
+
+function matchesFilters(item: Listing) {
+ const chipOk = Object.entries(selectedFilters.value).every(([key, values]) => {
+ if (!values.length) return true;
+ if (key === "server") return values.includes(getServerRegion(item));
+ if (key === "login") return values.includes(getLoginMethod(item));
+ if (key === "insurance") return values.includes(readAssetString(item, "season_insurance"));
+ if (key === "stamina") return values.includes(readAssetString(item, "stamina_level"));
+ if (key === "load") return values.includes(readAssetString(item, "load_level"));
+ if (key === "rank") return values.includes(item.rank_level);
+ if (isSkinGroupKey(key)) {
+ return getSkinGroup(item, key).some((skin) => values.includes(skin));
+ }
+ return true;
+ });
+ if (!chipOk) return false;
+
+ 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 === "secretKd") value = readAssetNumber(item, "secret_kd");
+ if (key === "deposit") value = Number(item.deposit_amount || 0);
+ if (key.startsWith("resource_")) {
+ value = getResourceQuantity(item, key.replace("resource_", ""));
+ }
+ return inRange(value, range);
+ });
+}
+
+function inRange(value: number, range: { min: string; max: string }) {
+ const min = range.min === "" ? undefined : Number(range.min);
+ const max = range.max === "" ? undefined : Number(range.max);
+ if (min !== undefined && Number.isFinite(min) && value < min) return false;
+ if (max !== undefined && Number.isFinite(max) && value > max) return false;
+ return true;
+}
+
+function searchText(item: Listing) {
+ return [
+ item.title,
+ item.description,
+ getServerRegion(item),
+ getLoginMethod(item),
+ item.rank_level,
+ readAssetString(item, "season_insurance"),
+ readAssetString(item, "stamina_level"),
+ readAssetString(item, "load_level"),
+ formatCoinWan(getCoinWan(item)),
+ assetRegions(item).join(" "),
+ ...publishOptions.value.skin_groups.map((group) =>
+ getSkinGroup(item, group.key).join(" ")
+ ),
+ ]
+ .join(" ")
+ .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);
+}
+
+function parseQuantityUnit(price: string) {
+ const unit = price.split("/")[1]?.trim();
+ return unit || undefined;
+}
+
+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) {
- if (!item.price_hourly) {
- return "--";
- }
- return `${Math.round(item.haf_coin_amount / item.price_hourly / 10000)}W/元`;
+ const ratio = getRatio(item);
+ return ratio > 0 ? `1:${Math.round(ratio)}` : "--";
}
+
@@ -220,68 +585,53 @@ function formatRatio(item: Listing) {
-
-
-
-
三角洲行动账号专区
-
高哈夫币 · 安全交接 · 随租随玩
-
新人领券最高减 20 元
-
-
HOT
-
-
-
-
-
+
+
- {{ item.icon }}
- {{ item.label }}
-
-
+
+
+
{{ slide.eyebrow }}
+
{{ slide.title }}
+
{{ slide.pill }}
+
+
{{ slide.badge }}
+
+
+
-
-
-
- {{ item.icon }}
- {{ item.value }}
- {{ item.label }}
-
+
+
+
-
-
-
-
+
+ {{ option.label }}
+
+
-
-
-
-
- {{ f.text }}
-
+
+ {{ displayListings.length }}
+ 个可租账号
@@ -295,6 +645,15 @@ function formatRatio(item: Listing) {
background="transparent"
text="接口暂不可用,当前展示移动端示例数据。"
/>
+
+
+ 重置条件
+
+
@@ -310,11 +669,15 @@ function formatRatio(item: Listing) {
{{
- item.server_region
- }}
- {{
- item.login_platform
+ getServerRegion(item)
}}
+ {{ getLoginMethod(item) }}
{{ item.rank_level }}
+
+ 保险 {{ readAssetString(item, "season_insurance") }}
+
+
+ 损耗 {{ readAssetString(item, "daily_loss") }}
+