增加了mcp server

codex/dev
mula.liu 2026-04-14 12:37:48 +08:00
parent d69b346f4c
commit f44453d93e
11 changed files with 411 additions and 110 deletions

View File

@ -24,7 +24,7 @@ from app.api.endpoints import (
voiceprint,
)
from app.core.config import UPLOAD_DIR
from app.core.middleware import TerminalCheckMiddleware
from app.core.middleware import MCPPathNormalizeMiddleware, TerminalCheckMiddleware
from app.mcp import create_mcp_http_app, get_mcp_session_manager
from app.services.system_config_service import SystemConfigService
@ -38,6 +38,7 @@ def create_app() -> FastAPI:
redoc_url=None,
)
app.add_middleware(MCPPathNormalizeMiddleware)
app.add_middleware(TerminalCheckMiddleware)
app.add_middleware(
CORSMiddleware,
@ -50,8 +51,8 @@ def create_app() -> FastAPI:
if UPLOAD_DIR.exists():
app.mount("/uploads", StaticFiles(directory=str(UPLOAD_DIR)), name="uploads")
mcp_session_manager = get_mcp_session_manager()
mcp_asgi_app = create_mcp_http_app()
mcp_session_manager = get_mcp_session_manager()
if mcp_asgi_app is not None:
app.mount("/mcp", mcp_asgi_app, name="mcp")

View File

@ -5,6 +5,26 @@ from app.services.terminal_service import terminal_service
from app.services.jwt_service import jwt_service
from app.core.response import create_api_response
class MCPPathNormalizeMiddleware:
"""Normalize the public MCP endpoint to /mcp while keeping the mounted app happy."""
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] == "http" and scope.get("path") == "/mcp":
normalized_scope = dict(scope)
normalized_scope["path"] = "/mcp/"
raw_path = scope.get("raw_path")
if raw_path in (None, b"/mcp"):
normalized_scope["raw_path"] = b"/mcp/"
scope = normalized_scope
await self.app(scope, receive, send)
class TerminalCheckMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# 1. 检查是否有 Imei 头,没有则认为是普通请求,直接放行

View File

@ -6,6 +6,7 @@ from __future__ import annotations
from datetime import datetime
import hmac
import json
import logging
from typing import Any, Dict, Optional
from fastapi.responses import JSONResponse
@ -16,7 +17,7 @@ try:
except ImportError: # pragma: no cover - runtime dependency
FastMCP = None
from app.core.config import APP_CONFIG
from app.core.config import APP_CONFIG, API_CONFIG
from app.core.database import get_db_connection
from app.mcp.context import (
MCPRequestContext,
@ -26,6 +27,8 @@ from app.mcp.context import (
)
from app.services import meeting_service
logger = logging.getLogger(__name__)
def _build_absolute_url(path: str) -> str:
normalized_base = APP_CONFIG["base_url"].rstrip("/")
@ -181,6 +184,9 @@ class MCPHeaderAuthApp:
reset_current_mcp_request(token)
_mcp_http_app = None
if FastMCP is not None:
mcp_server = FastMCP(
"iMeeting MCP",
@ -188,6 +194,9 @@ if FastMCP is not None:
stateless_http=True,
streamable_http_path="/",
)
# Keep FastMCP's server settings aligned with the outer Uvicorn process.
mcp_server.settings.host = API_CONFIG["host"]
mcp_server.settings.port = API_CONFIG["port"]
@mcp_server.tool()
def get_my_meetings() -> Dict[str, Any]:
@ -237,6 +246,7 @@ if FastMCP is not None:
}
else: # pragma: no cover - graceful fallback without runtime dependency
mcp_server = None
logger.warning("FastMCP is unavailable because the 'mcp' package is not installed; /mcp will not be mounted.")
def get_mcp_server():
@ -244,15 +254,21 @@ def get_mcp_server():
def get_mcp_session_manager():
if mcp_server is None:
if create_mcp_http_app() is None:
return None
return mcp_server.session_manager
def create_mcp_http_app():
global _mcp_http_app
if mcp_server is None:
return None
return MCPHeaderAuthApp(mcp_server.streamable_http_app())
if _mcp_http_app is None:
# FastMCP initializes its session manager when the Streamable HTTP app
# is created, so cache the app and reuse it across startup/mounting.
_mcp_http_app = MCPHeaderAuthApp(mcp_server.streamable_http_app())
return _mcp_http_app
def get_mcp_asgi_app():

