增加前端格式检查配置

This commit is contained in:
yml2213
2026-06-08 06:40:33 +08:00
parent 79ea3a476c
commit 500f511185
150 changed files with 7254 additions and 4013 deletions
@@ -1,24 +1,26 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { RouterLink, useRouter } from "vue-router";
import { showToast } from "vant";
import MobileBottomNav from "@/components/MobileBottomNav.vue";
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
import { showToast } from 'vant'
import MobileBottomNav from '@/components/MobileBottomNav.vue'
import { ensureSupportChat } from "@/features/chats/api/chats";
import { ensureSupportChat } from '@/features/chats/api/chats'
import {
emptyListingPublishOptions,
type ListingPublishOptions,
} from "@/features/listings/api/listingOptions";
import { fetchListingsPage, type Listing, type PublicListingQuery } from "@/features/listings/api/listings";
} from '@/features/listings/api/listingOptions'
import {
fetchListingsPage,
type Listing,
type PublicListingQuery,
} from '@/features/listings/api/listings'
import {
defaultHomeAnnouncements,
defaultHomeBanners,
fetchMobileHomeConfig,
type HomeBannerSlide,
} from "@/features/listings/api/homeConfig";
import MobileHomeFilterSheet, {
type FilterSection,
} from "./MobileHomeFilterSheet.vue";
} from '@/features/listings/api/homeConfig'
import MobileHomeFilterSheet, { type FilterSection } from './MobileHomeFilterSheet.vue'
import {
getListingChips,
getListingDisplayPrice,
@@ -28,237 +30,256 @@ import {
getServerRegion,
hasAcceleratedSaleRatio,
hasGiftResources,
} from "@/utils/listingDisplay";
import { useSessionStore } from "@/stores/session";
} from '@/utils/listingDisplay'
import { useSessionStore } from '@/stores/session'
const router = useRouter();
const session = useSessionStore();
const loading = ref(false);
const loadingMore = ref(false);
const loadFailed = ref(false);
const listings = ref<Listing[]>([]);
const totalListings = ref(0);
const currentPage = ref(1);
const hasMoreListings = ref(true);
const publishOptions = ref<ListingPublishOptions>(emptyListingPublishOptions);
const sortOpen = ref(false);
const activeSort = ref("comprehensive");
const filterOpen = ref(false);
const selectedFilters = ref<Record<string, string[]>>({});
const rangeFilters = ref<Record<string, { min: string; max: string }>>({});
const refreshing = ref(false);
const searchValue = ref("");
const announcements = ref<string[]>(defaultHomeAnnouncements);
const bannerSlides = ref<HomeBannerSlide[]>(defaultHomeBanners);
const supportLoading = ref(false);
const mobilePageSize = 10;
let listingRequestSeq = 0;
const router = useRouter()
const session = useSessionStore()
const loading = ref(false)
const loadingMore = ref(false)
const loadFailed = ref(false)
const listings = ref<Listing[]>([])
const totalListings = ref(0)
const currentPage = ref(1)
const hasMoreListings = ref(true)
const publishOptions = ref<ListingPublishOptions>(emptyListingPublishOptions)
const sortOpen = ref(false)
const activeSort = ref('comprehensive')
const filterOpen = ref(false)
const selectedFilters = ref<Record<string, string[]>>({})
const rangeFilters = ref<Record<string, { min: string; max: string }>>({})
const refreshing = ref(false)
const searchValue = ref('')
const announcements = ref<string[]>(defaultHomeAnnouncements)
const bannerSlides = ref<HomeBannerSlide[]>(defaultHomeBanners)
const supportLoading = ref(false)
const mobilePageSize = 10
let listingRequestSeq = 0
const sortOptions = [
{ key: "comprehensive", label: "综合排序" },
{ key: "published", label: "发布时间" },
{ key: "awmDesc", label: "AWM数量" },
{ key: "priceAsc", label: "价格最低" },
{ key: "priceDesc", label: "价格最高" },
];
{ key: 'comprehensive', label: '综合排序' },
{ key: 'published', label: '发布时间' },
{ key: 'awmDesc', label: 'AWM数量' },
{ key: 'priceAsc', label: '价格最低' },
{ key: 'priceDesc', label: '价格最高' },
]
const rangePresets: Record<string, Array<{ label: string; min: string; max: string }>> = {
coin: [
{ label: "50-100", min: "50", max: "100" },
{ label: "100-200", min: "100", max: "200" },
{ label: "200-300", min: "200", max: "300" },
{ label: "300-500", min: "300", max: "500" },
{ label: "500以上", min: "500", max: "" },
{ label: '50-100', min: '50', max: '100' },
{ label: '100-200', min: '100', max: '200' },
{ label: '200-300', min: '200', max: '300' },
{ label: '300-500', min: '300', max: '500' },
{ label: '500以上', min: '500', 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: "" },
{ 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 activeSortLabel = computed(
() =>
sortOptions.find((option) => option.key === activeSort.value)?.label ||
"综合排序"
);
() => sortOptions.find(option => option.key === activeSort.value)?.label || '综合排序'
)
async function handleSupportClick() {
if (!session.isLoggedIn) {
router.push({ path: "/m/login", query: { redirect: router.currentRoute.value.fullPath } });
return;
router.push({ path: '/m/login', query: { redirect: router.currentRoute.value.fullPath } })
return
}
if (supportLoading.value) return;
supportLoading.value = true;
if (supportLoading.value) return
supportLoading.value = true
try {
const chat = await ensureSupportChat();
router.push(`/m/chats/${chat.id}`);
const chat = await ensureSupportChat()
router.push(`/m/chats/${chat.id}`)
} catch {
showToast({ message: "联系客服失败,请稍后重试", icon: "cross" });
showToast({ message: '联系客服失败,请稍后重试', icon: 'cross' })
} finally {
supportLoading.value = false;
supportLoading.value = false
}
}
const serverFilterOptions = computed(() =>
uniqueOptions(
publishOptions.value.server_options
.map((item) => item.trim())
.filter(Boolean)
)
);
uniqueOptions(publishOptions.value.server_options.map(item => item.trim()).filter(Boolean))
)
const loginMethodFilterOptions = computed(() =>
uniqueOptions(
publishOptions.value.login_method_options
.map((item) => item.trim())
.filter(Boolean)
)
);
uniqueOptions(publishOptions.value.login_method_options.map(item => item.trim()).filter(Boolean))
)
const filterSections = computed<FilterSection[]>(() => [
{ key: "price", title: "价格区间", type: "range", unit: "元", minPlaceholder: "最低价", maxPlaceholder: "最高价" },
{ key: "coin", title: "哈夫币数量", type: "range", unit: "M", minPlaceholder: "最低", maxPlaceholder: "最高" },
{ key: "server", title: "区服", type: "chips", options: serverFilterOptions.value },
{ key: "login", title: "上号方式", type: "chips", options: loginMethodFilterOptions.value },
{ key: "insurance", title: "保险", type: "chips", options: publishOptions.value.insurance_options },
{ 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: 'price',
title: '价格区间',
type: 'range',
unit: '元',
minPlaceholder: '最低价',
maxPlaceholder: '最高价',
},
{
key: 'coin',
title: '哈夫币数量',
type: 'range',
unit: 'M',
minPlaceholder: '最低',
maxPlaceholder: '最高',
},
{ key: 'server', title: '区服', type: 'chips', options: serverFilterOptions.value },
{ key: 'login', title: '上号方式', type: 'chips', options: loginMethodFilterOptions.value },
{
key: 'insurance',
title: '保险',
type: 'chips',
options: publishOptions.value.insurance_options,
},
{ 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,
type: 'range' as const,
unit: parseQuantityUnit(item.price),
minPlaceholder: "最低",
maxPlaceholder: "最高",
minPlaceholder: '最低',
maxPlaceholder: '最高',
})),
...publishOptions.value.skin_groups.map((group) => ({
...publishOptions.value.skin_groups.map(group => ({
key: group.key,
title: group.title,
type: "chips" as const,
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: "最高" },
]);
{
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 rangeCount = Object.values(rangeFilters.value).filter(
(range) => range.min || range.max
).length;
return chipCount + rangeCount;
});
range => range.min || range.max
).length
return chipCount + rangeCount
})
const displayListings = computed(() => {
return listings.value;
});
return listings.value
})
onMounted(() => {
loadListings();
loadHomeConfig();
window.addEventListener("scroll", handleWindowScroll, { passive: true });
});
loadListings()
loadHomeConfig()
window.addEventListener('scroll', handleWindowScroll, { passive: true })
})
onBeforeUnmount(() => {
window.removeEventListener("scroll", handleWindowScroll);
});
window.removeEventListener('scroll', handleWindowScroll)
})
watch(
() => listingQuerySignature(),
() => {
loadListings(true);
loadListings(true)
}
);
)
async function loadListings(reset = true) {
if ((loadingMore.value && !reset) || (!hasMoreListings.value && !reset)) return;
const requestSeq = ++listingRequestSeq;
if ((loadingMore.value && !reset) || (!hasMoreListings.value && !reset)) return
const requestSeq = ++listingRequestSeq
if (reset) {
loading.value = true;
currentPage.value = 1;
hasMoreListings.value = true;
loading.value = true
currentPage.value = 1
hasMoreListings.value = true
}
loadingMore.value = true;
loadFailed.value = false;
loadingMore.value = true
loadFailed.value = false
try {
const page = await fetchListingsPage(buildListingQuery(currentPage.value));
if (requestSeq !== listingRequestSeq) return;
listings.value = reset ? page.items : [...listings.value, ...page.items];
totalListings.value = page.total;
hasMoreListings.value = listings.value.length < page.total;
currentPage.value = page.page + 1;
requestAnimationFrame(handleWindowScroll);
const page = await fetchListingsPage(buildListingQuery(currentPage.value))
if (requestSeq !== listingRequestSeq) return
listings.value = reset ? page.items : [...listings.value, ...page.items]
totalListings.value = page.total
hasMoreListings.value = listings.value.length < page.total
currentPage.value = page.page + 1
requestAnimationFrame(handleWindowScroll)
} catch {
if (reset) {
listings.value = [];
totalListings.value = 0;
hasMoreListings.value = false;
loadFailed.value = true;
listings.value = []
totalListings.value = 0
hasMoreListings.value = false
loadFailed.value = true
}
} finally {
if (requestSeq === listingRequestSeq) {
loading.value = false;
loadingMore.value = false;
loading.value = false
loadingMore.value = false
}
}
}
async function loadHomeConfig() {
try {
const config = await fetchMobileHomeConfig();
announcements.value = config.announcements;
bannerSlides.value = config.banners;
publishOptions.value = config.publish_options;
const config = await fetchMobileHomeConfig()
announcements.value = config.announcements
bannerSlides.value = config.banners
publishOptions.value = config.publish_options
} catch {
announcements.value = defaultHomeAnnouncements;
bannerSlides.value = defaultHomeBanners;
publishOptions.value = emptyListingPublishOptions;
announcements.value = defaultHomeAnnouncements
bannerSlides.value = defaultHomeBanners
publishOptions.value = emptyListingPublishOptions
}
}
async function onRefresh() {
refreshing.value = true;
refreshing.value = true
try {
const [, nextHomeConfig] = await Promise.all([
loadListings(true),
fetchMobileHomeConfig(),
]);
announcements.value = nextHomeConfig.announcements;
bannerSlides.value = nextHomeConfig.banners;
publishOptions.value = nextHomeConfig.publish_options;
showToast({ message: "刷新成功", icon: "passed" });
const [, nextHomeConfig] = await Promise.all([loadListings(true), fetchMobileHomeConfig()])
announcements.value = nextHomeConfig.announcements
bannerSlides.value = nextHomeConfig.banners
publishOptions.value = nextHomeConfig.publish_options
showToast({ message: '刷新成功', icon: 'passed' })
} catch {
// 静默处理
} finally {
refreshing.value = false;
refreshing.value = false
}
}
function openFilters() {
sortOpen.value = false;
filterOpen.value = true;
sortOpen.value = false
filterOpen.value = true
}
function toggleSortPanel() {
sortOpen.value = !sortOpen.value;
sortOpen.value = !sortOpen.value
}
function selectSort(sortKey: string) {
activeSort.value = sortKey;
sortOpen.value = false;
activeSort.value = sortKey
sortOpen.value = false
}
function clearFilters() {
selectedFilters.value = {};
rangeFilters.value = {};
searchValue.value = "";
selectedFilters.value = {}
rangeFilters.value = {}
searchValue.value = ''
}
function buildListingQuery(page: number): PublicListingQuery {
@@ -267,79 +288,78 @@ function buildListingQuery(page: number): PublicListingQuery {
page_size: mobilePageSize,
keyword: searchValue.value.trim(),
sort: activeSort.value,
};
const skinGroups: string[] = [];
const skinNames: string[] = [];
}
const skinGroups: string[] = []
const skinNames: string[] = []
for (const [key, values] of Object.entries(selectedFilters.value)) {
const value = values.filter(Boolean).join(",");
if (!value) continue;
if (key === "server") query.server = value;
else if (key === "login") query.login_method = value;
else if (key === "insurance") query.insurance = value;
else if (key === "stamina") query.stamina = value;
else if (key === "load") query.load = value;
else if (key === "rank") query.rank = value;
const value = values.filter(Boolean).join(',')
if (!value) continue
if (key === 'server') query.server = value
else if (key === 'login') query.login_method = value
else if (key === 'insurance') query.insurance = value
else if (key === 'stamina') query.stamina = value
else if (key === 'load') query.load = value
else if (key === 'rank') query.rank = value
else if (isSkinGroupKey(key)) {
skinGroups.push(key);
skinNames.push(...values);
skinGroups.push(key)
skinNames.push(...values)
}
}
if (skinGroups.length) query.skin_group = skinGroups.join(",");
if (skinNames.length) query.skin_name = skinNames.join(",");
if (skinGroups.length) query.skin_group = skinGroups.join(',')
if (skinNames.length) query.skin_name = skinNames.join(',')
for (const [key, range] of Object.entries(rangeFilters.value)) {
if (!range.min && !range.max) continue;
const min = parseOptionalNumber(range.min);
const max = parseOptionalNumber(range.max);
if (key === "price") {
query.min_price = min;
query.max_price = max;
} else if (key === "coin") {
query.min_coin = min;
query.max_coin = max;
} else if (key === "secretKd") {
query.min_secret_kd = min;
query.max_secret_kd = max;
} else if (key === "deposit") {
query.min_deposit = min;
query.max_deposit = max;
} else if (key.startsWith("resource_")) {
const resourceKey = key.replace("resource_", "");
query[`resource_${resourceKey}_min`] = min;
query[`resource_${resourceKey}_max`] = max;
if (!range.min && !range.max) continue
const min = parseOptionalNumber(range.min)
const max = parseOptionalNumber(range.max)
if (key === 'price') {
query.min_price = min
query.max_price = max
} else if (key === 'coin') {
query.min_coin = min
query.max_coin = max
} else if (key === 'secretKd') {
query.min_secret_kd = min
query.max_secret_kd = max
} else if (key === 'deposit') {
query.min_deposit = min
query.max_deposit = max
} else if (key.startsWith('resource_')) {
const resourceKey = key.replace('resource_', '')
query[`resource_${resourceKey}_min`] = min
query[`resource_${resourceKey}_max`] = max
}
}
return query;
return query
}
function listingQuerySignature() {
return JSON.stringify(buildListingQuery(1));
return JSON.stringify(buildListingQuery(1))
}
function parseOptionalNumber(value: string) {
if (value === "") return undefined;
const number = Number(value);
return Number.isFinite(number) ? number : undefined;
if (value === '') return undefined
const number = Number(value)
return Number.isFinite(number) ? number : undefined
}
function handleWindowScroll() {
if (window.innerHeight + window.scrollY < document.documentElement.scrollHeight - 360) return;
loadListings(false);
if (window.innerHeight + window.scrollY < document.documentElement.scrollHeight - 360) return
loadListings(false)
}
function isSkinGroupKey(key: string) {
return publishOptions.value.skin_groups.some((group) => group.key === key);
return publishOptions.value.skin_groups.some(group => group.key === key)
}
function parseQuantityUnit(price: string) {
const unit = price.split("/")[1]?.trim();
return unit || undefined;
const unit = price.split('/')[1]?.trim()
return unit || undefined
}
function uniqueOptions(values: string[]) {
return [...new Set(values.map((item) => item.trim()).filter(Boolean))];
return [...new Set(values.map(item => item.trim()).filter(Boolean))]
}
</script>
<template>
@@ -360,8 +380,13 @@ function uniqueOptions(values: string[]) {
placeholder="搜区服 / 段位"
class="home-search"
/>
<button class="mobile-service" type="button" :disabled="supportLoading" @click="handleSupportClick">
{{ supportLoading ? "接入中" : "客服" }}
<button
class="mobile-service"
type="button"
:disabled="supportLoading"
@click="handleSupportClick"
>
{{ supportLoading ? '接入中' : '客服' }}
</button>
</div>
@@ -388,10 +413,7 @@ function uniqueOptions(values: string[]) {
<section class="mobile-content">
<!-- Banner 轮播 -->
<van-swipe class="banner-swipe" :autoplay="3600" lazy-render>
<van-swipe-item
v-for="slide in bannerSlides"
:key="slide.title || slide.image_url"
>
<van-swipe-item v-for="slide in bannerSlides" :key="slide.title || slide.image_url">
<div
class="mobile-banner"
:class="[`tone-${slide.tone}`, { 'has-image': slide.image_url }]"
@@ -432,11 +454,7 @@ function uniqueOptions(values: string[]) {
@click="selectSort(option.key)"
>
<span>{{ option.label }}</span>
<van-icon
v-if="activeSort === option.key"
name="success"
:size="18"
/>
<van-icon v-if="activeSort === option.key" name="success" :size="18" />
</button>
</div>
<div class="result-count">
@@ -460,9 +478,7 @@ function uniqueOptions(values: string[]) {
image="search"
description="没有符合条件的账号"
>
<van-button size="small" type="primary" @click="clearFilters">
重置条件
</van-button>
<van-button size="small" type="primary" @click="clearFilters"> 重置条件 </van-button>
</van-empty>
<!-- 列表卡片全宽上下布局 -->
@@ -510,10 +526,7 @@ function uniqueOptions(values: string[]) {
</div>
</div>
<div class="card-chip-row">
<span
v-for="chip in getListingChips(item)"
:key="`${item.id}-${chip.label}`"
>
<span v-for="chip in getListingChips(item)" :key="`${item.id}-${chip.label}`">
{{ chip.label }}:{{ chip.value }}
</span>
</div>