Files
memind_adm/src/App.tsx
T

219 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { checkAuth, login, logout, setUnauthorizedHandler } from './api/client';
import { AdminLayout } from './admin/AdminLayout';
import { BillingPage } from './admin/pages/BillingPage';
import { CapabilitiesPage } from './admin/pages/CapabilitiesPage';
import { DashboardPage } from './admin/pages/DashboardPage';
import { PoliciesPage } from './admin/pages/PoliciesPage';
import { MindSpacePage } from './admin/pages/MindSpacePage';
import { MemoryV2Page } from './admin/pages/MemoryV2Page';
import { ProvidersPage } from './admin/pages/ProvidersPage';
import { SkillsPage } from './admin/pages/SkillsPage';
import { SystemTestsPage } from './admin/pages/SystemTestsPage';
import { UserDetailPage } from './admin/pages/UserDetailPage';
import { UsersPage } from './admin/pages/UsersPage';
import { WechatPage } from './admin/pages/WechatPage';
import { BlockedWordsPage } from './admin/pages/BlockedWordsPage';
import { defaultHomePath } from './lib/routes';
import { OpsLayout } from './ops/components/OpsLayout';
import { AnalyticsPage } from './ops/pages/AnalyticsPage';
import { CreatorsPage } from './ops/pages/CreatorsPage';
import { FeaturedPage } from './ops/pages/FeaturedPage';
import { ReportsPage } from './ops/pages/ReportsPage';
import { ReviewPage } from './ops/pages/ReviewPage';
import type { PortalUser } from './types';
function LegacyAdminRedirect() {
const { pathname } = useLocation();
const rest = pathname.replace(/^\/admin\/?/, '');
return <Navigate to={rest ? `/${rest}` : '/'} replace />;
}
function RejectOpsAdminRedirect() {
const { pathname } = useLocation();
const rest = pathname.replace(/^\/ops\/admin\/?/, '');
return <Navigate to={rest ? `/${rest}` : '/'} replace />;
}
function AdminLoginPage({
onAuth,
redirectTo,
}: {
onAuth: (user: PortalUser) => void;
redirectTo?: string;
}) {
const navigate = useNavigate();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
const user = await login(username, password);
if (!user) {
setError('登录失败');
return;
}
onAuth(user);
navigate(redirectTo ?? defaultHomePath(user.role), { replace: true });
} catch (err) {
setError(err instanceof Error ? err.message : '登录失败');
} finally {
setLoading(false);
}
};
return (
<div className="admin-login-page">
<div className="admin-login-card">
<h1 className="admin-login-title">TKMind </h1>
<p className="muted" style={{ marginTop: -8, marginBottom: 20 }}>
使
</p>
{error && <p className="banner banner-error">{error}</p>}
<form onSubmit={handleSubmit} className="admin-login-form">
<input
className="admin-login-input"
type="text"
placeholder="用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
autoFocus
required
/>
<input
className="admin-login-input"
type="password"
autoComplete="current-password"
placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit" className="send-btn" disabled={loading}>
{loading ? '登录中…' : '登录'}
</button>
</form>
</div>
</div>
);
}
function AdminApp({ user, onLogout }: { user: PortalUser; onLogout: () => void }) {
return (
<Routes>
{user.role === 'admin' ? (
<Route path="/" element={<AdminLayout user={user} onLogout={onLogout} />}>
<Route index element={<DashboardPage />} />
<Route path="users" element={<UsersPage />} />
<Route path="users/:userId" element={<UserDetailPage />} />
<Route path="billing/*" element={<BillingPage />} />
<Route path="capabilities" element={<CapabilitiesPage />} />
<Route path="skills" element={<SkillsPage />} />
<Route path="system-tests" element={<SystemTestsPage />} />
<Route path="policies" element={<PoliciesPage />} />
<Route path="mindspace" element={<MindSpacePage />} />
<Route path="memory-v2" element={<MemoryV2Page />} />
<Route path="providers" element={<ProvidersPage />} />
<Route path="wechat" element={<WechatPage />} />
<Route path="blocked-words" element={<BlockedWordsPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
) : (
<Route path="/" element={<Navigate to="/ops" replace />} />
)}
<Route path="/ops" element={<OpsLayout user={user} onLogout={onLogout} />}>
<Route index element={<ReviewPage />} />
<Route path="reports" element={<ReportsPage />} />
<Route path="featured" element={<FeaturedPage />} />
<Route path="creators" element={<CreatorsPage />} />
<Route path="analytics" element={<AnalyticsPage />} />
</Route>
<Route path="/admin/*" element={<LegacyAdminRedirect />} />
<Route path="/ops/admin" element={<Navigate to="/" replace />} />
<Route path="/ops/admin/*" element={<RejectOpsAdminRedirect />} />
<Route path="*" element={<Navigate to={defaultHomePath(user.role)} replace />} />
</Routes>
);
}
function loginRedirectPath(pathname: string, role: string | undefined) {
if (pathname.startsWith('/ops/admin')) {
const rest = pathname.replace(/^\/ops\/admin\/?/, '');
return rest ? `/${rest}` : '/';
}
if (pathname.startsWith('/admin')) {
const rest = pathname.replace(/^\/admin\/?/, '');
if (role === undefined) return rest ? `/${rest}` : '/';
return role === 'admin' ? (rest ? `/${rest}` : '/') : '/ops';
}
if (pathname === '/' || pathname.startsWith('/ops')) {
return pathname;
}
if (
pathname.startsWith('/users')
|| pathname.startsWith('/billing')
|| pathname.startsWith('/capabilities')
|| pathname.startsWith('/skills')
|| pathname.startsWith('/system-tests')
|| pathname.startsWith('/policies')
|| pathname.startsWith('/mindspace')
|| pathname.startsWith('/memory-v2')
|| pathname.startsWith('/providers')
|| pathname.startsWith('/wechat')
) {
return role === 'admin' || role === undefined ? pathname : '/ops';
}
return defaultHomePath(role);
}
export function App() {
const navigate = useNavigate();
const location = useLocation();
const [authed, setAuthed] = useState<boolean | null>(null);
const [user, setUser] = useState<PortalUser | null>(null);
useEffect(() => {
setUnauthorizedHandler(() => {
setAuthed(false);
setUser(null);
navigate('/', { replace: true });
});
void checkAuth().then((status) => {
setAuthed(status.authenticated);
setUser(status.authenticated ? (status.user ?? null) : null);
});
}, [navigate]);
if (authed === null) {
return <div className="app-loading"></div>;
}
if (!authed || !user) {
return (
<AdminLoginPage
redirectTo={loginRedirectPath(location.pathname, undefined)}
onAuth={(nextUser) => {
setAuthed(true);
setUser(nextUser);
}}
/>
);
}
const handleLogout = () => {
void logout().finally(() => {
setAuthed(false);
setUser(null);
navigate('/', { replace: true });
});
};
return <AdminApp user={user} onLogout={handleLogout} />;
}