feat(env, config, i18n): 增强环境配置与多语言支持

更新 .env.example,提供更清晰的 API 配置说明与本地开发环境配置指引。
修改 next.config.ts:支持动态解析允许的开发环境来源(origins),提升配置灵活性。
重构 admin-language-switcher:优化语言切换同步机制,确保语言变更能够及时生效。
优化英文、尼泊尔语与中文语言包中的错误提示文案,进一步明确 API 配置要求。
精简 admin-http.ts:将 API Base URL 校验逻辑抽离至独立模块并统一导出,提升代码可维护性。
This commit is contained in:
2026-05-29 09:17:37 +08:00
parent 0bfcf6c59c
commit 27727f6371
10 changed files with 76 additions and 19 deletions

View File

@@ -1,9 +1,32 @@
# =============================================================================
# 管理端本地配置示例:复制为 .env.local 后按需修改
# =============================================================================
# 三端联调速查lotterLaravel + lotteryadmin + lotteryfront
# - Laravel APIphp artisan serve → 默认 http://127.0.0.1:8000
# - 管理端npm run dev → http://localhost:3801浏览器请求 /api → 反代到 API_BASE_URL
# - 玩家端npm run dev → http://localhost:3800
# - Laravel .envCORS_ALLOWED_ORIGINS、SANCTUM_STATEFUL_DOMAINS 需包含上述前端 origin
# - ReverbLaravel REVERB_* 与玩家端 NEXT_PUBLIC_REVERB_* 一致(管理端一般不需 Echo
# -----------------------------------------------------------------------------
# Laravel APINext rewrites/api/* → ${API_BASE_URL}/api/*
# -----------------------------------------------------------------------------
# 手动切换环境:保留一个生效,另一个注释掉
# 测试
API_BASE_URL=http://127.0.0.1:8000
# 线上
# API_BASE_URL=https://api.your-production-domain.com
# -----------------------------------------------------------------------------
# 可选:直连 Laravel不经 Next 反代);一般本地开发用 API_BASE_URL 即可
# -----------------------------------------------------------------------------
# NEXT_PUBLIC_LOTTERY_API_BASE_URL=http://127.0.0.1:8000
# 显式关闭「已配置 API」检测极少用
# NEXT_PUBLIC_LOTTERY_API_PROXY_DISABLED=true
# -----------------------------------------------------------------------------
# Next 开发:局域网用 IP 访问时,允许该 host逗号分隔无协议
# 示例:手机访问 http://192.168.0.101:3801 时设置
# -----------------------------------------------------------------------------
# ALLOWED_DEV_ORIGINS=192.168.0.101

View File

