diff --git a/src/App.vue b/src/App.vue index 1f4ba6f..2aa298a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -6,12 +6,18 @@ import { Copy, Filter, LoaderCircle, - RefreshCw, Search, ShieldCheck, Sparkles, } 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 { formatCoin, formatListingCode, @@ -28,34 +34,61 @@ const loadError = ref(""); const listings = ref([]); const total = ref(0); const page = ref(1); -const zoneCounts = ref>({}); const announcements = ref([]); const banners = ref([]); +const publishOptions = ref({ + server_options: [], + login_method_options: [], + rank_options: [], + insurance_options: [], + level_options: [], + skin_groups: [], +}); const copiedCode = ref(""); const activeSort = ref("recommended"); -const activeZone = ref("all"); +const filterOpen = ref(false); const filters = reactive({ keyword: "", minCoin: "", maxCoin: "", minPrice: "", maxPrice: "", + minKd: "", + maxKd: "", + stamina: [] as string[], + load: [] as string[], + insurance: [] as string[], + skinNames: [] as string[], }); const sortOptions = [ - { key: "recommended", label: "默认" }, - { key: "priceAsc", label: "价格" }, - { key: "coinDesc", label: "哈夫币" }, - { key: "awmDesc", label: "AWM" }, + { key: "recommended", label: "默认", sortable: false }, + { key: "price", label: "价格", sortable: true }, + { key: "ratio", label: "比例", sortable: true }, + { key: "coin", label: "哈夫币", sortable: true }, ]; -const zoneOptions = computed(() => [ - { key: "all", label: "全部", count: zoneCount("all") }, - { key: "sale", label: "特惠", count: zoneCount("sale") }, - { key: "highCoin", label: "高币", count: zoneCount("highCoin") }, - { key: "password", label: "账密", count: zoneCount("password") }, - { key: "night", label: "夜间", count: zoneCount("night") }, -]); +const fallbackLevels = ["3级", "4级", "5级", "6级", "7级"]; +const fallbackInsurance = ["2*1", "2*2", "2*3", "3*3"]; +const fallbackSkins = ["信条", "坠星者", "处刑者", "影锋", "暗星", "电锯", "蚀金玫瑰", "维什戴尔", "水墨云图"]; + +const levelOptions = computed(() => publishOptions.value.level_options.length ? publishOptions.value.level_options : fallbackLevels); +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] || { eyebrow: "三角洲行动账号专区", @@ -66,13 +99,40 @@ const topBanner = computed(() => banners.value[0] || { }); 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(() => { loadHome(); }); watch( - () => [activeSort.value, activeZone.value], + () => activeSort.value, () => loadListings(true) ); @@ -82,6 +142,7 @@ async function loadHome() { const [home] = await Promise.all([fetchMobileHomeConfig(), loadListings(true)]); announcements.value = home.announcements; banners.value = home.banners; + publishOptions.value = home.publish_options; } catch (error) { loadError.value = error instanceof Error ? error.message : "加载失败"; } finally { @@ -99,7 +160,6 @@ async function loadListings(reset = false) { listings.value = reset ? result.items : [...listings.value, ...result.items]; total.value = result.total; page.value = result.page + 1; - zoneCounts.value = result.zone_counts; } catch (error) { loadError.value = error instanceof Error ? error.message : "加载失败"; if (reset) listings.value = []; @@ -113,12 +173,17 @@ function buildQuery(nextPage: number): PublicListingQuery { page: nextPage, page_size: pageSize, keyword: filters.keyword.trim(), - sort: activeSort.value, - zone: activeZone.value, + sort: backendSortKey(), min_coin: toNumber(filters.minCoin), max_coin: toNumber(filters.maxCoin), min_price: toNumber(filters.minPrice), 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.minPrice = ""; filters.maxPrice = ""; + filters.minKd = ""; + filters.maxKd = ""; + filters.stamina = []; + filters.load = []; + filters.insurance = []; + filters.skinNames = []; activeSort.value = "recommended"; - activeZone.value = "all"; 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) { const text = `${formatListingCode(item)} 纯币 ${formatCoin(item)} ¥${item.price} 比例 ${formatRatio(item)}`; await navigator.clipboard?.writeText(text); @@ -146,15 +271,32 @@ async function copyListing(item: Listing) { }, 1600); } -function zoneCount(key: string) { - return Number(zoneCounts.value[key] || 0); -} - function toNumber(value: string) { if (!value.trim()) return undefined; const parsed = Number(value); 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; +}