219 lines
7.8 KiB
TypeScript
219 lines
7.8 KiB
TypeScript
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} />;
|
||
}
|