175 lines
5.0 KiB
TypeScript
175 lines
5.0 KiB
TypeScript
export interface ApiResponse<T> {
|
|
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<string, unknown>;
|
|
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<string, number>;
|
|
}
|
|
|
|
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<Partial<PublicListingPage>>("/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<Partial<MobileHomeConfig>>("/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<HomeBannerSlide> => 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<string, unknown>) : {};
|
|
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<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> = {}) {
|
|
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<T>;
|
|
return payload.data;
|
|
}
|