imetting/frontend/src/App.jsx

274 lines
9.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import React, { useState, useEffect } from 'react';
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';
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';
import MeetingCenterPage from './pages/MeetingCenterPage';
import MainLayout from './components/MainLayout';
import menuService from './services/menuService';
import configService from './utils/configService';
import './App.css';
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(() => {
const savedAuth = localStorage.getItem('iMeetingUser');
if (savedAuth && savedAuth !== "undefined" && savedAuth !== "null") {
try {
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);
}, []);
useEffect(() => {
let active = true;
configService.getBrandingConfig().then((branding) => {
if (active && branding?.app_name) {
document.title = branding.app_name;
}
}).catch(() => {});
return () => {
active = false;
};
}, []);
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');
menuService.clearCache();
window.location.href = '/';
}
};
if (isLoading) {
return (
<div className="app-loading">
<div className="loading-spinner"></div>
<p>加载中...</p>
</div>
);
}
return (
<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>
);
}
export default App;