229 lines
6.6 KiB
TypeScript
229 lines
6.6 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
|
import { checkAuth, logout, setUnauthorizedHandler } from './api/client';
|
|
import { clearAllStoredSessionIds } from './utils/sessionStorage';
|
|
import { loadBlockedWords } from './utils/wordFilter';
|
|
import { AuthView } from './components/AuthView';
|
|
import { ChatView } from './components/ChatView';
|
|
import { MindSpaceView } from './components/MindSpaceView';
|
|
import { ChatProvider } from './context/ChatProvider';
|
|
import { PREVIEW_USER } from './dev/mindspacePreviewData';
|
|
import { MindSpaceRoute } from './routes/MindSpaceRoute';
|
|
import { FeedbackRoutes } from './routes/FeedbackRoute';
|
|
import { useProductAnalytics } from './analytics/productAnalytics';
|
|
import type { CapabilityMap, PortalUser } from './types';
|
|
|
|
function isMindSpacePreview() {
|
|
return (
|
|
import.meta.env.DEV &&
|
|
new URLSearchParams(window.location.search).get('preview') === 'mindspace'
|
|
);
|
|
}
|
|
|
|
function MindSpaceAuthGate({
|
|
user,
|
|
onLogout,
|
|
}: {
|
|
user: PortalUser | null;
|
|
onLogout: () => void;
|
|
}) {
|
|
const location = useLocation();
|
|
if (!user) {
|
|
const params = new URLSearchParams();
|
|
params.set('return_to', `${location.pathname}${location.search}`);
|
|
const utmSource = new URLSearchParams(location.search).get('utm_source');
|
|
if (utmSource) params.set('utm_source', utmSource);
|
|
return <Navigate to={`/?${params.toString()}`} replace />;
|
|
}
|
|
return <MindSpaceRoute user={user} onLogout={onLogout} />;
|
|
}
|
|
|
|
function AuthenticatedApp({
|
|
user,
|
|
capabilities,
|
|
grantedSkills,
|
|
onUserUpdate,
|
|
onLogout,
|
|
}: {
|
|
user: PortalUser | null;
|
|
capabilities?: CapabilityMap;
|
|
grantedSkills?: string[];
|
|
onUserUpdate: (user: PortalUser) => void;
|
|
onLogout: () => void;
|
|
}) {
|
|
const navigate = useNavigate();
|
|
|
|
const handleLogout = () => {
|
|
clearAllStoredSessionIds();
|
|
void logout().finally(() => {
|
|
onLogout();
|
|
navigate('/', { replace: true });
|
|
});
|
|
};
|
|
|
|
const chatElement = (
|
|
<ChatView
|
|
user={user}
|
|
capabilities={capabilities}
|
|
grantedSkills={grantedSkills}
|
|
onUserUpdate={onUserUpdate}
|
|
onOpenSpace={
|
|
user
|
|
? (target) => {
|
|
if (target?.pageId) {
|
|
navigate(`/space/page/${target.pageId}`);
|
|
return;
|
|
}
|
|
if (target?.categoryCode) {
|
|
navigate(`/space?category=${target.categoryCode}`);
|
|
return;
|
|
}
|
|
navigate('/space');
|
|
}
|
|
: undefined
|
|
}
|
|
onOpenPage={user ? (pageId) => navigate(`/space/page/${pageId}`) : undefined}
|
|
onOpenAdmin={
|
|
user?.role === 'admin'
|
|
? () => { window.location.href = import.meta.env.VITE_ADMIN_APP_URL ?? '/admin-app'; }
|
|
: undefined
|
|
}
|
|
onOpenFeedback={() => navigate('/feedback')}
|
|
onLogout={handleLogout}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<ChatProvider
|
|
user={user}
|
|
capabilities={capabilities}
|
|
grantedSkills={grantedSkills}
|
|
onUserUpdate={onUserUpdate}
|
|
>
|
|
<Routes>
|
|
<Route
|
|
path="/space/*"
|
|
element={<MindSpaceAuthGate user={user} onLogout={handleLogout} />}
|
|
/>
|
|
<Route
|
|
path="/feedback/*"
|
|
element={<FeedbackRoutes user={user} onLogout={handleLogout} />}
|
|
/>
|
|
<Route path="/" element={chatElement} />
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
</Routes>
|
|
</ChatProvider>
|
|
);
|
|
}
|
|
|
|
export function App() {
|
|
const mindSpacePreview = isMindSpacePreview();
|
|
const navigate = useNavigate();
|
|
const [authed, setAuthed] = useState<boolean | null>(null);
|
|
const [user, setUser] = useState<PortalUser | null>(null);
|
|
const [capabilities, setCapabilities] = useState<CapabilityMap | undefined>();
|
|
const [grantedSkills, setGrantedSkills] = useState<string[] | undefined>();
|
|
const [legacyMode, setLegacyMode] = useState(false);
|
|
const [authUnavailable, setAuthUnavailable] = useState<string | null>(null);
|
|
useProductAnalytics(user?.id);
|
|
|
|
useEffect(() => {
|
|
if (mindSpacePreview) return;
|
|
setUnauthorizedHandler(() => {
|
|
setAuthed(false);
|
|
setUser(null);
|
|
if (window.location.pathname !== '/') {
|
|
navigate('/', { replace: true });
|
|
}
|
|
});
|
|
void checkAuth().then((status) => {
|
|
if (status.mode === 'unavailable') {
|
|
setAuthUnavailable(status.message ?? '用户认证服务暂时不可用,请稍后刷新');
|
|
setAuthed(false);
|
|
return;
|
|
}
|
|
setAuthUnavailable(null);
|
|
setLegacyMode(status.mode === 'legacy');
|
|
setAuthed(status.authenticated);
|
|
setUser(status.user ?? null);
|
|
setCapabilities(status.capabilities);
|
|
setGrantedSkills(status.grantedSkills);
|
|
if (status.authenticated) void loadBlockedWords();
|
|
});
|
|
return () => setUnauthorizedHandler(null);
|
|
}, [mindSpacePreview, navigate]);
|
|
|
|
if (mindSpacePreview) {
|
|
return (
|
|
<ChatProvider user={PREVIEW_USER}>
|
|
<MindSpaceView
|
|
previewMode
|
|
user={PREVIEW_USER}
|
|
onBack={() => {
|
|
window.location.href = '/';
|
|
}}
|
|
onLogout={() => {
|
|
window.location.href = '/';
|
|
}}
|
|
/>
|
|
</ChatProvider>
|
|
);
|
|
}
|
|
|
|
if (authed === null) {
|
|
return <div className="app-loading">正在验证登录状态…</div>;
|
|
}
|
|
|
|
if (authUnavailable) {
|
|
return (
|
|
<div className="app-loading">
|
|
{authUnavailable}
|
|
<button type="button" className="login-link" onClick={() => window.location.reload()}>
|
|
重新加载
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!authed) {
|
|
return (
|
|
<AuthView
|
|
legacyMode={legacyMode}
|
|
onAuth={(nextUser, nextCapabilities, nextSkills) => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const returnTo = params.get('return_to') ?? null;
|
|
if (returnTo) {
|
|
try {
|
|
const url = new URL(returnTo, window.location.origin);
|
|
if (url.origin === window.location.origin) {
|
|
window.location.href = url.toString();
|
|
return;
|
|
}
|
|
} catch {
|
|
// ignore invalid URLs
|
|
}
|
|
}
|
|
setAuthed(true);
|
|
setUser(nextUser ?? null);
|
|
setCapabilities(nextCapabilities);
|
|
setGrantedSkills(nextSkills);
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AuthenticatedApp
|
|
user={user}
|
|
capabilities={capabilities}
|
|
grantedSkills={grantedSkills}
|
|
onUserUpdate={setUser}
|
|
onLogout={() => {
|
|
clearAllStoredSessionIds();
|
|
setAuthed(false);
|
|
setUser(null);
|
|
}}
|
|
/>
|
|
);
|
|
}
|