export interface ApiResponse { code: string; message: string; data: T; } export interface ListingResource { key: string; label: string; price: string; quantity: number; mode: string; /** 收费项小计(元),赠送为 0 */ amount: number; } export interface Listing { id: number; listing_no?: string; title: string; description: string; game_name: string; server_region: string; login_platform: string; rank_level: string; haf_coin_amount: number; asset_summary?: Record; screenshot_urls: string[]; cover_url: string; price_cent: number; deposit_amount_cent: number; /** @deprecated 兼容旧字段,优先使用 price_cent */ price?: number; /** @deprecated 兼容旧字段,优先使用 deposit_amount_cent */ deposit_amount?: number; is_accelerated_sale?: boolean; in_transaction: boolean; published_at?: string; } export interface PublicListingPage { items: Listing[]; total: number; page: number; page_size: number; zone_counts: Record; } export interface PublicListingQuery { page?: number; page_size?: number; keyword?: string; sort?: string; zone?: string; login_method?: string; rank?: string; insurance?: string; stamina?: string; load?: string; skin_name?: string; min_coin?: number; max_coin?: number; min_price?: number; max_price?: number; min_secret_kd?: number; max_secret_kd?: number; } export interface HomeBannerSlide { eyebrow: string; title: string; badge: string; pill: string; tone: string; image_url?: string; } export interface ListingPublishOptions { server_options: string[]; login_method_options: string[]; rank_options: string[]; insurance_options: string[]; level_options: string[]; skin_groups: Array<{ key: string; title: string; options: string[]; }>; } export interface MobileHomeConfig { announcements: string[]; banners: HomeBannerSlide[]; publish_options: ListingPublishOptions; } const apiBase = import.meta.env.VITE_API_BASE_URL || "/api"; export async function fetchListingsPage(query: PublicListingQuery) { return request>("/listings", { ...query }).then((data) => ({ items: Array.isArray(data.items) ? data.items : [], total: Number(data.total || 0), page: Number(data.page || query.page || 1), page_size: Number(data.page_size || query.page_size || 12), zone_counts: data.zone_counts && typeof data.zone_counts === "object" ? data.zone_counts : {}, })); } export async function fetchMobileHomeConfig() { return request>("/mobile-home-config").then((data) => ({ announcements: normalizeStringArray(data.announcements), banners: normalizeBanners(data.banners), publish_options: normalizeOptions(data.publish_options), })); } function normalizeStringArray(value: unknown) { return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.trim() !== "") : []; } function normalizeBanners(value: unknown): HomeBannerSlide[] { if (!Array.isArray(value)) return []; return value .filter((item): item is Partial => typeof item === "object" && item !== null) .map((item) => ({ eyebrow: item.eyebrow || "", title: item.title || "", badge: item.badge || "", pill: item.pill || "", tone: item.tone || "blue", image_url: item.image_url || "", })) .filter((item) => item.title || item.image_url); } function normalizeOptions(value: unknown): ListingPublishOptions { const options = typeof value === "object" && value !== null ? (value as Record) : {}; return { server_options: normalizeStringArray(options.server_options), login_method_options: normalizeStringArray(options.login_method_options), rank_options: normalizeStringArray(options.rank_options), insurance_options: normalizeStringArray(options.insurance_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 => 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(path: string, query: Record = {}) { const url = new URL(`${apiBase}${path}`, window.location.origin); Object.entries(query).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== "") { url.searchParams.set(key, String(value)); } }); const response = await fetch(url); if (!response.ok) { throw new Error(`请求失败:${response.status}`); } const payload = (await response.json()) as ApiResponse; return payload.data; }