chore: initialize WanderQ MiniAPP 2.0 repository

This commit is contained in:
inman
2026-07-23 15:48:34 +08:00
commit b4c0a1c516
93 changed files with 53525 additions and 0 deletions

217
src/api.ts Normal file
View File

@@ -0,0 +1,217 @@
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "";
export type PublicHeroSlide = {
id: string;
title: string;
kicker?: string | null;
image: string;
targetType?: string | null;
targetValue?: string | null;
isActive?: boolean;
};
export type PublicDestination = {
id: string;
name: string;
image?: string | null;
isHot?: boolean;
isActive?: boolean;
aliases?: { id: string; alias: string }[];
};
export type PublicTheme = {
id: string;
label: string;
image: string;
targetType?: string | null;
targetValue?: string | null;
isActive?: boolean;
};
export type PublicCta = {
id: string;
alt: string;
image: string;
targetType?: string | null;
targetValue?: string | null;
isActive?: boolean;
};
export type PublicSearchPageKeyword = {
id: string;
label: string;
image?: string | null;
isActive?: boolean;
sortOrder: number;
};
export type PublicSearchPageGroup = {
id: string;
label: string;
items: PublicSearchPageKeyword[];
isActive?: boolean;
sortOrder: number;
};
export type PublicSearchPageModuleCopy = {
title: string;
subtitle: string;
};
export type PublicSearchPageConfig = {
placeholder: string;
modules: {
seasonalInspiration: PublicSearchPageModuleCopy;
preferenceDiscovery: PublicSearchPageModuleCopy;
};
popularKeywords: PublicSearchPageKeyword[];
groups: PublicSearchPageGroup[];
/** Transitional fields accepted while an older API response is still cached. */
title?: string;
subtitle?: string;
};
export type PublicHomeModule = {
id: string;
label: string;
sortOrder: number;
isActive?: boolean;
templateType?: "explore" | "themes" | "deals" | "hotels" | "vehicles";
content?: {
items: Array<{
id: string;
title: string;
subtitle?: string;
description?: string;
image?: string | null;
targetType?: string | null;
targetValue?: string | null;
productIds?: string[];
chips?: string[];
isActive: boolean;
sortOrder: number;
}>;
};
};
export type ProductPricingTier = {
groupSize: number;
adultPrice: number;
child6PlusPrice: number;
childUnder6Price: number;
};
export type PublicProduct = {
id: string;
sourceId?: number | null;
title: string;
subtitle?: string | null;
destination?: { id: string; name: string } | null;
priceAmount?: number | null;
priceUnit?: string | null;
pricingTiers?: ProductPricingTier[] | null;
tags?: string[];
coverImage?: string | null;
summary?: string | null;
durationDays?: number | null;
durationNights?: number | null;
departureDates?: string[];
remainingSpots?: number | null;
recommendation?: string | null;
images?: Array<{ id?: string; url: string; alt?: string | null; sortOrder: number }>;
keyFacts?: ProductKeyFact[] | null;
contentBlocks?: ProductContentBlock[] | null;
detailSections?: ProductDetailSection[] | null;
status?: string;
sortWeight?: number;
};
export type ProductKeyFact = { label: string; value: string };
export type ProductContentBlock =
| { type: "title"; text: string }
| { type: "image"; url: string; alt?: string | null };
export type ProductDetailBlock =
| { type: "text"; text: string }
| { type: "image"; url: string; alt?: string | null };
export type ProductDetailSection = {
key: string;
label: string;
title?: string | null;
blocks: ProductDetailBlock[];
};
export type PublicSiteConfig = {
homeModules?: PublicHomeModule[];
heroSlides?: PublicHeroSlide[];
destinations?: PublicDestination[];
themes?: PublicTheme[];
ctaBanners?: PublicCta[];
destinationRecommendations?: { productIds: string[] };
searchPage?: PublicSearchPageConfig;
};
export type PublicLeadPayload = {
destination?: string;
phone: string;
contactName?: string;
wechat?: string;
travelDate?: string;
peopleCount?: number;
adultCount?: number;
childCount?: number;
roomType?: string;
plan?: string;
addOns?: string[];
budgetMin?: number;
budgetMax?: number;
note?: string;
sourcePage?: string;
sourceTheme?: string;
sourceProductId?: string;
};
async function request<T>(path: string, init?: RequestInit) {
const response = await fetch(`${API_BASE}${path}`, {
headers: {
"content-type": "application/json",
...init?.headers,
},
...init,
});
if (!response.ok) {
const message = await response.text();
throw new Error(message || `Request failed: ${response.status}`);
}
return response.json() as Promise<T>;
}
export async function fetchPublicContent() {
const [siteConfig, products] = await Promise.all([
request<PublicSiteConfig>("/api/public/site-config"),
request<{ items: PublicProduct[] }>("/api/public/products"),
]);
return {
siteConfig,
products: products.items,
};
}
export async function createPublicLead(payload: PublicLeadPayload) {
return request<{ id: string; status: string }>("/api/public/leads", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function createPublicBooking(payload: PublicLeadPayload) {
return request<{ id: string; status: string; requestType: "booking" }>("/api/public/bookings", {
method: "POST",
body: JSON.stringify(payload),
});
}