优化首页筛选来源

This commit is contained in:
yml
2026-05-23 00:43:23 +08:00
parent 0394fcb2b8
commit 0b87754ab3
6 changed files with 1270 additions and 261 deletions
+1
View File
@@ -14,6 +14,7 @@
docker compose -f deploy/docker-compose.dev.yml up -d 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/migrations/000001_init.sql
docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < backend/seeds/dev_orders.sql
cd backend cd backend
cp .env.example .env cp .env.example .env
+146
View File
@@ -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);
+2
View File
@@ -25,6 +25,8 @@ declare module 'vue' {
VanPopup: typeof import('vant/es')['Popup'] VanPopup: typeof import('vant/es')['Popup']
VanPullRefresh: typeof import('vant/es')['PullRefresh'] VanPullRefresh: typeof import('vant/es')['PullRefresh']
VanSearch: typeof import('vant/es')['Search'] VanSearch: typeof import('vant/es')['Search']
VanSwipe: typeof import('vant/es')['Swipe']
VanSwipeItem: typeof import('vant/es')['SwipeItem']
VanTag: typeof import('vant/es')['Tag'] VanTag: typeof import('vant/es')['Tag']
} }
} }
@@ -0,0 +1,448 @@
<script setup lang="ts">
import { nextTick, ref } from "vue";
export type FilterSection =
| {
key: string;
title: string;
type: "range";
unit?: string;
minPlaceholder?: string;
maxPlaceholder?: string;
}
| { key: string; title: string; type: "chips"; options: string[] };
type RangeFilters = Record<string, { min: string; max: string }>;
type SelectedFilters = Record<string, string[]>;
type RangePresets = Record<
string,
Array<{ label: string; min: string; max: string }>
>;
const props = defineProps<{
show: boolean;
sections: FilterSection[];
selectedFilters: SelectedFilters;
rangeFilters: RangeFilters;
rangePresets: RangePresets;
}>();
const emit = defineEmits<{
"update:show": [value: boolean];
"update:selectedFilters": [value: SelectedFilters];
"update:rangeFilters": [value: RangeFilters];
reset: [];
}>();
const activeFilterSection = ref("price");
const filterContentRef = ref<HTMLElement | null>(null);
const filterTabsRef = ref<HTMLElement | null>(null);
const filterSectionRefs = new Map<string, HTMLElement>();
const filterTabRefs = new Map<string, HTMLElement>();
function handleShowChange(value: boolean) {
emit("update:show", value);
if (value) {
activeFilterSection.value = props.sections[0]?.key || "price";
nextTick(() => {
filterContentRef.value?.scrollTo({ top: 0 });
});
}
}
function close() {
emit("update:show", false);
}
function confirm() {
close();
}
function reset() {
emit("reset");
}
function toggleChip(sectionKey: string, value: string) {
const selected = props.selectedFilters[sectionKey] || [];
const next = selected.includes(value)
? selected.filter((item) => item !== value)
: [...selected, value];
emit("update:selectedFilters", {
...props.selectedFilters,
[sectionKey]: next,
});
}
function updateRange(sectionKey: string, side: "min" | "max", value: string) {
const current = props.rangeFilters[sectionKey] || { min: "", max: "" };
emit("update:rangeFilters", {
...props.rangeFilters,
[sectionKey]: {
...current,
[side]: value,
},
});
}
function applyRangePreset(sectionKey: string, min: string, max: string) {
emit("update:rangeFilters", {
...props.rangeFilters,
[sectionKey]: { min, max },
});
}
function isRangePresetActive(sectionKey: string, min: string, max: string) {
const range = props.rangeFilters[sectionKey];
return range?.min === min && range?.max === max;
}
function setFilterSectionRef(key: string, el: Element | null) {
if (el instanceof HTMLElement) {
filterSectionRefs.set(key, el);
} else {
filterSectionRefs.delete(key);
}
}
function setFilterTabRef(key: string, el: Element | null) {
if (el instanceof HTMLElement) {
filterTabRefs.set(key, el);
} else {
filterTabRefs.delete(key);
}
}
function scrollToFilterSection(key: string) {
const container = filterContentRef.value;
const target = filterSectionRefs.get(key);
if (!container || !target) return;
activeFilterSection.value = key;
container.scrollTo({
top: Math.max(target.offsetTop - 8, 0),
behavior: "smooth",
});
keepActiveTabVisible(key);
}
function handleFilterScroll() {
const container = filterContentRef.value;
if (!container) return;
const anchorTop = container.scrollTop + 16;
let activeKey = props.sections[0]?.key || "";
for (const section of props.sections) {
const el = filterSectionRefs.get(section.key);
if (el && el.offsetTop <= anchorTop) {
activeKey = section.key;
}
}
if (activeKey && activeKey !== activeFilterSection.value) {
activeFilterSection.value = activeKey;
keepActiveTabVisible(activeKey);
}
}
function keepActiveTabVisible(key: string) {
const container = filterTabsRef.value;
const tab = filterTabRefs.get(key);
if (!container || !tab) return;
const tabTop = tab.offsetTop;
const tabBottom = tabTop + tab.offsetHeight;
const visibleTop = container.scrollTop;
const visibleBottom = visibleTop + container.clientHeight;
if (tabTop < visibleTop) {
container.scrollTo({ top: tabTop, behavior: "smooth" });
} else if (tabBottom > visibleBottom) {
container.scrollTo({
top: tabBottom - container.clientHeight,
behavior: "smooth",
});
}
}
</script>
<template>
<van-popup
:show="show"
position="right"
class="filter-popup"
teleport="body"
@update:show="handleShowChange"
>
<div class="filter-sheet">
<header class="filter-header">
<button type="button" class="filter-close" @click="close">
<van-icon name="cross" :size="18" />
</button>
<h2>筛选</h2>
<span></span>
</header>
<div class="filter-body">
<aside ref="filterTabsRef" class="filter-tabs">
<button
v-for="section in sections"
:key="section.key"
:ref="(el) => setFilterTabRef(section.key, el as Element | null)"
type="button"
:class="{ active: activeFilterSection === section.key }"
@click="scrollToFilterSection(section.key)"
>
{{ section.title }}
</button>
</aside>
<section
ref="filterContentRef"
class="filter-content"
@scroll.passive="handleFilterScroll"
>
<div
v-for="section in sections"
:key="section.key"
:ref="(el) => setFilterSectionRef(section.key, el as Element | null)"
class="filter-block"
>
<h3>{{ section.title }}</h3>
<p v-if="section.type === 'range' && section.unit">
单位{{ section.unit }}
</p>
<template v-if="section.type === 'range'">
<div class="range-editor">
<input
:value="rangeFilters[section.key]?.min || ''"
type="number"
inputmode="decimal"
:placeholder="section.minPlaceholder || '最低'"
@input="updateRange(section.key, 'min', ($event.target as HTMLInputElement).value)"
/>
<span></span>
<input
:value="rangeFilters[section.key]?.max || ''"
type="number"
inputmode="decimal"
:placeholder="section.maxPlaceholder || '最高'"
@input="updateRange(section.key, 'max', ($event.target as HTMLInputElement).value)"
/>
</div>
<div
v-if="rangePresets[section.key]?.length"
class="sheet-chip-grid range-preset-grid"
>
<button
v-for="preset in rangePresets[section.key]"
:key="`${section.key}-${preset.label}`"
type="button"
:class="{
active: isRangePresetActive(section.key, preset.min, preset.max),
}"
@click="applyRangePreset(section.key, preset.min, preset.max)"
>
{{ preset.label }}
</button>
</div>
</template>
<div v-else class="sheet-chip-grid">
<button
v-for="option in section.options"
:key="`${section.key}-${option}`"
type="button"
:class="{
active: selectedFilters[section.key]?.includes(option),
}"
@click="toggleChip(section.key, option)"
>
{{ option }}
</button>
</div>
</div>
</section>
</div>
<footer class="filter-actions">
<button type="button" class="reset-btn" @click="reset">重置</button>
<button type="button" class="confirm-btn" @click="confirm">确定</button>
</footer>
</div>
</van-popup>
</template>
<style scoped>
.filter-popup {
width: min(86vw, 390px);
height: 100dvh;
background: #fff;
}
.filter-sheet {
display: grid;
grid-template-rows: 48px minmax(0, 1fr) 64px;
height: 100%;
background: #fff;
}
.filter-header {
position: relative;
display: grid;
grid-template-columns: 40px 1fr 40px;
align-items: center;
border-bottom: 1px solid #eef1f5;
}
.filter-header h2 {
margin: 0;
text-align: center;
font-size: 15px;
font-weight: 800;
}
.filter-close {
display: grid;
width: 40px;
height: 40px;
place-items: center;
border: 0;
background: transparent;
color: #17233d;
}
.filter-body {
display: grid;
grid-template-columns: 88px minmax(0, 1fr);
min-height: 0;
}
.filter-tabs {
overflow-y: auto;
background: #f6f7f9;
-webkit-overflow-scrolling: touch;
}
.filter-tabs button {
display: block;
width: 100%;
min-height: 42px;
border: 0;
background: transparent;
color: #2d3748;
padding: 0 8px;
text-align: center;
font-size: 12px;
}
.filter-tabs button.active {
background: #fff;
color: #ff7a00;
font-weight: 800;
}
.filter-content {
overflow-y: auto;
min-width: 0;
padding: 18px 14px 28px;
-webkit-overflow-scrolling: touch;
}
.filter-block {
scroll-margin-top: 8px;
}
.filter-block + .filter-block {
margin-top: 26px;
}
.filter-block h3 {
margin: 0 0 12px;
font-size: 17px;
font-weight: 800;
color: #17233d;
}
.filter-block p {
margin: -6px 0 12px;
color: #8a96a8;
font-size: 11px;
}
.range-editor {
display: grid;
grid-template-columns: minmax(0, 1fr) 18px minmax(0, 1fr);
align-items: center;
gap: 8px;
}
.range-editor span {
height: 1px;
background: #cfd6df;
}
.range-editor input {
min-width: 0;
height: 42px;
border: 1px solid #edf0f4;
border-radius: 8px;
background: #fff;
color: #17233d;
padding: 0 10px;
text-align: center;
font-size: 13px;
}
.sheet-chip-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 9px;
}
.range-preset-grid {
margin-top: 10px;
}
.sheet-chip-grid button {
min-width: 0;
min-height: 38px;
border: 0;
border-radius: 7px;
background: #f4f5f7;
color: #2d3748;
padding: 6px 8px;
font-size: 12px;
line-height: 1.25;
}
.sheet-chip-grid button.active {
background: #fff3d1;
color: #cc7a00;
font-weight: 800;
box-shadow: inset 0 0 0 1px #ffd15c;
}
.filter-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
align-items: center;
border-top: 1px solid #eef1f5;
padding: 10px 12px;
}
.filter-actions button {
height: 42px;
border-radius: 8px;
font-size: 14px;
font-weight: 800;
}
.reset-btn {
border: 1px solid #e6eaf0;
background: #fff;
color: #2d3748;
}
.confirm-btn {
border: 0;
background: #ffd21e;
color: #17233d;
}
</style>
File diff suppressed because it is too large Load Diff
+14 -8
View File
@@ -33,9 +33,9 @@ async function loadOrders() {
/* 状态筛选 */ /* 状态筛选 */
const statusTabs = [ const statusTabs = [
{ key: "all", label: "全部" }, { key: "all", label: "全部" },
{ key: "pending", label: "待交接" }, { key: "pending_handoff", label: "待交接" },
{ key: "renting", label: "租赁中" }, { key: "renting", label: "租赁中" },
{ key: "returning", label: "待归还" }, { key: "pending_return_confirm", label: "待归还" },
{ key: "completed", label: "已完成" }, { key: "completed", label: "已完成" },
]; ];
@@ -47,24 +47,30 @@ const displayOrders = computed(() => {
/* 状态颜色映射 */ /* 状态颜色映射 */
function statusColor(status: string) { function statusColor(status: string) {
const map: Record<string, string> = { const map: Record<string, string> = {
pending: "#ff9800", pending_handoff: "#ff9800",
renting: "#1477ff", renting: "#1477ff",
returning: "#e91e63", overdue: "#e91e63",
pending_return_confirm: "#e91e63",
completed: "#4caf50", completed: "#4caf50",
cancelled: "#999", cancelled: "#999",
dispute: "#f44336", disputing: "#f44336",
abnormal: "#f44336",
closed: "#666",
}; };
return map[status] || "#999"; return map[status] || "#999";
} }
function statusLabel(status: string) { function statusLabel(status: string) {
const map: Record<string, string> = { const map: Record<string, string> = {
pending: "待交接", pending_handoff: "待交接",
renting: "租赁中", renting: "租赁中",
returning: "待归还", overdue: "已逾期",
pending_return_confirm: "待归还",
completed: "已完成", completed: "已完成",
cancelled: "已取消", cancelled: "已取消",
dispute: "申诉中", disputing: "申诉中",
abnormal: "异常",
closed: "已关闭",
}; };
return map[status] || status; return map[status] || status;
} }