/** * 类型化路由常量 * * 关键设计: * - `as const` 让每个值都是字符串字面量类型,可用于 `PageProps<'/literal'>`。 * - 不导出 Client-only 模块(`use-auth-gate` 走独立路径导入),保证 Server bundle 不被污染。 * - 动态查询路由通过 `ROUTE_BUILDERS` 函数生成,避免手拼字符串。 */ import type { PayChannel } from "@/data/schemas/payment"; import type { AppSubscriptionReturnTo } from "@/router/navigation-types"; import type { CharacterProfile } from "@/data/constants/character"; import { PAYMENT_ANALYTICS_ENTRY_PARAM, PAYMENT_ANALYTICS_REASON_PARAM, type PaymentAnalyticsContext, } from "@/lib/analytics/payment_analytics_context"; /** 静态路由字面量 */ export const ROUTES = { root: "/", splash: "/splash", chat: "/chat", privateZone: "/private-zone", tip: "/tip", externalEntry: "/external-entry", auth: "/auth", profile: "/profile", feedback: "/feedback", subscription: "/subscription", coinsRules: "/coins-rules", } as const; export type StaticRoute = (typeof ROUTES)[keyof typeof ROUTES]; export interface CharacterRoutes { readonly splash: string; readonly chat: string; readonly privateZone: string; readonly tip: string; } export function getCharacterRoutes( characterSlug: CharacterProfile["slug"], ): CharacterRoutes { const root = `/characters/${encodeURIComponent(characterSlug)}`; return { splash: `${root}/splash`, chat: `${root}/chat`, privateZone: `${root}/private-zone`, tip: `${root}/tip`, }; } export type RouteSearchParams = Record< string, string | readonly string[] | undefined >; export function appendRouteSearchParams( pathname: string, searchParams: RouteSearchParams, ): string { const params = new URLSearchParams(); for (const [key, value] of Object.entries(searchParams)) { if (typeof value === "string") { params.set(key, value); continue; } value?.forEach((item) => params.append(key, item)); } const query = params.toString(); return query ? `${pathname}?${query}` : pathname; } /** 动态查询路由构造器 */ export const ROUTE_BUILDERS = { subscription: ( type: "vip" | "topup", options: { payChannel?: PayChannel; returnTo?: Exclude; sourceCharacterSlug?: string; analytics?: PaymentAnalyticsContext; planId?: string; commercialOfferId?: string; } = {}, ): `/subscription?${string}` => { const params = new URLSearchParams({ type }); if (options.payChannel) params.set("payChannel", options.payChannel); if (options.returnTo) params.set("returnTo", options.returnTo); if (options.sourceCharacterSlug) { params.set("character", options.sourceCharacterSlug); } if (options.planId) params.set("planId", options.planId); if (options.commercialOfferId) { params.set("commercialOfferId", options.commercialOfferId); } if (options.analytics) { params.set( PAYMENT_ANALYTICS_ENTRY_PARAM, options.analytics.entryPoint, ); params.set( PAYMENT_ANALYTICS_REASON_PARAM, options.analytics.triggerReason, ); } return `${ROUTES.subscription}?${params.toString()}` as const; }, authWithRedirect: (redirectTo: string): `/auth?redirect=${string}` => `${ROUTES.auth}?redirect=${encodeURIComponent(redirectTo)}` as const, } as const; /** 所有静态路由,供 `proxy.ts` matcher 与未来 sitemap 使用 */ export const ALL_STATIC_ROUTES: readonly StaticRoute[] = [ ROUTES.splash, ROUTES.chat, ROUTES.privateZone, ROUTES.tip, ROUTES.externalEntry, ROUTES.auth, ROUTES.profile, ROUTES.feedback, ROUTES.coinsRules, ] as const; /** 联合路由类型 */ export type Route = StaticRoute;