View File

@ -33,6 +33,28 @@ server {
proxy_send_timeout 300s;
}
# MCP Streamable HTTP 代理
# 对外以 /mcp 作为标准入口,避免 MCP Client 处理尾斜杠重定向。
location = /mcp {
proxy_pass http://backend:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
chunked_transfer_encoding on;
}
location = /mcp/ {
return 308 /mcp;
}
location = /docs {
proxy_pass http://backend:8000/docs;
proxy_http_version 1.1;

View File

@ -1,22 +1,9 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { Suspense, lazy, useCallback, useEffect, useState } 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 {
@ -28,6 +15,33 @@ import configService from './utils/configService';
import './App.css';
import './styles/console-theme.css';
const HomePage = lazy(() => import('./pages/HomePage'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
const MeetingDetails = lazy(() => import('./pages/MeetingDetails'));
const MeetingPreview = lazy(() => import('./pages/MeetingPreview'));
const AdminManagement = lazy(() => import('./pages/AdminManagement'));
const PromptManagementPage = lazy(() => import('./pages/PromptManagementPage'));
const PromptConfigPage = lazy(() => import('./pages/PromptConfigPage'));
const KnowledgeBasePage = lazy(() => import('./pages/KnowledgeBasePage'));
const EditKnowledgeBase = lazy(() => import('./pages/EditKnowledgeBase'));
const ClientDownloadPage = lazy(() => import('./pages/ClientDownloadPage'));
const AccountSettings = lazy(() => import('./pages/AccountSettings'));
const MeetingCenterPage = lazy(() => import('./pages/MeetingCenterPage'));
const RouteLoading = ({ text = '页面加载中...' }) => (
<div className="app-loading">
<div className="loading-spinner"></div>
<p>{text}</p>
</div>
);
const withRouteSuspense = (node, text) => (
<Suspense fallback={<RouteLoading text={text} />}>
{node}
</Suspense>
);
// Layout Wrapper to inject user and handleLogout
const AuthenticatedLayout = ({ user, handleLogout }) => {
//
@ -228,26 +242,28 @@ function App() {
<Routes>
{/* Public Routes */}
<Route path="/" element={
user ? <DefaultMenuRedirect user={user} /> : <HomePage onLogin={handleLogin} />
user
? <DefaultMenuRedirect user={user} />
: withRouteSuspense(<HomePage onLogin={handleLogin} />, '加载登录页中...')
} />
<Route path="/meetings/preview/:meeting_id" element={<MeetingPreview />} />
<Route path="/downloads" element={<ClientDownloadPage />} />
<Route path="/meetings/preview/:meeting_id" element={withRouteSuspense(<MeetingPreview />, '加载会议预览中...')} />
<Route path="/downloads" element={withRouteSuspense(<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} />
? withRouteSuspense(<AdminDashboard user={user} onLogout={handleLogout} />, '加载工作台中...')
: withRouteSuspense(<Dashboard user={user} onLogout={handleLogout} />, '加载工作台中...')
} />
<Route path="/meetings/center" element={
user?.role_id === 1
? <Navigate to="/dashboard" replace />
: <MeetingCenterPage user={user} />
: withRouteSuspense(<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/:meeting_id" element={withRouteSuspense(<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={
@ -256,18 +272,22 @@ function App() {
: <Navigate to="/dashboard" replace />
} />
<Route path="/admin/management/:moduleKey" element={
user?.role_id === 1 ? <AdminManagement user={user} /> : <Navigate to="/dashboard" replace />
user?.role_id === 1
? withRouteSuspense(<AdminManagement user={user} />, '加载管理模块中...')
: <Navigate to="/dashboard" replace />
} />
<Route path="/prompt-management" element={
user?.role_id === 1 ? <PromptManagementPage user={user} /> : <Navigate to="/dashboard" replace />
user?.role_id === 1
? withRouteSuspense(<PromptManagementPage user={user} />, '加载提示词页面中...')
: <Navigate to="/dashboard" replace />
} />
<Route path="/prompt-config" element={<PromptConfigPage user={user} />} />
<Route path="/prompt-config" element={withRouteSuspense(<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 path="/knowledge-base" element={withRouteSuspense(<KnowledgeBasePage user={user} />, '加载知识库中...')} />
<Route path="/knowledge-base/edit/:kb_id" element={withRouteSuspense(<EditKnowledgeBase user={user} />, '加载知识库编辑页中...')} />
<Route path="/account-settings" element={withRouteSuspense(<AccountSettings user={user} onUpdateUser={handleLogin} />, '加载账号设置中...')} />
</Route>
{/* Catch all */}

View File

@ -1,9 +1,6 @@
import React, { useState, useRef, useMemo } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { EditorView } from '@codemirror/view';
import React, { Suspense, lazy, useEffect, useMemo, useRef, useState } from 'react';
import {
Space, Button, Tooltip, Card,
Space, Button, Tooltip, Card, Spin,
Divider, Dropdown, Typography
} from 'antd';
import {
@ -13,7 +10,8 @@ import {
UnorderedListOutlined, LineOutlined, EyeOutlined,
EditOutlined
} from '@ant-design/icons';
import MarkdownRenderer from './MarkdownRenderer';
const MarkdownRenderer = lazy(() => import('./MarkdownRenderer'));
const { Text } = Typography;
@ -28,11 +26,41 @@ const MarkdownEditor = ({
const editorRef = useRef(null);
const imageInputRef = useRef(null);
const [showPreview, setShowPreview] = useState(false);
const [editorRuntime, setEditorRuntime] = useState(null);
useEffect(() => {
let active = true;
Promise.all([
import('@uiw/react-codemirror'),
import('@codemirror/lang-markdown'),
import('@codemirror/view'),
])
.then(([codeMirrorModule, markdownModule, viewModule]) => {
if (!active) {
return;
}
setEditorRuntime({
CodeMirror: codeMirrorModule.default,
markdown: markdownModule.markdown,
markdownLanguage: markdownModule.markdownLanguage,
EditorView: viewModule.EditorView,
});
})
.catch((error) => {
console.error('Load markdown editor runtime failed:', error);
});
return () => {
active = false;
};
}, []);
const editorExtensions = useMemo(() => [
markdown({ base: markdownLanguage }),
EditorView.lineWrapping,
EditorView.theme({
...(editorRuntime ? [editorRuntime.markdown({ base: editorRuntime.markdownLanguage })] : []),
editorRuntime?.EditorView?.lineWrapping,
editorRuntime?.EditorView?.theme({
"&": {
fontSize: "14px",
border: "1px solid #d9d9d9",
@ -50,7 +78,9 @@ const MarkdownEditor = ({
boxShadow: "0 0 0 2px rgba(22, 119, 255, 0.1)",
}
})
], [height]);
].filter(Boolean), [editorRuntime, height]);
const CodeMirror = editorRuntime?.CodeMirror;
const insertMarkdown = (before, after = '', placeholder = '') => {
if (!editorRef.current?.view) return;
@ -133,9 +163,12 @@ const MarkdownEditor = ({
{showPreview ? (
<Card bordered styles={{ body: { padding: 16, minHeight: height, overflowY: 'auto' } }} style={{ borderRadius: '0 0 8px 8px' }}>
<Suspense fallback={<Spin tip="预览加载中..." />}>
<MarkdownRenderer content={value} />
</Suspense>
</Card>
) : (
CodeMirror ? (
<CodeMirror
ref={editorRef}
value={value}
@ -144,6 +177,15 @@ const MarkdownEditor = ({
placeholder={placeholder}
basicSetup={{ lineNumbers: false, foldGutter: false }}
/>
) : (
<Card
bordered
styles={{ body: { minHeight: height, display: 'flex', alignItems: 'center', justifyContent: 'center' } }}
style={{ borderRadius: '0 0 8px 8px' }}
>
<Spin tip="编辑器加载中..." />
</Card>
)
)}
<input ref={imageInputRef} type="file" accept="image/*" onChange={(e) => {

View File

@ -1,16 +1,68 @@
import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import { Typography, Empty } from 'antd';
import React, { useEffect, useState } from 'react';
import { Typography, Empty, Spin } from 'antd';
const { Paragraph } = Typography;
const MarkdownRenderer = ({ content, className = "", emptyMessage = "暂无内容" }) => {
const [runtime, setRuntime] = useState(null);
useEffect(() => {
if (!content || runtime) {
return undefined;
}
let active = true;
Promise.all([
import('react-markdown'),
import('remark-gfm'),
import('rehype-raw'),
])
.then(([reactMarkdownModule, remarkGfmModule, rehypeRawModule]) => {
if (!active) {
return;
}
setRuntime({
ReactMarkdown: reactMarkdownModule.default,
remarkGfm: remarkGfmModule.default,
rehypeRaw: rehypeRawModule.default,
});
})
.catch((error) => {
console.error('Load markdown renderer runtime failed:', error);
});
return () => {
active = false;
};
}, [content, runtime]);
if (!content) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={emptyMessage} />;
}
if (!runtime) {
return (
<div
className={`markdown-renderer-modern ${className}`}
style={{
minHeight: 120,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '15px',
lineHeight: 1.8,
color: '#262626',
}}
>
<Spin tip="内容渲染中..." />
</div>
);
}
const { ReactMarkdown, remarkGfm, rehypeRaw } = runtime;
return (
<div className={`markdown-renderer-modern ${className}`} style={{ fontSize: '15px', lineHeight: 1.8, color: '#262626' }}>
<ReactMarkdown

View File

@ -1,11 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { Transformer } from 'markmap-lib';
import { Markmap } from 'markmap-view';
import { Spin, Empty, Button, Space } from 'antd';
import { FullscreenOutlined, ZoomInOutlined, ZoomOutOutlined, SyncOutlined } from '@ant-design/icons';
const transformer = new Transformer();
const hasRenderableSize = (element) => {
if (!element) return false;
const rect = element.getBoundingClientRect();
@ -16,13 +12,48 @@ const MindMap = ({ content }) => {
const svgRef = useRef(null);
const markmapRef = useRef(null);
const latestRootRef = useRef(null);
const runtimeRef = useRef(null);
const runtimePromiseRef = useRef(null);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState(null);
const ensureRuntime = async () => {
if (runtimeRef.current) {
return runtimeRef.current;
}
if (!runtimePromiseRef.current) {
runtimePromiseRef.current = Promise.all([
import('markmap-lib'),
import('markmap-view'),
]).then(([markmapLibModule, markmapViewModule]) => {
runtimeRef.current = {
transformer: new markmapLibModule.Transformer(),
Markmap: markmapViewModule.Markmap,
};
return runtimeRef.current;
});
}
return runtimePromiseRef.current;
};
useEffect(() => {
if (!content || !svgRef.current) return;
if (!content || !svgRef.current) return undefined;
let active = true;
setLoading(true);
setLoadError(null);
(async () => {
try {
const { transformer, Markmap } = await ensureRuntime();
if (!active || !svgRef.current) {
return;
}
const { root } = transformer.transform(content);
latestRootRef.current = root;
@ -42,9 +73,19 @@ const MindMap = ({ content }) => {
});
} catch (error) {
console.error('Markmap error:', error);
if (active) {
setLoadError(error);
}
} finally {
if (active) {
setLoading(false);
}
}
})();
return () => {
active = false;
};
}, [content]);
useEffect(() => {
@ -59,10 +100,20 @@ const MindMap = ({ content }) => {
}
if (!markmapRef.current && latestRootRef.current) {
ensureRuntime()
.then(({ Markmap }) => {
if (markmapRef.current || !latestRootRef.current) {
return;
}
markmapRef.current = Markmap.create(svgElement, {
autoFit: false,
duration: 500,
}, latestRootRef.current);
})
.catch((error) => {
console.error('Resize observer markmap init failed:', error);
});
}
requestAnimationFrame(() => {
@ -80,6 +131,10 @@ const MindMap = ({ content }) => {
if (!content) return <Empty description="暂无内容,无法生成思维导图" />;
if (loadError) {
return <Empty description="思维导图加载失败,请稍后重试" />;
}
return (
<div className="mindmap-container" style={{ position: 'relative', width: '100%', height: '100%', minHeight: 500 }}>
{loading && (

View File

@ -9,15 +9,8 @@ import {
DesktopOutlined,
DeploymentUnitOutlined,
} from '@ant-design/icons';
import UserManagement from '../pages/admin/UserManagement';
import PermissionManagement from '../pages/admin/PermissionManagement';
import DictManagement from '../pages/admin/DictManagement';
import HotWordManagement from '../pages/admin/HotWordManagement';
import ParameterManagement from '../pages/admin/ParameterManagement';
import ModelManagement from '../pages/admin/ModelManagement';
import ClientManagement from '../pages/ClientManagement';
import ExternalAppManagement from '../pages/admin/ExternalAppManagement';
import TerminalManagement from '../pages/admin/TerminalManagement';
export const SYSTEM_MANAGEMENT_OVERVIEW_LOADER = () => import('../pages/admin/SystemManagementOverview');
export const SYSTEM_MANAGEMENT_MODULES = [
{
@ -26,7 +19,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
desc: '账号、角色、密码重置',
path: '/admin/management/user-management',
icon: <UserOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
component: UserManagement,
loader: () => import('../pages/admin/UserManagement'),
group: '基础治理',
},
{
@ -35,7 +28,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
desc: '菜单与角色授权矩阵',
path: '/admin/management/permission-management',
icon: <KeyOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
component: PermissionManagement,
loader: () => import('../pages/admin/PermissionManagement'),
group: '基础治理',
},
{
@ -44,7 +37,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
desc: '码表、平台类型、扩展属性',
path: '/admin/management/dict-management',
icon: <DatabaseOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
component: DictManagement,
loader: () => import('../pages/admin/DictManagement'),
group: '基础治理',
},
{
@ -53,7 +46,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
desc: 'ASR 热词与同步',
path: '/admin/management/hot-word-management',
icon: <FontSizeOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
component: HotWordManagement,
loader: () => import('../pages/admin/HotWordManagement'),
group: '基础治理',
},
{
@ -62,7 +55,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
desc: '系统参数统一配置',
path: '/admin/management/parameter-management',
icon: <SettingOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
component: ParameterManagement,
loader: () => import('../pages/admin/ParameterManagement'),
group: '平台配置',
},
{
@ -71,7 +64,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
desc: '音频模型与 LLM 模型配置',
path: '/admin/management/model-management',
icon: <AppstoreOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
component: ModelManagement,
loader: () => import('../pages/admin/ModelManagement'),
group: '平台配置',
},
{
@ -80,7 +73,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
desc: '版本、下载地址、发布状态',
path: '/admin/management/client-management',
icon: <DesktopOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
component: ClientManagement,
loader: () => import('../pages/ClientManagement'),
group: '设备与应用',
},
{
@ -89,7 +82,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
desc: '外部系统入口与图标配置',
path: '/admin/management/external-app-management',
icon: <AppstoreOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
component: ExternalAppManagement,
loader: () => import('../pages/admin/ExternalAppManagement'),
group: '设备与应用',
},
{
@ -98,7 +91,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
desc: '专用设备、激活和绑定状态',
path: '/admin/management/terminal-management',
icon: <DeploymentUnitOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
component: TerminalManagement,
loader: () => import('../pages/admin/TerminalManagement'),
group: '设备与应用',
},
];

View File

@ -1,9 +1,9 @@
import React, { useMemo } from 'react';
import React, { Suspense, lazy, useMemo } from 'react';
import { Alert } from 'antd';
import { Navigate, useParams } from 'react-router-dom';
import SystemManagementOverview from './admin/SystemManagementOverview';
import {
DEFAULT_SYSTEM_MANAGEMENT_MODULE,
SYSTEM_MANAGEMENT_OVERVIEW_LOADER,
SYSTEM_MANAGEMENT_MODULE_MAP,
} from '../config/systemManagementModules';
@ -13,15 +13,23 @@ const AdminManagement = () => {
const currentModule = useMemo(() => (
currentModuleKey === DEFAULT_SYSTEM_MANAGEMENT_MODULE
? { component: SystemManagementOverview }
? { loader: SYSTEM_MANAGEMENT_OVERVIEW_LOADER }
: SYSTEM_MANAGEMENT_MODULE_MAP[currentModuleKey]
), [currentModuleKey]);
const CurrentComponent = useMemo(() => {
if (!currentModule?.loader) {
return null;
}
return lazy(currentModule.loader);
}, [currentModule]);
if (!moduleKey) {
return <Navigate to={`/admin/management/${DEFAULT_SYSTEM_MANAGEMENT_MODULE}`} replace />;
}
if (!currentModule) {
if (!currentModule || !CurrentComponent) {
return (
<Alert
type="warning"
@ -32,8 +40,11 @@ const AdminManagement = () => {
);
}
const CurrentComponent = currentModule.component;
return <CurrentComponent />;
return (
<Suspense fallback={<Alert type="info" showIcon message="管理模块加载中..." />}>
<CurrentComponent />
</Suspense>
);
};
export default AdminManagement;

View File

@ -1,9 +1,78 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
const manualChunks = (id) => {
if (!id.includes('node_modules')) {
return undefined;
}
const normalizedId = id.split('node_modules/')[1];
if (
normalizedId.startsWith('react/') ||
normalizedId.startsWith('react-dom/') ||
normalizedId.startsWith('scheduler/')
) {
return 'react-vendor';
}
if (
normalizedId.startsWith('@ant-design/icons/') ||
normalizedId.startsWith('@ant-design/icons-svg/')
) {
return 'ant-icons-vendor';
}
if (normalizedId.startsWith('axios/')) {
return 'http-vendor';
}
if (normalizedId.startsWith('yaml/')) {
return 'yaml-vendor';
}
if (
normalizedId.startsWith('markdown-it/') ||
normalizedId.startsWith('markdown-it-') ||
normalizedId.startsWith('linkify-it/') ||
normalizedId.startsWith('mdurl/') ||
normalizedId.startsWith('uc.micro/') ||
normalizedId.startsWith('entities/') ||
normalizedId.startsWith('punycode.js/') ||
normalizedId.startsWith('katex/') ||
normalizedId.startsWith('@vscode/markdown-it-katex/')
) {
return 'markdown-parser-vendor';
}
if (
normalizedId.startsWith('html2canvas/') ||
normalizedId.startsWith('jspdf/') ||
normalizedId.startsWith('canvg/')
) {
return 'capture-vendor';
}
if (
normalizedId.startsWith('qrcode.react/') ||
normalizedId.startsWith('qrcode/')
) {
return 'qrcode-vendor';
}
return undefined;
};
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks,
},
},
},
server: {
host: true, // Optional: Allows the server to be accessible externally
allowedHosts: ['dev.imeeting.unisspace.com','nonperforated-ivonne-fezzy.ngrok-free.dev'], // Add the problematic hostname here