feat: 接入玩家入口与API代理

- 新增 /api 重写代理,支持 LOTTERY_API_PROXY_TARGET 配置
- 玩家首页切换为 EntryGate,并移除 layout 对 PlayerAppShell 的包裹
- 请求层拆分语言头与玩家鉴权注入逻辑,引入 zustand 依赖
- 允许提交 .env.example 供本地配置参考
This commit is contained in:
2026-05-09 10:17:39 +08:00
parent 7bed43ac96
commit 14c297fe1a
16 changed files with 750 additions and 81 deletions

View File

@@ -1,52 +1,17 @@
import axios, {
AxiosHeaders,
isAxiosError,
type AxiosRequestConfig,
type AxiosResponse,
} from "axios";
import { withPlayerAuthHeader } from "@/lib/lottery-auth";
import { withLotteryLocaleHeaders } from "@/lib/lottery-locale";
import {
LotteryApiBizError,
LotteryApiEnvelopeError,
} from "@/types/api/errors";
import { isApiEnvelope } from "@/types/api/envelope";
export function setLotteryRequestLocale(locale: string | null): void {
if (locale === null) {
overrideLocale = null;
return;
}
const p = locale.trim().toLowerCase().split("-")[0] ?? "";
overrideLocale =
p === "zh" || p === "en" || p === "ne" ? (p as "zh" | "en" | "ne") : null;
}
let overrideLocale: "zh" | "en" | "ne" | null = null;
function requestLocale(): "zh" | "en" | "ne" {
if (overrideLocale) {
return overrideLocale;
}
if (typeof document !== "undefined") {
const tag = document.documentElement.lang.trim().toLowerCase();
const primary = tag.split("-")[0] ?? tag;
if (primary === "zh" || primary === "en" || primary === "ne") {
return primary;
}
}
return "en";
}
function acceptLanguage(loc: ReturnType<typeof requestLocale>): string {
if (loc === "zh") {
return "zh-CN,zh;q=0.9,en;q=0.8";
}
if (loc === "ne") {
return "ne,ne-NP;q=0.9,en;q=0.8";
}
return "en-US,en;q=0.9";
}
const baseURL = process.env.NEXT_PUBLIC_LOTTERY_API_BASE_URL?.trim();
/**
@@ -58,21 +23,6 @@ export const lotteryHttp = axios.create({
headers: { Accept: "application/json" },
});
function mergeLocaleHeaders(
config: AxiosRequestConfig,
): AxiosRequestConfig {
const loc = requestLocale();
const merged: AxiosRequestConfig = { ...config };
// Axios 的 RequestConfig.headers 类型比 concat 能接受的头对象更窄
const headers = AxiosHeaders.concat(
merged.headers as Parameters<typeof AxiosHeaders.concat>[0],
);
headers.set("X-Locale", loc);
headers.set("Accept-Language", acceptLanguage(loc));
merged.headers = headers;
return merged;
}
/**
* 对 **payload**(通常是 `response.data`)校验信封并成功时返回 `data`
* `code !== 0` 抛 {@link LotteryApiBizError}。
@@ -93,11 +43,11 @@ export function unwrapResponse<T>(res: AxiosResponse<unknown>): T {
}
/**
* **第二层**自动带语言头,用 `lotteryHttp` 发请求,再 `unwrapResponse`。
* **第二层**补齐玩家鉴权与语言头,用 `lotteryHttp` 发请求,再 `unwrapResponse`。
* 是否提示用户由各页面/特性自己 `catch` 决定。
*/
export async function request<T>(config: AxiosRequestConfig): Promise<T> {
const merged = mergeLocaleHeaders(config);
const merged = withPlayerAuthHeader(withLotteryLocaleHeaders(config));
try {
const res = await lotteryHttp.request<unknown>(merged);
return unwrapResponse<T>(res);
@@ -139,4 +89,4 @@ export const lotteryRequest = {
data?: unknown,
config?: Omit<AxiosRequestConfig, "url" | "method" | "data">,
) => request<T>({ ...config, url, method: "PATCH", data }),
};
};