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:
23
.env.example
23
.env.example
@@ -1,9 +1,32 @@
|
||||
# =============================================================================
|
||||
# 管理端本地配置示例:复制为 .env.local 后按需修改
|
||||
# =============================================================================
|
||||
# 三端联调速查(lotterLaravel + lotteryadmin + lotteryfront):
|
||||
# - Laravel API:php 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 .env:CORS_ALLOWED_ORIGINS、SANCTUM_STATEFUL_DOMAINS 需包含上述前端 origin
|
||||
# - Reverb:Laravel REVERB_* 与玩家端 NEXT_PUBLIC_REVERB_* 一致(管理端一般不需 Echo)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Laravel API(Next 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
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "नेटवर्क अनुरोध असफल भयो",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"submit": "登录",
|
||||
"submitting": "登录中…",
|
||||
"captchaLoadFailed": "无法获取验证码,请检查接口或网络",
|
||||
"apiBaseMissingToast": "未配置 NEXT_PUBLIC_LOTTERY_API_BASE_URL",
|
||||
"apiBaseMissingToast": "未配置 API:请在管理端 .env.local 设置 API_BASE_URL(Next 反代),或设置 NEXT_PUBLIC_LOTTERY_API_BASE_URL(直连)",
|
||||
"captchaRequired": "请先刷新验证码",
|
||||
"welcome": "欢迎,{{name}}",
|
||||
"networkFailed": "网络请求失败",
|
||||
|
||||
@@ -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/...`。
|
||||
|
||||
19
src/lib/lottery-api-env.ts
Normal file
19
src/lib/lottery-api-env.ts
Normal 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;
|
||||
}
|
||||
11
src/lib/next-dev-origins.ts
Normal file
11
src/lib/next-dev-origins.ts
Normal 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 !== "");
|
||||
}
|
||||
Reference in New Issue
Block a user