Files
Academic-Affairs-System/web-react/src/app/App.tsx
T
biss 1ab9b76101 feat(web): migrate account and user management to React
Add React account and user administration flows, including TOTP and SSO handling. Route /next SSO callbacks to React while preserving the existing Vue and native app behavior, and cover the routing policy with API tests.
2026-10-01 13:28:42 +08:00

95 lines
3.4 KiB
TypeScript

import { lazy, Suspense, useEffect } from 'react'
import { App as AntdApp, ConfigProvider, Skeleton } from 'antd'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { BrowserRouter, Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'
import zhCN from 'antd/locale/zh_CN'
import { useAuthStore } from '../stores/auth'
const LoginPage = lazy(() => import('../pages/LoginPage'))
const DashboardPage = lazy(() => import('../pages/DashboardPage'))
const AccountPage = lazy(() => import('../pages/AccountPage'))
const UsersPage = lazy(() => import('../pages/UsersPage'))
const SsoCallbackPage = lazy(() => import('../pages/SsoCallbackPage'))
const SsoBindPage = lazy(() => import('../pages/SsoBindPage'))
const AppShell = lazy(() => import('../layout/AppShell'))
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
retry: 1,
refetchOnWindowFocus: false,
},
},
})
function ProtectedRoute() {
const token = useAuthStore((state) => state.token)
const location = useLocation()
if (!token) return <Navigate to={`/login?redirect=${encodeURIComponent(location.pathname + location.search)}`} replace />
return <AppShell />
}
function SessionExpiryBridge() {
const navigate = useNavigate()
useEffect(() => {
const onExpired = () => navigate(`/login?redirect=${encodeURIComponent(location.pathname + location.search + location.hash)}`, { replace: true })
window.addEventListener('mingxu-session-expired', onExpired)
return () => window.removeEventListener('mingxu-session-expired', onExpired)
}, [navigate])
return null
}
function AppRoutes() {
return (
<Suspense fallback={<div style={{ maxWidth: 1180, margin: '64px auto', padding: 24 }}><Skeleton active paragraph={{ rows: 8 }} /></div>}>
<SessionExpiryBridge />
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/sso/callback" element={<SsoCallbackPage />} />
<Route path="/sso/bind" element={<SsoBindPage />} />
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/account" element={<AccountPage />} />
<Route path="/users" element={<UsersPage />} />
<Route path="/" element={<Navigate to="/dashboard" replace />} />
</Route>
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</Suspense>
)
}
export default function App() {
return (
<ConfigProvider
locale={zhCN}
theme={{
token: {
colorPrimary: '#284583',
colorSuccess: '#087f73',
colorWarning: '#b8781d',
colorError: '#b42318',
colorInfo: '#476282',
colorText: '#182033',
colorBgLayout: '#f2f4f7',
borderRadius: 7,
fontFamily: '"Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", sans-serif',
},
components: {
Button: { controlHeightLG: 44, fontWeight: 650 },
Input: { controlHeightLG: 44 },
Layout: { headerBg: '#ffffff', siderBg: '#162858' },
Menu: { darkItemBg: '#162858', darkItemSelectedBg: '#284583', darkItemSelectedColor: '#ffffff' },
},
}}
>
<AntdApp>
<QueryClientProvider client={queryClient}>
<BrowserRouter basename="/next"><AppRoutes /></BrowserRouter>
</QueryClientProvider>
</AntdApp>
</ConfigProvider>
)
}