feat: 增强国际化支持与安全头配置
- 在 .env.example 中新增 i18next 相关配置项以支持多语言功能 - 在 next.config.ts 中添加安全头配置以支持 iframe 嵌入 - 更新 Providers 组件以引入 i18n 配置 - 在 PlayerAppShell 中集成 LanguageSwitcher 组件以实现语言切换功能 - 优化 HallWalletStrip 组件的网络状态管理逻辑 - 更新多个组件以支持国际化文本
This commit is contained in:
220
src/components/iframe-bridge.tsx
Normal file
220
src/components/iframe-bridge.tsx
Normal 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 || "",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user