imetting/frontend/src/App.jsx

274 lines
9.3 KiB
React
Raw Normal View History

import React, { useState, useEffect } from 'react';
2026-03-26 06:55:12 +00:00
import { BrowserRouter as Router, Routes, Route, Navigate, Outlet } from 'react-router-dom';
import { ConfigProvider, theme, App as AntdApp } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import apiClient from './utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from './config/api';
import HomePage from './pages/HomePage';
import Dashboard from './pages/Dashboard';
import AdminDashboard from './pages/AdminDashboard';
import MeetingDetails from './pages/MeetingDetails';
import MeetingPreview from './pages/MeetingPreview';
import AdminManagement from './pages/AdminManagement';
import PromptManagementPage from './pages/PromptManagementPage';
2026-03-26 06:55:12 +00:00
import PromptConfigPage from './pages/PromptConfigPage';
import KnowledgeBasePage from './pages/KnowledgeBasePage';
import EditKnowledgeBase from './pages/EditKnowledgeBase';
import ClientDownloadPage from './pages/ClientDownloadPage';
import AccountSettings from './pages/AccountSettings';
2026-03-26 06:55:12 +00:00
import MeetingCenterPage from './pages/MeetingCenterPage';
import MainLayout from './components/MainLayout';
import menuService from './services/menuService';
2026-04-08 09:29:06 +00:00
import configService from './utils/configService';
import './App.css';
2026-03-26 06:55:12 +00:00
import './styles/console-theme.css';
// Layout Wrapper to inject user and handleLogout
const AuthenticatedLayout = ({ user, handleLogout }) => {
// 如果还在加载中或用户不存在,不渲染,避免闪烁
if (!user) return null;
return (
<MainLayout user={user} onLogout={handleLogout}>
<Outlet />
</MainLayout>
);
};
const DefaultMenuRedirect = ({ user }) => {
const [targetPath, setTargetPath] = useState(null);
useEffect(() => {
let active = true;
const resolveDefaultPath = async () => {
try {
const path = await menuService.getDefaultPath();
if (active) {
setTargetPath(path || '/dashboard');
}
} catch (error) {
console.error('Resolve default menu path failed:', error);
if (active) {
setTargetPath('/dashboard');
}
}
};
if (user) {
resolveDefaultPath();
}
return () => {
active = false;
};
}, [user]);
if (!user) {
return <Navigate to="/" replace />;
}
if (!targetPath) {
return (
<div className="app-loading">
<div className="loading-spinner"></div>
<p>加载菜单中...</p>
</div>
);
}
return <Navigate to={targetPath} replace />;
};
function App() {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
// Load user from localStorage on app start
useEffect(() => {
2026-03-26 06:55:12 +00:00
const savedAuth = localStorage.getItem('iMeetingUser');
if (savedAuth && savedAuth !== "undefined" && savedAuth !== "null") {
try {
2026-03-26 06:55:12 +00:00
const authData = JSON.parse(savedAuth);
// 如果数据包含 user 字段,则提取 user 字段(适应新结构)
// 否则使用整个对象(兼容旧结构)
const userData = authData.user || authData;
if (userData && typeof userData === 'object' && (userData.user_id || userData.id)) {
setUser(userData);
} else {
localStorage.removeItem('iMeetingUser');
}
} catch (error) {
console.error('Error parsing saved user:', error);
localStorage.removeItem('iMeetingUser');
}
}
setIsLoading(false);
}, []);
2026-04-08 09:29:06 +00:00
useEffect(() => {
let active = true;
configService.getBrandingConfig().then((branding) => {
if (active && branding?.app_name) {
document.title = branding.app_name;
}
}).catch(() => {});
return () => {
active = false;
};
}, []);
2026-03-26 06:55:12 +00:00
const handleLogin = (authData) => {
if (authData) {
menuService.clearCache();
// 提取用户信息用于 UI 展示
const userData = authData.user || authData;
setUser(userData);
// 存入完整 auth 数据(包含 token供拦截器使用
localStorage.setItem('iMeetingUser', JSON.stringify(authData));
}
};
const handleLogout = async () => {
try {
await apiClient.post(buildApiUrl(API_ENDPOINTS.AUTH.LOGOUT));
} catch (error) {
console.error('Logout API error:', error);
} finally {
setUser(null);
localStorage.removeItem('iMeetingUser');
2026-03-26 06:55:12 +00:00
menuService.clearCache();
window.location.href = '/';
}
};
if (isLoading) {
return (
<div className="app-loading">
<div className="loading-spinner"></div>
<p>加载中...</p>
</div>
);
}
return (
2026-03-26 06:55:12 +00:00
<ConfigProvider
locale={zhCN}
theme={{
token: {
colorPrimary: '#1d4ed8',
colorSuccess: '#0f766e',
colorWarning: '#d97706',
colorError: '#c2410c',
borderRadius: 12,
borderRadiusLG: 16,
wireframe: false,
fontFamily: '"MiSans", "PingFang SC", "Noto Sans SC", "Microsoft YaHei", sans-serif',
fontSize: 14,
colorTextBase: '#112b4e',
colorBgLayout: 'transparent',
},
algorithm: theme.defaultAlgorithm,
components: {
Layout: {
bodyBg: 'transparent',
siderBg: 'rgba(255,255,255,0.82)',
headerBg: 'transparent',
},
Card: {
paddingLG: 18,
},
Table: {
headerBorderRadius: 14,
},
Button: {
controlHeight: 40,
controlHeightLG: 46,
borderRadius: 12,
fontWeight: 600,
paddingInline: 18,
defaultBorderColor: 'rgba(148, 163, 184, 0.24)',
defaultColor: '#274365',
defaultBg: 'rgba(255,255,255,0.92)',
defaultHoverBg: '#ffffff',
defaultHoverBorderColor: 'rgba(59, 130, 246, 0.3)',
defaultHoverColor: '#1d4ed8',
defaultActiveBg: '#eff6ff',
primaryShadow: '0 12px 24px rgba(29, 78, 216, 0.18)',
dangerShadow: '0 12px 24px rgba(220, 38, 38, 0.16)',
},
Switch: {
trackMinWidth: 40,
trackHeight: 22,
trackPadding: 2,
handleSize: 18,
innerMinMargin: 4,
innerMaxMargin: 26,
borderRadius: 100,
},
},
}}
>
<AntdApp message={{ top: 64, maxCount: 3 }}>
<Router>
<div className="app">
<Routes>
{/* Public Routes */}
<Route path="/" element={
user ? <DefaultMenuRedirect user={user} /> : <HomePage onLogin={handleLogin} />
} />
<Route path="/meetings/preview/:meeting_id" element={<MeetingPreview />} />
<Route path="/downloads" element={<ClientDownloadPage />} />
{/* Authenticated Routes */}
<Route element={user ? <AuthenticatedLayout user={user} handleLogout={handleLogout} /> : <Navigate to="/" replace />}>
<Route path="/dashboard" element={
user?.role_id === 1
? <AdminDashboard user={user} onLogout={handleLogout} />
: <Dashboard user={user} onLogout={handleLogout} />
} />
<Route path="/meetings/center" element={
user?.role_id === 1
? <Navigate to="/dashboard" replace />
: <MeetingCenterPage user={user} />
} />
<Route path="/meetings/history" element={<Navigate to="/meetings/center" replace />} />
<Route path="/meetings/:meeting_id" element={<MeetingDetails user={user} />} />
<Route path="/meetings/create" element={<Navigate to="/meetings/center" replace />} />
<Route path="/meetings/edit/:meeting_id" element={<Navigate to="/meetings/center" replace />} />
<Route path="/admin/management" element={
user?.role_id === 1
? <Navigate to="/admin/management/system-overview" replace />
: <Navigate to="/dashboard" replace />
} />
<Route path="/admin/management/:moduleKey" element={
user?.role_id === 1 ? <AdminManagement user={user} /> : <Navigate to="/dashboard" replace />
} />
<Route path="/prompt-management" element={
user?.role_id === 1 ? <PromptManagementPage user={user} /> : <Navigate to="/dashboard" replace />
} />
<Route path="/prompt-config" element={<PromptConfigPage user={user} />} />
<Route path="/personal-prompts" element={
user?.role_id === 1 ? <Navigate to="/dashboard" replace /> : <Navigate to="/prompt-config" replace />
} />
<Route path="/knowledge-base" element={<KnowledgeBasePage user={user} />} />
<Route path="/knowledge-base/edit/:kb_id" element={<EditKnowledgeBase user={user} />} />
<Route path="/account-settings" element={<AccountSettings user={user} onUpdateUser={handleLogin} />} />
</Route>
{/* Catch all */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</div>
</Router>
</AntdApp>
</ConfigProvider>
);
}
2026-03-26 06:55:12 +00:00
export default App;