此提交完成了全项目的国际化适配: 1. 新增多语言翻译文件与基础配置 2. 替换所有硬编码文本为i18n调用 3. 优化语言切换与文档语言同步逻辑 4. 重构部分业务逻辑以支持动态翻译 5. 移除过时代码与硬编码配置
124 lines
3.5 KiB
TypeScript
124 lines
3.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { RefreshCw, AlertCircle } from "lucide-react";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
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 { t } = useTranslation("player");
|
|
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();
|
|
|
|
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 ? t("token.critical") : t("token.warning")}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
{remainingSeconds !== null && (
|
|
<>
|
|
{t("token.remaining", {
|
|
time: `${Math.floor(remainingSeconds / 60)}:${String(
|
|
remainingSeconds % 60,
|
|
).padStart(2, "0")}`,
|
|
})}{" "}
|
|
</>
|
|
)}
|
|
{t("token.autoRenewing")}
|
|
</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")}
|
|
/>
|
|
{t("token.renewNow")}
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|