Files
hfb_show/src/api.ts
T
yml2213 51991071e7 展示额外消耗品总价与明细悬浮卡片
租金改为「包含额外物品」;按 resources 收费项汇总额外消耗品金额,悬浮展示物品明细。
2026-07-13 23:54:09 +08:00

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;
}