- classifyVehicleType now parses dic_type.dic_name (e.g. "4.5吨冷链车") instead of raw model code - Remove overly strict completionRate >= 0.8 filter for hopeless candidates - Use vehicle's yearTarget as fallback when inventory has no assessment target - Filter out suggestions with no candidates (not actionable) - estimatedGain counts rescue_hopeless suggestions as potential gains Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
112 lines
3.5 KiB
TypeScript
112 lines
3.5 KiB
TypeScript
import { useState, useEffect, useRef, type ReactNode } from 'react';
|
||
import { AuthContext, type AuthState } from './useAuth';
|
||
import { setTokenGetter } from './api-client';
|
||
|
||
const AUTH_API = '/api/auth';
|
||
|
||
export default function AuthProvider({ children }: { children: ReactNode }) {
|
||
const [state, setState] = useState<AuthState>({
|
||
isLoading: true,
|
||
isAuthenticated: false,
|
||
user: null,
|
||
error: null,
|
||
});
|
||
const tokenRef = useRef<string | null>(null);
|
||
const authStarted = useRef(false);
|
||
|
||
useEffect(() => {
|
||
// 设置全局 token getter
|
||
setTokenGetter(() => tokenRef.current);
|
||
|
||
// 防止 StrictMode 双重调用(jumpToken 一次性使用)
|
||
if (authStarted.current) return;
|
||
authStarted.current = true;
|
||
|
||
// 监听 401 事件
|
||
const onUnauthorized = () => {
|
||
tokenRef.current = null;
|
||
sessionStorage.removeItem('bi_jwt');
|
||
setState({ isLoading: false, isAuthenticated: false, user: null, error: '会话已过期' });
|
||
};
|
||
window.addEventListener('auth:unauthorized', onUnauthorized);
|
||
|
||
authenticate();
|
||
|
||
return () => window.removeEventListener('auth:unauthorized', onUnauthorized);
|
||
}, []);
|
||
|
||
async function authenticate() {
|
||
// 1. 检查 sessionStorage 中是否有 JWT
|
||
const savedToken = sessionStorage.getItem('bi_jwt');
|
||
if (savedToken) {
|
||
tokenRef.current = savedToken;
|
||
// 验证 token 是否仍然有效(尝试请求 health)
|
||
try {
|
||
const res = await fetch('/api/health', {
|
||
headers: { Authorization: `Bearer ${savedToken}` },
|
||
});
|
||
if (res.ok) {
|
||
const savedUser = sessionStorage.getItem('bi_user');
|
||
setState({
|
||
isLoading: false,
|
||
isAuthenticated: true,
|
||
user: savedUser ? JSON.parse(savedUser) : null,
|
||
error: null,
|
||
});
|
||
return;
|
||
}
|
||
} catch { /* token 无效,继续流程 */ }
|
||
sessionStorage.removeItem('bi_jwt');
|
||
sessionStorage.removeItem('bi_user');
|
||
}
|
||
|
||
// 2. 从 URL 提取 jumpToken
|
||
const params = new URLSearchParams(window.location.search);
|
||
const jumpToken = params.get('jumpToken');
|
||
|
||
if (!jumpToken) {
|
||
// 临时:无 token 时直接放行
|
||
setState({ isLoading: false, isAuthenticated: true, user: null, error: null });
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 3. 一步完成:jumpToken → 用户信息 + JWT
|
||
const res = await fetch(`${AUTH_API}/exchange?jumpToken=${encodeURIComponent(jumpToken)}`);
|
||
const data = await res.json();
|
||
|
||
if (!res.ok || !data.token) {
|
||
setState({ isLoading: false, isAuthenticated: false, user: null, error: data.message || '跳转令牌无效或已过期' });
|
||
return;
|
||
}
|
||
|
||
// 4. 存储 JWT
|
||
tokenRef.current = data.token;
|
||
sessionStorage.setItem('bi_jwt', data.token);
|
||
sessionStorage.setItem('bi_user', JSON.stringify(data.user));
|
||
|
||
// 6. 清除 URL 中的 jumpToken
|
||
params.delete('jumpToken');
|
||
const cleanUrl = params.toString()
|
||
? `${window.location.pathname}?${params.toString()}${window.location.hash}`
|
||
: `${window.location.pathname}${window.location.hash}`;
|
||
window.history.replaceState({}, '', cleanUrl);
|
||
|
||
setState({
|
||
isLoading: false,
|
||
isAuthenticated: true,
|
||
user: data.user,
|
||
error: null,
|
||
});
|
||
} catch (e) {
|
||
setState({ isLoading: false, isAuthenticated: false, user: null, error: '认证过程出错' });
|
||
}
|
||
}
|
||
|
||
return (
|
||
<AuthContext.Provider value={state}>
|
||
{children}
|
||
</AuthContext.Provider>
|
||
);
|
||
}
|