Files
WanderQ-MiniAPP2.0/src/api.ts
2026-07-24 14:55:13 +08:00

281 lines
7.0 KiB
TypeScript

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 PublicUserProfile = {
id: string;
nickname?: string | null;
avatarUrl?: string | null;
phoneMasked: string | null;
hasPhone: boolean;
status: "active" | "disabled" | "anonymized";
source: string;
firstSeenAt: string;
lastLoginAt: string;
createdAt: string;
leads?: number;
favorites?: number;
history?: number;
};
export type PublicActivity = {
favorites: Array<PublicProduct & { savedAt: string }>;
history: Array<PublicProduct & { viewedAt: string; viewCount: number }>;
};
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}`, {
...init,
credentials: "include",
headers: {
"content-type": "application/json",
...init?.headers,
},
});
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 fetchCurrentUser() {
return request<PublicUserProfile>("/api/public/users/me");
}
export async function exchangeH5Ticket(ticket: string) {
return request<{ user: PublicUserProfile }>("/api/public/auth/h5-exchange", {
method: "POST",
body: JSON.stringify({ ticket }),
});
}
export async function syncPublicActivity(favoriteProductIds: string[], historyProductIds: string[]) {
return request<PublicActivity>("/api/public/users/me/activity/sync", {
method: "POST",
body: JSON.stringify({ favoriteProductIds, historyProductIds }),
});
}
export async function savePublicFavorite(productId: string, saved: boolean) {
const path = "/api/public/users/me/favorites/" + encodeURIComponent(productId);
return request<{ saved: boolean }>(path, { method: saved ? "POST" : "DELETE" });
}
export async function recordPublicHistory(productId: string) {
return request<{ saved: boolean }>("/api/public/users/me/history", {
method: "POST",
body: JSON.stringify({ productId }),
});
}
export async function clearPublicHistory() {
return request<{ cleared: boolean }>("/api/public/users/me/history", { method: "DELETE" });
}
export async function logoutPublicSession() {
return request<{ loggedOut: boolean }>("/api/public/auth/logout", {
method: "POST",
body: JSON.stringify({}),
});
}
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),
});
}