优化筛选
This commit is contained in:
+275
-70
@@ -6,12 +6,18 @@ import {
|
|||||||
Copy,
|
Copy,
|
||||||
Filter,
|
Filter,
|
||||||
LoaderCircle,
|
LoaderCircle,
|
||||||
RefreshCw,
|
|
||||||
Search,
|
Search,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
} from "@lucide/vue";
|
} from "@lucide/vue";
|
||||||
import { fetchListingsPage, fetchMobileHomeConfig, type HomeBannerSlide, type Listing, type PublicListingQuery } from "./api";
|
import {
|
||||||
|
fetchListingsPage,
|
||||||
|
fetchMobileHomeConfig,
|
||||||
|
type HomeBannerSlide,
|
||||||
|
type Listing,
|
||||||
|
type ListingPublishOptions,
|
||||||
|
type PublicListingQuery,
|
||||||
|
} from "./api";
|
||||||
import {
|
import {
|
||||||
formatCoin,
|
formatCoin,
|
||||||
formatListingCode,
|
formatListingCode,
|
||||||
@@ -28,34 +34,61 @@ const loadError = ref("");
|
|||||||
const listings = ref<Listing[]>([]);
|
const listings = ref<Listing[]>([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const zoneCounts = ref<Record<string, number>>({});
|
|
||||||
const announcements = ref<string[]>([]);
|
const announcements = ref<string[]>([]);
|
||||||
const banners = ref<HomeBannerSlide[]>([]);
|
const banners = ref<HomeBannerSlide[]>([]);
|
||||||
|
const publishOptions = ref<ListingPublishOptions>({
|
||||||
|
server_options: [],
|
||||||
|
login_method_options: [],
|
||||||
|
rank_options: [],
|
||||||
|
insurance_options: [],
|
||||||
|
level_options: [],
|
||||||
|
skin_groups: [],
|
||||||
|
});
|
||||||
const copiedCode = ref("");
|
const copiedCode = ref("");
|
||||||
const activeSort = ref("recommended");
|
const activeSort = ref("recommended");
|
||||||
const activeZone = ref("all");
|
const filterOpen = ref(false);
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
keyword: "",
|
keyword: "",
|
||||||
minCoin: "",
|
minCoin: "",
|
||||||
maxCoin: "",
|
maxCoin: "",
|
||||||
minPrice: "",
|
minPrice: "",
|
||||||
maxPrice: "",
|
maxPrice: "",
|
||||||
|
minKd: "",
|
||||||
|
maxKd: "",
|
||||||
|
stamina: [] as string[],
|
||||||
|
load: [] as string[],
|
||||||
|
insurance: [] as string[],
|
||||||
|
skinNames: [] as string[],
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortOptions = [
|
const sortOptions = [
|
||||||
{ key: "recommended", label: "默认" },
|
{ key: "recommended", label: "默认", sortable: false },
|
||||||
{ key: "priceAsc", label: "价格" },
|
{ key: "price", label: "价格", sortable: true },
|
||||||
{ key: "coinDesc", label: "哈夫币" },
|
{ key: "ratio", label: "比例", sortable: true },
|
||||||
{ key: "awmDesc", label: "AWM" },
|
{ key: "coin", label: "哈夫币", sortable: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
const zoneOptions = computed(() => [
|
const fallbackLevels = ["3级", "4级", "5级", "6级", "7级"];
|
||||||
{ key: "all", label: "全部", count: zoneCount("all") },
|
const fallbackInsurance = ["2*1", "2*2", "2*3", "3*3"];
|
||||||
{ key: "sale", label: "特惠", count: zoneCount("sale") },
|
const fallbackSkins = ["信条", "坠星者", "处刑者", "影锋", "暗星", "电锯", "蚀金玫瑰", "维什戴尔", "水墨云图"];
|
||||||
{ key: "highCoin", label: "高币", count: zoneCount("highCoin") },
|
|
||||||
{ key: "password", label: "账密", count: zoneCount("password") },
|
const levelOptions = computed(() => publishOptions.value.level_options.length ? publishOptions.value.level_options : fallbackLevels);
|
||||||
{ key: "night", label: "夜间", count: zoneCount("night") },
|
const insuranceOptions = computed(() =>
|
||||||
]);
|
publishOptions.value.insurance_options.length ? publishOptions.value.insurance_options : fallbackInsurance
|
||||||
|
);
|
||||||
|
const skinSections = computed(() => {
|
||||||
|
const groups = publishOptions.value.skin_groups.filter((group) =>
|
||||||
|
["melee", "operatorGold", "operatorRed", "weapon"].includes(group.key)
|
||||||
|
);
|
||||||
|
if (groups.length) {
|
||||||
|
return groups.map((group) => ({
|
||||||
|
key: group.key,
|
||||||
|
title: groupTitle(group.key, group.title),
|
||||||
|
options: group.options,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [{ key: "skin", title: "刀皮", options: fallbackSkins }];
|
||||||
|
});
|
||||||
|
|
||||||
const topBanner = computed(() => banners.value[0] || {
|
const topBanner = computed(() => banners.value[0] || {
|
||||||
eyebrow: "三角洲行动账号专区",
|
eyebrow: "三角洲行动账号专区",
|
||||||
@@ -66,13 +99,40 @@ const topBanner = computed(() => banners.value[0] || {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const hasMore = computed(() => listings.value.length < total.value);
|
const hasMore = computed(() => listings.value.length < total.value);
|
||||||
|
const displayListings = computed(() => {
|
||||||
|
const items = [...listings.value];
|
||||||
|
if (activeSort.value === "ratioAsc") {
|
||||||
|
return items.sort((a, b) => ratioValue(a) - ratioValue(b));
|
||||||
|
}
|
||||||
|
if (activeSort.value === "ratioDesc") {
|
||||||
|
return items.sort((a, b) => ratioValue(b) - ratioValue(a));
|
||||||
|
}
|
||||||
|
if (activeSort.value === "coinAsc") {
|
||||||
|
return items.sort((a, b) => a.haf_coin_amount - b.haf_coin_amount);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
const hasAdvancedFilters = computed(() => {
|
||||||
|
return Boolean(
|
||||||
|
filters.minCoin ||
|
||||||
|
filters.maxCoin ||
|
||||||
|
filters.minPrice ||
|
||||||
|
filters.maxPrice ||
|
||||||
|
filters.minKd ||
|
||||||
|
filters.maxKd ||
|
||||||
|
filters.stamina.length ||
|
||||||
|
filters.load.length ||
|
||||||
|
filters.insurance.length ||
|
||||||
|
filters.skinNames.length
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadHome();
|
loadHome();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [activeSort.value, activeZone.value],
|
() => activeSort.value,
|
||||||
() => loadListings(true)
|
() => loadListings(true)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -82,6 +142,7 @@ async function loadHome() {
|
|||||||
const [home] = await Promise.all([fetchMobileHomeConfig(), loadListings(true)]);
|
const [home] = await Promise.all([fetchMobileHomeConfig(), loadListings(true)]);
|
||||||
announcements.value = home.announcements;
|
announcements.value = home.announcements;
|
||||||
banners.value = home.banners;
|
banners.value = home.banners;
|
||||||
|
publishOptions.value = home.publish_options;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
loadError.value = error instanceof Error ? error.message : "加载失败";
|
loadError.value = error instanceof Error ? error.message : "加载失败";
|
||||||
} finally {
|
} finally {
|
||||||
@@ -99,7 +160,6 @@ async function loadListings(reset = false) {
|
|||||||
listings.value = reset ? result.items : [...listings.value, ...result.items];
|
listings.value = reset ? result.items : [...listings.value, ...result.items];
|
||||||
total.value = result.total;
|
total.value = result.total;
|
||||||
page.value = result.page + 1;
|
page.value = result.page + 1;
|
||||||
zoneCounts.value = result.zone_counts;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
loadError.value = error instanceof Error ? error.message : "加载失败";
|
loadError.value = error instanceof Error ? error.message : "加载失败";
|
||||||
if (reset) listings.value = [];
|
if (reset) listings.value = [];
|
||||||
@@ -113,12 +173,17 @@ function buildQuery(nextPage: number): PublicListingQuery {
|
|||||||
page: nextPage,
|
page: nextPage,
|
||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
keyword: filters.keyword.trim(),
|
keyword: filters.keyword.trim(),
|
||||||
sort: activeSort.value,
|
sort: backendSortKey(),
|
||||||
zone: activeZone.value,
|
|
||||||
min_coin: toNumber(filters.minCoin),
|
min_coin: toNumber(filters.minCoin),
|
||||||
max_coin: toNumber(filters.maxCoin),
|
max_coin: toNumber(filters.maxCoin),
|
||||||
min_price: toNumber(filters.minPrice),
|
min_price: toNumber(filters.minPrice),
|
||||||
max_price: toNumber(filters.maxPrice),
|
max_price: toNumber(filters.maxPrice),
|
||||||
|
min_secret_kd: toNumber(filters.minKd),
|
||||||
|
max_secret_kd: toNumber(filters.maxKd),
|
||||||
|
stamina: filters.stamina.join(","),
|
||||||
|
load: filters.load.join(","),
|
||||||
|
insurance: filters.insurance.join(","),
|
||||||
|
skin_name: filters.skinNames.join(","),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,11 +197,71 @@ function resetFilters() {
|
|||||||
filters.maxCoin = "";
|
filters.maxCoin = "";
|
||||||
filters.minPrice = "";
|
filters.minPrice = "";
|
||||||
filters.maxPrice = "";
|
filters.maxPrice = "";
|
||||||
|
filters.minKd = "";
|
||||||
|
filters.maxKd = "";
|
||||||
|
filters.stamina = [];
|
||||||
|
filters.load = [];
|
||||||
|
filters.insurance = [];
|
||||||
|
filters.skinNames = [];
|
||||||
activeSort.value = "recommended";
|
activeSort.value = "recommended";
|
||||||
activeZone.value = "all";
|
|
||||||
loadListings(true);
|
loadListings(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function confirmFilters() {
|
||||||
|
filterOpen.value = false;
|
||||||
|
loadListings(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setQuickSort(key: string) {
|
||||||
|
if (key === "recommended") {
|
||||||
|
activeSort.value = "recommended";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key === "price") {
|
||||||
|
activeSort.value = activeSort.value === "priceAsc" ? "priceDesc" : "priceAsc";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key === "ratio") {
|
||||||
|
activeSort.value = activeSort.value === "ratioDesc" ? "ratioAsc" : "ratioDesc";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key === "coin") {
|
||||||
|
activeSort.value = activeSort.value === "coinDesc" ? "coinAsc" : "coinDesc";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isQuickSortActive(key: string) {
|
||||||
|
if (key === "recommended") return activeSort.value === "recommended";
|
||||||
|
return activeSort.value.startsWith(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortMark(key: string) {
|
||||||
|
if (key === "price") return activeSort.value === "priceAsc" ? "▲" : activeSort.value === "priceDesc" ? "▼" : "↕";
|
||||||
|
if (key === "ratio") return activeSort.value === "ratioAsc" ? "▲" : activeSort.value === "ratioDesc" ? "▼" : "↕";
|
||||||
|
if (key === "coin") return activeSort.value === "coinAsc" ? "▲" : activeSort.value === "coinDesc" ? "▼" : "↕";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function backendSortKey() {
|
||||||
|
if (activeSort.value === "priceAsc" || activeSort.value === "priceDesc" || activeSort.value === "coinDesc") {
|
||||||
|
return activeSort.value;
|
||||||
|
}
|
||||||
|
return "recommended";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleListValue(list: string[], value: string) {
|
||||||
|
const index = list.indexOf(value);
|
||||||
|
if (index >= 0) {
|
||||||
|
list.splice(index, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelected(list: string[], value: string) {
|
||||||
|
return list.includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
async function copyListing(item: Listing) {
|
async function copyListing(item: Listing) {
|
||||||
const text = `${formatListingCode(item)} 纯币 ${formatCoin(item)} ¥${item.price} 比例 ${formatRatio(item)}`;
|
const text = `${formatListingCode(item)} 纯币 ${formatCoin(item)} ¥${item.price} 比例 ${formatRatio(item)}`;
|
||||||
await navigator.clipboard?.writeText(text);
|
await navigator.clipboard?.writeText(text);
|
||||||
@@ -146,15 +271,32 @@ async function copyListing(item: Listing) {
|
|||||||
}, 1600);
|
}, 1600);
|
||||||
}
|
}
|
||||||
|
|
||||||
function zoneCount(key: string) {
|
|
||||||
return Number(zoneCounts.value[key] || 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toNumber(value: string) {
|
function toNumber(value: string) {
|
||||||
if (!value.trim()) return undefined;
|
if (!value.trim()) return undefined;
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
return Number.isFinite(parsed) ? parsed : undefined;
|
return Number.isFinite(parsed) ? parsed : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ratioValue(item: Listing) {
|
||||||
|
const configuredRatio = readAssetNumber(item, "publish_ratio");
|
||||||
|
if (configuredRatio > 0) return configuredRatio;
|
||||||
|
if (!item.price) return 0;
|
||||||
|
return Number(item.haf_coin_amount || 0) / 10000 / item.price;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupTitle(key: string, title: string) {
|
||||||
|
if (key === "melee") return "刀皮";
|
||||||
|
if (key === "operatorGold") return "干员金皮";
|
||||||
|
if (key === "operatorRed") return "干员红皮";
|
||||||
|
if (key === "weapon") return "枪皮";
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
function insuranceLabel(value: string) {
|
||||||
|
const [rows, cols] = value.split("*").map((item) => Number(item));
|
||||||
|
if (rows > 0 && cols > 0) return `${rows * cols}格`;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -193,64 +335,127 @@ function toNumber(value: string) {
|
|||||||
<span>{{ announcements[0] }}</span>
|
<span>{{ announcements[0] }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="filters">
|
<section class="quick-filter">
|
||||||
<div class="segmented" aria-label="专区筛选">
|
<div class="sort-line">
|
||||||
<button
|
|
||||||
v-for="zone in zoneOptions"
|
|
||||||
:key="zone.key"
|
|
||||||
:class="{ active: activeZone === zone.key }"
|
|
||||||
type="button"
|
|
||||||
@click="activeZone = zone.key"
|
|
||||||
>
|
|
||||||
{{ zone.label }} <span>{{ zone.count }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sorts">
|
|
||||||
<button
|
<button
|
||||||
v-for="sort in sortOptions"
|
v-for="sort in sortOptions"
|
||||||
:key="sort.key"
|
:key="sort.key"
|
||||||
:class="{ active: activeSort === sort.key }"
|
:class="{ active: isQuickSortActive(sort.key) }"
|
||||||
type="button"
|
type="button"
|
||||||
@click="activeSort = sort.key"
|
@click="setQuickSort(sort.key)"
|
||||||
>
|
>
|
||||||
<ArrowDownUp v-if="sort.key !== 'recommended'" :size="13" />
|
|
||||||
{{ sort.label }}
|
{{ sort.label }}
|
||||||
|
<span v-if="sort.sortable" class="sort-mark">{{ sortMark(sort.key) }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button :class="{ active: filterOpen || hasAdvancedFilters }" type="button" @click="filterOpen = !filterOpen">
|
||||||
|
筛选 <Filter :size="17" />
|
||||||
<div class="rangebar">
|
|
||||||
<label>
|
|
||||||
<span>哈夫币</span>
|
|
||||||
<input v-model="filters.minCoin" inputmode="numeric" placeholder="最低M" @keyup.enter="submitSearch" />
|
|
||||||
<input v-model="filters.maxCoin" inputmode="numeric" placeholder="最高M" @keyup.enter="submitSearch" />
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>价格</span>
|
|
||||||
<input v-model="filters.minPrice" inputmode="numeric" placeholder="最低¥" @keyup.enter="submitSearch" />
|
|
||||||
<input v-model="filters.maxPrice" inputmode="numeric" placeholder="最高¥" @keyup.enter="submitSearch" />
|
|
||||||
</label>
|
|
||||||
<button class="icon-action" type="button" title="应用筛选" @click="submitSearch">
|
|
||||||
<Filter :size="17" />
|
|
||||||
</button>
|
|
||||||
<button class="icon-action" type="button" title="重置" @click="resetFilters">
|
|
||||||
<RefreshCw :size="17" />
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="summary">
|
<section v-if="filterOpen" class="filter-panel">
|
||||||
<div>
|
<div class="filter-head">
|
||||||
<strong>{{ total }}</strong>
|
<strong>筛选条件</strong>
|
||||||
<span>当前可租账号</span>
|
<button type="button" @click="resetFilters">重置</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<strong>{{ zoneCount("highCoin") }}</strong>
|
<div class="filter-body">
|
||||||
<span>100M 以上</span>
|
<label class="filter-field filter-field-wide">
|
||||||
|
<span>账号编号</span>
|
||||||
|
<input v-model="filters.keyword" placeholder="输入编号搜索" @keyup.enter="confirmFilters" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<strong>体力等级</strong>
|
||||||
|
<div class="filter-chips">
|
||||||
|
<button
|
||||||
|
v-for="option in levelOptions"
|
||||||
|
:key="`stamina-${option}`"
|
||||||
|
:class="{ active: isSelected(filters.stamina, option) }"
|
||||||
|
type="button"
|
||||||
|
@click="toggleListValue(filters.stamina, option)"
|
||||||
|
>
|
||||||
|
{{ option }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<strong>负重等级</strong>
|
||||||
|
<div class="filter-chips">
|
||||||
|
<button
|
||||||
|
v-for="option in levelOptions"
|
||||||
|
:key="`load-${option}`"
|
||||||
|
:class="{ active: isSelected(filters.load, option) }"
|
||||||
|
type="button"
|
||||||
|
@click="toggleListValue(filters.load, option)"
|
||||||
|
>
|
||||||
|
{{ option }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<strong>保险格数</strong>
|
||||||
|
<div class="filter-chips">
|
||||||
|
<button
|
||||||
|
v-for="option in insuranceOptions"
|
||||||
|
:key="`insurance-${option}`"
|
||||||
|
:class="{ active: isSelected(filters.insurance, option) }"
|
||||||
|
type="button"
|
||||||
|
@click="toggleListValue(filters.insurance, option)"
|
||||||
|
>
|
||||||
|
{{ insuranceLabel(option) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="section in skinSections" :key="section.key" class="filter-group filter-group-wide">
|
||||||
|
<strong>{{ section.title }}</strong>
|
||||||
|
<div class="filter-chips">
|
||||||
|
<button
|
||||||
|
v-for="option in section.options"
|
||||||
|
:key="`${section.key}-${option}`"
|
||||||
|
:class="{ active: isSelected(filters.skinNames, option) }"
|
||||||
|
type="button"
|
||||||
|
@click="toggleListValue(filters.skinNames, option)"
|
||||||
|
>
|
||||||
|
{{ option }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-range filter-group-wide">
|
||||||
|
<strong>哈夫币范围(M)</strong>
|
||||||
|
<div>
|
||||||
|
<input v-model="filters.minCoin" inputmode="numeric" placeholder="最小" @keyup.enter="confirmFilters" />
|
||||||
|
<span>-</span>
|
||||||
|
<input v-model="filters.maxCoin" inputmode="numeric" placeholder="最大" @keyup.enter="confirmFilters" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-range filter-group-wide">
|
||||||
|
<strong>价格范围</strong>
|
||||||
|
<div>
|
||||||
|
<input v-model="filters.minPrice" inputmode="numeric" placeholder="最低¥" @keyup.enter="confirmFilters" />
|
||||||
|
<span>-</span>
|
||||||
|
<input v-model="filters.maxPrice" inputmode="numeric" placeholder="最高¥" @keyup.enter="confirmFilters" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-range filter-group-wide">
|
||||||
|
<strong>KD 范围</strong>
|
||||||
|
<div>
|
||||||
|
<input v-model="filters.minKd" inputmode="decimal" placeholder="最小" @keyup.enter="confirmFilters" />
|
||||||
|
<span>-</span>
|
||||||
|
<input v-model="filters.maxKd" inputmode="decimal" placeholder="最大" @keyup.enter="confirmFilters" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<strong>{{ zoneCount("sale") }}</strong>
|
<div class="filter-actions">
|
||||||
<span>特惠账号</span>
|
<button type="button" @click="filterOpen = false">取消</button>
|
||||||
|
<button type="button" @click="confirmFilters">确定筛选</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -265,7 +470,7 @@ function toNumber(value: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section v-else class="grid">
|
<section v-else class="grid">
|
||||||
<article v-for="item in listings" :key="item.id" class="card">
|
<article v-for="item in displayListings" :key="item.id" class="card">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<div>
|
<div>
|
||||||
<h2>编号 {{ formatListingCode(item) }}</h2>
|
<h2>编号 {{ formatListingCode(item) }}</h2>
|
||||||
|
|||||||
+26
@@ -45,10 +45,18 @@ export interface PublicListingQuery {
|
|||||||
keyword?: string;
|
keyword?: string;
|
||||||
sort?: string;
|
sort?: string;
|
||||||
zone?: string;
|
zone?: string;
|
||||||
|
login_method?: string;
|
||||||
|
rank?: string;
|
||||||
|
insurance?: string;
|
||||||
|
stamina?: string;
|
||||||
|
load?: string;
|
||||||
|
skin_name?: string;
|
||||||
min_coin?: number;
|
min_coin?: number;
|
||||||
max_coin?: number;
|
max_coin?: number;
|
||||||
min_price?: number;
|
min_price?: number;
|
||||||
max_price?: number;
|
max_price?: number;
|
||||||
|
min_secret_kd?: number;
|
||||||
|
max_secret_kd?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HomeBannerSlide {
|
export interface HomeBannerSlide {
|
||||||
@@ -66,6 +74,11 @@ export interface ListingPublishOptions {
|
|||||||
rank_options: string[];
|
rank_options: string[];
|
||||||
insurance_options: string[];
|
insurance_options: string[];
|
||||||
level_options: string[];
|
level_options: string[];
|
||||||
|
skin_groups: Array<{
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
options: string[];
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MobileHomeConfig {
|
export interface MobileHomeConfig {
|
||||||
@@ -121,9 +134,22 @@ function normalizeOptions(value: unknown): ListingPublishOptions {
|
|||||||
rank_options: normalizeStringArray(options.rank_options),
|
rank_options: normalizeStringArray(options.rank_options),
|
||||||
insurance_options: normalizeStringArray(options.insurance_options),
|
insurance_options: normalizeStringArray(options.insurance_options),
|
||||||
level_options: normalizeStringArray(options.level_options),
|
level_options: normalizeStringArray(options.level_options),
|
||||||
|
skin_groups: normalizeSkinGroups(options.skin_groups),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSkinGroups(value: unknown) {
|
||||||
|
if (!Array.isArray(value)) return [];
|
||||||
|
return value
|
||||||
|
.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null)
|
||||||
|
.map((item) => ({
|
||||||
|
key: typeof item.key === "string" ? item.key : "",
|
||||||
|
title: typeof item.title === "string" ? item.title : "",
|
||||||
|
options: normalizeStringArray(item.options),
|
||||||
|
}))
|
||||||
|
.filter((item) => item.key && item.title);
|
||||||
|
}
|
||||||
|
|
||||||
async function request<T>(path: string, query: Record<string, unknown> = {}) {
|
async function request<T>(path: string, query: Record<string, unknown> = {}) {
|
||||||
const url = new URL(`${apiBase}${path}`, window.location.origin);
|
const url = new URL(`${apiBase}${path}`, window.location.origin);
|
||||||
Object.entries(query).forEach(([key, value]) => {
|
Object.entries(query).forEach(([key, value]) => {
|
||||||
|
|||||||
+162
-125
@@ -196,10 +196,8 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.notice span,
|
.notice span,
|
||||||
.segmented,
|
.quick-filter,
|
||||||
.sorts,
|
.filter-panel,
|
||||||
.rangebar,
|
|
||||||
.summary,
|
|
||||||
.grid {
|
.grid {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
@@ -209,114 +207,160 @@ button {
|
|||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters {
|
.quick-filter {
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
margin-bottom: 20px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.segmented,
|
.sort-line {
|
||||||
.sorts {
|
min-height: 66px;
|
||||||
display: flex;
|
display: grid;
|
||||||
justify-content: center;
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
gap: 6px;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.segmented button,
|
.sort-line button {
|
||||||
.sorts button {
|
min-height: 66px;
|
||||||
min-height: 34px;
|
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #777b85;
|
color: #686d77;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 5px;
|
||||||
border-radius: 6px;
|
padding: 0 8px;
|
||||||
padding: 0 18px;
|
font-weight: 700;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-line button.active {
|
||||||
|
color: #ff506d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-mark {
|
||||||
|
color: #c4c6cc;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-panel {
|
||||||
|
background: #fff;
|
||||||
|
margin: -8px 0 20px;
|
||||||
|
border-top: 1px solid #f0f1f4;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
box-shadow: 0 8px 18px rgba(24, 26, 32, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-head {
|
||||||
|
min-height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-bottom: 1px solid #f0f1f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-head button {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #ff506d;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.segmented button.active,
|
.filter-body {
|
||||||
.sorts button.active {
|
|
||||||
color: #ff506d;
|
|
||||||
background: #fff0f3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.segmented span {
|
|
||||||
color: #b4b7bd;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rangebar {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 36px 36px;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 8px;
|
gap: 18px 28px;
|
||||||
|
padding: 16px 10px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rangebar label {
|
.filter-field,
|
||||||
|
.filter-group,
|
||||||
|
.filter-range {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: max-content 1fr 1fr;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rangebar label span {
|
|
||||||
color: #757985;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rangebar input {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
height: 34px;
|
|
||||||
border: 1px solid #eceef2;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0 8px;
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-action {
|
|
||||||
width: 36px;
|
|
||||||
height: 34px;
|
|
||||||
border: 1px solid #eceef2;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #fff;
|
|
||||||
color: #606571;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin: 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary div {
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 58px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #fff;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
align-content: center;
|
|
||||||
box-shadow: 0 1px 8px rgba(24, 26, 32, 0.04);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary strong {
|
.filter-field-wide,
|
||||||
font-size: 22px;
|
.filter-group-wide {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-field span,
|
||||||
|
.filter-group strong,
|
||||||
|
.filter-range strong {
|
||||||
|
color: #33363d;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-field input,
|
||||||
|
.filter-range input {
|
||||||
|
width: 100%;
|
||||||
|
height: 38px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 5px;
|
||||||
|
outline: 0;
|
||||||
|
padding: 0 12px;
|
||||||
color: #33363d;
|
color: #33363d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary span {
|
.filter-chips {
|
||||||
font-size: 12px;
|
display: flex;
|
||||||
color: #8e929c;
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chips button {
|
||||||
|
min-width: 58px;
|
||||||
|
min-height: 36px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #fff;
|
||||||
|
color: #555b66;
|
||||||
|
padding: 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chips button.active {
|
||||||
|
border-color: #ff6f86;
|
||||||
|
color: #ff506d;
|
||||||
|
background: #fff2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-range > div {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) max-content minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-range span {
|
||||||
|
color: #9aa0aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 10px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions button {
|
||||||
|
height: 44px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions button:first-child {
|
||||||
|
background: #f2f2f3;
|
||||||
|
color: #70757f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions button:last-child {
|
||||||
|
background: #ff6f86;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
@@ -516,8 +560,8 @@ button {
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.rangebar {
|
.filter-body {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,49 +583,42 @@ button {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.segmented,
|
|
||||||
.sorts {
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
overflow-x: hidden;
|
|
||||||
max-width: 100%;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.segmented::-webkit-scrollbar,
|
|
||||||
.sorts::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.segmented button,
|
|
||||||
.sorts button {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-copy h1 {
|
.hero-copy h1 {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sort-line {
|
||||||
|
grid-template-columns: repeat(5, max-content);
|
||||||
|
overflow-x: auto;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-line button {
|
||||||
|
min-width: 68px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-panel {
|
||||||
|
margin-top: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-body {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chips button {
|
||||||
|
min-width: 58px;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rangebar {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rangebar label {
|
|
||||||
grid-template-columns: 52px 1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-action {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user