274 lines
9.3 KiB
JavaScript
274 lines
9.3 KiB
JavaScript
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;
|