- 计费双单价:含视频输入28元/百万tokens,不含视频输入46元/百万tokens - QuotaConfig 加 base_token_price_video 字段,系统设置页两个并排输入框 - 预估费用和实际结算按参考素材类型自动选择单价 - Token 刷新加锁:同页面内并发 401 共用一次 refresh 请求 - 关闭 BLACKLIST_AFTER_ROTATION:防止快速刷新导致误登出 - ProtectedRoute 容错:请求中断时自动重试,不误跳转 - CSV 导出上限从 100 提升到 10000 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
84 lines
2.3 KiB
TypeScript
84 lines
2.3 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import { Navigate } from 'react-router-dom';
|
|
import { useAuthStore } from '../store/auth';
|
|
|
|
interface Props {
|
|
children: React.ReactNode;
|
|
requireAdmin?: boolean;
|
|
requireTeamAdmin?: boolean;
|
|
requireTeamMember?: boolean;
|
|
}
|
|
|
|
export function ProtectedRoute({ children, requireAdmin, requireTeamAdmin, requireTeamMember }: Props) {
|
|
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
|
const isLoading = useAuthStore((s) => s.isLoading);
|
|
const user = useAuthStore((s) => s.user);
|
|
const mustChangePassword = useAuthStore((s) => s.mustChangePassword);
|
|
const fetchUserInfo = useAuthStore((s) => s.fetchUserInfo);
|
|
const retrying = useRef(false);
|
|
|
|
// If we have a token but user info hasn't loaded, keep retrying
|
|
useEffect(() => {
|
|
if (!isAuthenticated || user || isLoading) return;
|
|
if (retrying.current) return;
|
|
retrying.current = true;
|
|
|
|
let cancelled = false;
|
|
const retry = async () => {
|
|
let delay = 500;
|
|
while (!cancelled) {
|
|
try {
|
|
await fetchUserInfo();
|
|
break; // success
|
|
} catch {
|
|
await new Promise(r => setTimeout(r, delay));
|
|
delay = Math.min(delay * 2, 3000);
|
|
}
|
|
}
|
|
retrying.current = false;
|
|
};
|
|
retry();
|
|
|
|
return () => { cancelled = true; };
|
|
}, [isAuthenticated, user, isLoading, fetchUserInfo]);
|
|
|
|
if (isLoading || (isAuthenticated && !user)) {
|
|
return (
|
|
<div style={{
|
|
width: '100%',
|
|
height: '100%',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
background: 'var(--color-bg-page)',
|
|
color: 'var(--color-text-secondary)',
|
|
}}>
|
|
加载中...
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!isAuthenticated) {
|
|
return <Navigate to="/login" replace />;
|
|
}
|
|
|
|
if (mustChangePassword) {
|
|
return <Navigate to="/" replace />;
|
|
}
|
|
|
|
if (requireAdmin && user?.role !== 'super_admin') {
|
|
return <Navigate to="/app" replace />;
|
|
}
|
|
|
|
if (requireTeamAdmin && user?.role !== 'team_admin') {
|
|
return <Navigate to="/app" replace />;
|
|
}
|
|
|
|
// requireTeamMember: must have a team (team_admin or member)
|
|
if (requireTeamMember && user?.role === 'super_admin') {
|
|
return <Navigate to="/admin/dashboard" replace />;
|
|
}
|
|
|
|
return <>{children}</>;
|
|
}
|