feat: 增强国际化支持与安全头配置

- 在 .env.example 中新增 i18next 相关配置项以支持多语言功能
- 在 next.config.ts 中添加安全头配置以支持 iframe 嵌入
- 更新 Providers 组件以引入 i18n 配置
- 在 PlayerAppShell 中集成 LanguageSwitcher 组件以实现语言切换功能
- 优化 HallWalletStrip 组件的网络状态管理逻辑
- 更新多个组件以支持国际化文本
This commit is contained in:
2026-05-13 17:53:56 +08:00
parent c8f8f90515
commit 587a6ad66c
32 changed files with 2126 additions and 436 deletions

View File

@@ -0,0 +1,220 @@
"use client";
import { useEffect, useCallback, type ReactNode } from "react";
import { usePlayerSessionStore } from "@/stores/player-session-store";
import { setPlayerBearerToken } from "@/lib/lottery-auth";
/**
* iframe 通信桥接组件
*
* 功能:
* 1. 监听父窗口(主站)通过 postMessage 发送的 Token
* 2. 向父窗口发送心跳和状态通知
* 3. 支持在主站 iframe 内嵌入时的双向通信
*/
export function IframeBridge({ children }: { children: ReactNode }): ReactNode {
const setBearerToken = usePlayerSessionStore((state) => state.setBearerToken);
/**
* 向父窗口发送消息
*/
const sendToParent = useCallback(
(type: string, payload?: Record<string, unknown>): void => {
if (typeof window === "undefined" || window.parent === window) return;
window.parent.postMessage(
{
type: `LOTTERY_${type}`,
payload,
timestamp: Date.now(),
source: "lottery-iframe",
},
"*", // 生产环境应指定具体域名
);
},
[],
);
/**
* 通知父窗口:已准备就绪
*/
const notifyReady = useCallback((): void => {
sendToParent("READY", {
url: window.location.href,
userAgent: navigator.userAgent,
});
}, [sendToParent]);
/**
* 通知父窗口:需要新 Token
*/
const notifyTokenNeeded = useCallback((): void => {
sendToParent("TOKEN_NEEDED", {
reason: "token_expired",
});
}, [sendToParent]);
/**
* 通知父窗口Token 刷新成功
*/
const notifyTokenRefreshed = useCallback((): void => {
sendToParent("TOKEN_REFRESHED");
}, [sendToParent]);
/**
* 通知父窗口:发生错误
*/
const notifyError = useCallback(
(error: string): void => {
sendToParent("ERROR", { error });
},
[sendToParent],
);
/**
* 监听父窗口消息
*/
useEffect(() => {
if (typeof window === "undefined") return;
// 检查是否在 iframe 内
const isInIframe = window.self !== window.top;
if (!isInIframe) {
console.log("[IframeBridge] Not in iframe, skipping bridge setup");
return;
}
console.log("[IframeBridge] Setting up iframe communication");
const handleMessage = (event: MessageEvent): void => {
// 安全:验证来源域名
const allowedOrigins = [
process.env.NEXT_PUBLIC_MAIN_SITE_URL,
process.env.NEXT_PUBLIC_PARENT_ORIGIN,
"http://localhost:3000",
"http://127.0.0.1:3000",
].filter(Boolean);
if (
allowedOrigins.length > 0 &&
!allowedOrigins.includes(event.origin)
) {
console.warn("[IframeBridge] Rejected message from:", event.origin);
return;
}
const { data } = event;
if (!data || typeof data !== "object") return;
console.log("[IframeBridge] Received message:", data.type);
switch (data.type) {
// 主站发送初始化 Token
case "MAIN_INIT_TOKEN":
if (data.token) {
console.log("[IframeBridge] Received initial token");
setBearerToken(data.token);
setPlayerBearerToken(data.token);
notifyReady();
}
break;
// 主站刷新 Token
case "MAIN_REFRESH_TOKEN":
if (data.token) {
console.log("[IframeBridge] Received refreshed token");
setBearerToken(data.token);
setPlayerBearerToken(data.token);
notifyTokenRefreshed();
}
break;
// 主站通知 Token 即将过期
case "MAIN_TOKEN_EXPIRING":
console.log("[IframeBridge] Token expiring soon");
// 可以显示提示或自动刷新
break;
// 主站请求当前状态
case "MAIN_REQUEST_STATUS":
sendToParent("STATUS_RESPONSE", {
isReady: true,
currentPath: window.location.pathname,
});
break;
// 主站导航请求
case "MAIN_NAVIGATE":
if (data.path && typeof data.path === "string") {
window.history.pushState({}, "", data.path);
}
break;
default:
break;
}
};
window.addEventListener("message", handleMessage);
// 发送就绪通知
notifyReady();
// 定期发送心跳
const heartbeat = setInterval(() => {
sendToParent("HEARTBEAT", {
timestamp: Date.now(),
});
}, 30000); // 每 30 秒
return () => {
window.removeEventListener("message", handleMessage);
clearInterval(heartbeat);
};
}, [notifyReady, sendToParent, setBearerToken]);
// 暴露全局方法供调试
useEffect(() => {
if (typeof window === "undefined") return;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as unknown as Record<string, unknown>).lotteryIframeBridge = {
notifyReady,
notifyTokenNeeded,
notifyError,
sendToParent,
};
}, [notifyError, notifyReady, notifyTokenNeeded, sendToParent]);
return children;
}
/**
* 检测当前是否在 iframe 内
*/
export function isInIframe(): boolean {
if (typeof window === "undefined") return false;
try {
return window.self !== window.top;
} catch {
return true; // 跨域时无法访问 window.top说明在 iframe 内
}
}
/**
* 获取父窗口信息
*/
export function getParentInfo(): {
isInIframe: boolean;
referrer: string;
} {
if (typeof window === "undefined") {
return { isInIframe: false, referrer: "" };
}
return {
isInIframe: isInIframe(),
referrer: document.referrer || "",
};
}