@@ -1,10 +1,13 @@
import type { NextConfig } from "next";
import { parseAllowedDevOrigins } from "./src/lib/next-dev-origins";
const apiBaseUrl = process.env.API_BASE_URL?.trim() || "http://127.0.0.1:8000";
const allowedDevOrigins = parseAllowedDevOrigins(process.env.ALLOWED_DEV_ORIGINS);
const nextConfig: NextConfig = {
/* config options here */
allowedDevOrigins: ["192.168.0.101"],
...(allowedDevOrigins.length > 0 ? { allowedDevOrigins } : {}),
reactCompiler: true,
async rewrites() {
return [

View File

@@ -1,6 +1,5 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Script from "next/script";
import { Providers } from "@/components/providers";
import "./globals.css";
@@ -37,12 +36,10 @@ export default function RootLayout({
suppressHydrationWarning
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<head>
<Script id="admin-locale-bootstrap" strategy="beforeInteractive">
{ADMIN_LOCALE_BOOTSTRAP}
</Script>
</head>
<body className="flex min-h-full flex-col">
<script
dangerouslySetInnerHTML={{ __html: ADMIN_LOCALE_BOOTSTRAP }}
/>
<Providers>{children}</Providers>
</body>
</html>

View File

@@ -30,15 +30,21 @@ const LOCALE_FLAGS: Record<AdminApiLocale, string> = {
export function AdminLanguageSwitcher() {
const { i18n, t } = useTranslation("common");
const [locale, setLocale] = useState<AdminApiLocale>(() =>
typeof document !== "undefined" ? getAdminRequestLocale() : "en",
);
// Match SSR: do not read document/localStorage until after mount.
const [locale, setLocale] = useState<AdminApiLocale>("en");
useEffect(() => {
queueMicrotask(() => {
const syncLocale = () => {
setLocale(getAdminRequestLocale());
});
}, []);
};
syncLocale();
i18n.on("languageChanged", syncLocale);
return () => {
i18n.off("languageChanged", syncLocale);
};
}, [i18n]);
async function onSelectLocale(next: AdminApiLocale) {
applyAdminUiLocale(next);

View File

@@ -16,7 +16,7 @@
"submit": "Log in",
"submitting": "Signing in…",
"captchaLoadFailed": "Failed to load captcha. Check the API or network.",
"apiBaseMissingToast": "NEXT_PUBLIC_LOTTERY_API_BASE_URL is not configured",
"apiBaseMissingToast": "API not configured: set API_BASE_URL in admin .env.local (Next proxy) or NEXT_PUBLIC_LOTTERY_API_BASE_URL (direct)",
"captchaRequired": "Refresh the captcha first",
"welcome": "Welcome, {{name}}",
"networkFailed": "Network request failed",

View File

@@ -16,7 +16,7 @@
"submit": "लगइन",
"submitting": "लगइन हुँदैछ…",
"captchaLoadFailed": "क्याप्चा लोड गर्न सकिएन। API वा नेटवर्क जाँच गर्नुहोस्।",
"apiBaseMissingToast": "NEXT_PUBLIC_LOTTERY_API_BASE_URL सेट गरिएको छैन",
"apiBaseMissingToast": "API कन्फिग गरिएको छैन: admin .env.local मा API_BASE_URL (Next proxy) वा NEXT_PUBLIC_LOTTERY_API_BASE_URL (direct) सेट गर्नुहोस्",
"captchaRequired": "पहिले क्याप्चा रिफ्रेस गर्नुहोस्",
"welcome": "स्वागत छ, {{name}}",
"networkFailed": "नेटवर्क अनुरोध असफल भयो",

View File

@@ -16,7 +16,7 @@
"submit": "登录",
"submitting": "登录中…",
"captchaLoadFailed": "无法获取验证码,请检查接口或网络",
"apiBaseMissingToast": "未配置 NEXT_PUBLIC_LOTTERY_API_BASE_URL",
"apiBaseMissingToast": "未配置 API请在管理端 .env.local 设置 API_BASE_URLNext 反代),或设置 NEXT_PUBLIC_LOTTERY_API_BASE_URL(直连)",
"captchaRequired": "请先刷新验证码",
"welcome": "欢迎,{{name}}",
"networkFailed": "网络请求失败",

View File

@@ -9,9 +9,7 @@ import { withAdminLocaleHeaders } from "@/lib/admin-locale";
import { LotteryApiBizError, LotteryApiEnvelopeError } from "@/types/api/errors";
import { isApiEnvelope } from "@/types/api/envelope";
export function hasLotteryAdminApiBaseUrl(): boolean {
return true;
}
export { hasLotteryAdminApiBaseUrl } from "@/lib/lottery-api-env";
export const adminHttp = axios.create({
// API 路径统一由调用方传 `/api/v1/...`,避免与前缀重复拼接成 `/api/api/v1/...`。

View File

@@ -0,0 +1,19 @@
/**
* 管理端 API 连接方式:
* - 默认:浏览器请求同源 `/api/*`,由 next.config `rewrites` 转发到 Laravel需配置服务端 `API_BASE_URL`)。
* - 可选:设置 `NEXT_PUBLIC_LOTTERY_API_BASE_URL` 直连后端(不经 Next 反代)。
*/
export function hasLotteryAdminApiBaseUrl(): boolean {
const direct = process.env.NEXT_PUBLIC_LOTTERY_API_BASE_URL?.trim();
if (direct !== undefined && direct !== "") {
return true;
}
return process.env.NEXT_PUBLIC_LOTTERY_API_PROXY_DISABLED !== "true";
}
/** 直连模式下的 Laravel API 根(含 `/api` 前缀由调用方 path 决定) */
export function lotteryAdminApiDirectOrigin(): string | null {
const direct = process.env.NEXT_PUBLIC_LOTTERY_API_BASE_URL?.trim();
return direct !== undefined && direct !== "" ? direct.replace(/\/$/, "") : null;
}

View File

@@ -0,0 +1,11 @@
/** 解析 `ALLOWED_DEV_ORIGINS`(逗号分隔),供 next.config `allowedDevOrigins` 使用 */
export function parseAllowedDevOrigins(envValue: string | undefined): string[] {
if (envValue === undefined || envValue.trim() === "") {
return [];
}
return envValue
.split(",")
.map((origin) => origin.trim())
.filter((origin) => origin !== "");
}