Files
lotteryFront/src/components/token-refresh-indicator.tsx
kang 587a6ad66c feat: 增强国际化支持与安全头配置
- 在 .env.example 中新增 i18next 相关配置项以支持多语言功能
- 在 next.config.ts 中添加安全头配置以支持 iframe 嵌入
- 更新 Providers 组件以引入 i18n 配置
- 在 PlayerAppShell 中集成 LanguageSwitcher 组件以实现语言切换功能
- 优化 HallWalletStrip 组件的网络状态管理逻辑
- 更新多个组件以支持国际化文本
2026-05-13 17:53:56 +08:00

120 lines
3.4 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { RefreshCw, AlertCircle } from "lucide-react";
import { useTokenRefresh } from "@/hooks/use-token-refresh";
import { Button } from "@/components/ui/button";
import { ERROR_COLORS } from "@/stores/error-store";
import { cn } from "@/lib/utils";
/**
* Token 续签状态指示器组件
*
* 当 Token 即将过期或正在刷新时显示提示
*/
export function TokenRefreshIndicator(): React.ReactElement | null {
const { isTokenExpiringSoon, getTokenRemainingTime, refreshToken } =
useTokenRefresh();
const [isRefreshing, setIsRefreshing] = useState(false);
const [showWarning, setShowWarning] = useState(false);
const [remainingSeconds, setRemainingSeconds] = useState<number | null>(null);
// 检测 Token 状态
useEffect(() => {
const checkToken = (): void => {
const remaining = getTokenRemainingTime();
const expiringSoon = isTokenExpiringSoon();
if (remaining > 0 && remaining < 120000) {
// 2 分钟内显示
setShowWarning(true);
setRemainingSeconds(Math.floor(remaining / 1000));
} else {
setShowWarning(false);
setRemainingSeconds(null);
}
};
checkToken();
const interval = setInterval(checkToken, 10000); // 每 10 秒检查
return () => clearInterval(interval);
}, [getTokenRemainingTime, isTokenExpiringSoon]);
// 手动刷新
const handleRefresh = async (): Promise<void> => {
setIsRefreshing(true);
try {
await refreshToken();
} finally {
setIsRefreshing(false);
}
};
if (!showWarning) {
return null;
}
const isCritical = remainingSeconds !== null && remainingSeconds < 60;
return (
<div
className={cn(
"fixed bottom-4 right-4 z-50 flex items-center gap-3 rounded-lg px-4 py-3 shadow-lg",
"animate-in slide-in-from-bottom-4 duration-300",
)}
style={{
backgroundColor: isCritical
? `${ERROR_COLORS.error}15`
: `${ERROR_COLORS.warning}15`,
border: `1px solid ${isCritical ? ERROR_COLORS.error : ERROR_COLORS.warning}`,
}}
>
<AlertCircle
className="size-5 shrink-0"
style={{
color: isCritical ? ERROR_COLORS.error : ERROR_COLORS.warning,
}}
/>
<div className="flex flex-col">
<span
className="text-sm font-medium"
style={{
color: isCritical ? ERROR_COLORS.error : ERROR_COLORS.warning,
}}
>
{isCritical ? "登录即将失效" : "登录即将过期"}
</span>
<span className="text-xs text-muted-foreground">
{remainingSeconds !== null && (
<>
{Math.floor(remainingSeconds / 60)}:
{String(remainingSeconds % 60).padStart(2, "0")} {" "}
</>
)}
...
</span>
</div>
<Button
size="sm"
variant="outline"
className={cn("ml-2 h-8 gap-1 border-current")}
style={{
color: isCritical ? ERROR_COLORS.error : ERROR_COLORS.warning,
borderColor: isCritical ? ERROR_COLORS.error : ERROR_COLORS.warning,
}}
onClick={handleRefresh}
disabled={isRefreshing}
>
<RefreshCw
className={cn("size-3.5", isRefreshing && "animate-spin")}
/>
</Button>
</div>
);
}