增加了mcp server
parent
d69b346f4c
commit
f44453d93e
|
|
@ -24,7 +24,7 @@ from app.api.endpoints import (
|
||||||
voiceprint,
|
voiceprint,
|
||||||
)
|
)
|
||||||
from app.core.config import UPLOAD_DIR
|
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.mcp import create_mcp_http_app, get_mcp_session_manager
|
||||||
from app.services.system_config_service import SystemConfigService
|
from app.services.system_config_service import SystemConfigService
|
||||||
|
|
||||||
|
|
@ -38,6 +38,7 @@ def create_app() -> FastAPI:
|
||||||
redoc_url=None,
|
redoc_url=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.add_middleware(MCPPathNormalizeMiddleware)
|
||||||
app.add_middleware(TerminalCheckMiddleware)
|
app.add_middleware(TerminalCheckMiddleware)
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
|
|
@ -50,8 +51,8 @@ def create_app() -> FastAPI:
|
||||||
if UPLOAD_DIR.exists():
|
if UPLOAD_DIR.exists():
|
||||||
app.mount("/uploads", StaticFiles(directory=str(UPLOAD_DIR)), name="uploads")
|
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_asgi_app = create_mcp_http_app()
|
||||||
|
mcp_session_manager = get_mcp_session_manager()
|
||||||
if mcp_asgi_app is not None:
|
if mcp_asgi_app is not None:
|
||||||
app.mount("/mcp", mcp_asgi_app, name="mcp")
|
app.mount("/mcp", mcp_asgi_app, name="mcp")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,26 @@ from app.services.terminal_service import terminal_service
|
||||||
from app.services.jwt_service import jwt_service
|
from app.services.jwt_service import jwt_service
|
||||||
from app.core.response import create_api_response
|
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):
|
class TerminalCheckMiddleware(BaseHTTPMiddleware):
|
||||||
async def dispatch(self, request: Request, call_next):
|
async def dispatch(self, request: Request, call_next):
|
||||||
# 1. 检查是否有 Imei 头,没有则认为是普通请求,直接放行
|
# 1. 检查是否有 Imei 头,没有则认为是普通请求,直接放行
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from __future__ import annotations
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import hmac
|
import hmac
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
@ -16,7 +17,7 @@ try:
|
||||||
except ImportError: # pragma: no cover - runtime dependency
|
except ImportError: # pragma: no cover - runtime dependency
|
||||||
FastMCP = None
|
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.core.database import get_db_connection
|
||||||
from app.mcp.context import (
|
from app.mcp.context import (
|
||||||
MCPRequestContext,
|
MCPRequestContext,
|
||||||
|
|
@ -26,6 +27,8 @@ from app.mcp.context import (
|
||||||
)
|
)
|
||||||
from app.services import meeting_service
|
from app.services import meeting_service
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _build_absolute_url(path: str) -> str:
|
def _build_absolute_url(path: str) -> str:
|
||||||
normalized_base = APP_CONFIG["base_url"].rstrip("/")
|
normalized_base = APP_CONFIG["base_url"].rstrip("/")
|
||||||
|
|
@ -181,6 +184,9 @@ class MCPHeaderAuthApp:
|
||||||
reset_current_mcp_request(token)
|
reset_current_mcp_request(token)
|
||||||
|
|
||||||
|
|
||||||
|
_mcp_http_app = None
|
||||||
|
|
||||||
|
|
||||||
if FastMCP is not None:
|
if FastMCP is not None:
|
||||||
mcp_server = FastMCP(
|
mcp_server = FastMCP(
|
||||||
"iMeeting MCP",
|
"iMeeting MCP",
|
||||||
|
|
@ -188,6 +194,9 @@ if FastMCP is not None:
|
||||||
stateless_http=True,
|
stateless_http=True,
|
||||||
streamable_http_path="/",
|
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()
|
@mcp_server.tool()
|
||||||
def get_my_meetings() -> Dict[str, Any]:
|
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
|
else: # pragma: no cover - graceful fallback without runtime dependency
|
||||||
mcp_server = None
|
mcp_server = None
|
||||||
|
logger.warning("FastMCP is unavailable because the 'mcp' package is not installed; /mcp will not be mounted.")
|
||||||
|
|
||||||
|
|
||||||
def get_mcp_server():
|
def get_mcp_server():
|
||||||
|
|
@ -244,15 +254,21 @@ def get_mcp_server():
|
||||||
|
|
||||||
|
|
||||||
def get_mcp_session_manager():
|
def get_mcp_session_manager():
|
||||||
if mcp_server is None:
|
if create_mcp_http_app() is None:
|
||||||
return None
|
return None
|
||||||
return mcp_server.session_manager
|
return mcp_server.session_manager
|
||||||
|
|
||||||
|
|
||||||
def create_mcp_http_app():
|
def create_mcp_http_app():
|
||||||
|
global _mcp_http_app
|
||||||
|
|
||||||
if mcp_server is None:
|
if mcp_server is None:
|
||||||
return 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():
|
def get_mcp_asgi_app():
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,28 @@ server {
|
||||||
proxy_send_timeout 300s;
|
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 {
|
location = /docs {
|
||||||
proxy_pass http://backend:8000/docs;
|
proxy_pass http://backend:8000/docs;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|
|
||||||
|
|
@ -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 { BrowserRouter as Router, Routes, Route, Navigate, Outlet } from 'react-router-dom';
|
||||||
import { ConfigProvider, theme, App as AntdApp } from 'antd';
|
import { ConfigProvider, theme, App as AntdApp } from 'antd';
|
||||||
import zhCN from 'antd/locale/zh_CN';
|
import zhCN from 'antd/locale/zh_CN';
|
||||||
import apiClient from './utils/apiClient';
|
import apiClient from './utils/apiClient';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from './config/api';
|
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 MainLayout from './components/MainLayout';
|
||||||
import menuService from './services/menuService';
|
import menuService from './services/menuService';
|
||||||
import {
|
import {
|
||||||
|
|
@ -28,6 +15,33 @@ import configService from './utils/configService';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import './styles/console-theme.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
|
// Layout Wrapper to inject user and handleLogout
|
||||||
const AuthenticatedLayout = ({ user, handleLogout }) => {
|
const AuthenticatedLayout = ({ user, handleLogout }) => {
|
||||||
// 如果还在加载中或用户不存在,不渲染,避免闪烁
|
// 如果还在加载中或用户不存在,不渲染,避免闪烁
|
||||||
|
|
@ -228,26 +242,28 @@ function App() {
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Public Routes */}
|
{/* Public Routes */}
|
||||||
<Route path="/" element={
|
<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="/meetings/preview/:meeting_id" element={withRouteSuspense(<MeetingPreview />, '加载会议预览中...')} />
|
||||||
<Route path="/downloads" element={<ClientDownloadPage />} />
|
<Route path="/downloads" element={withRouteSuspense(<ClientDownloadPage />, '加载客户端下载页中...')} />
|
||||||
|
|
||||||
{/* Authenticated Routes */}
|
{/* Authenticated Routes */}
|
||||||
<Route element={user ? <AuthenticatedLayout user={user} handleLogout={handleLogout} /> : <Navigate to="/" replace />}>
|
<Route element={user ? <AuthenticatedLayout user={user} handleLogout={handleLogout} /> : <Navigate to="/" replace />}>
|
||||||
<Route path="/dashboard" element={
|
<Route path="/dashboard" element={
|
||||||
user?.role_id === 1
|
user?.role_id === 1
|
||||||
? <AdminDashboard user={user} onLogout={handleLogout} />
|
? withRouteSuspense(<AdminDashboard user={user} onLogout={handleLogout} />, '加载工作台中...')
|
||||||
: <Dashboard user={user} onLogout={handleLogout} />
|
: withRouteSuspense(<Dashboard user={user} onLogout={handleLogout} />, '加载工作台中...')
|
||||||
} />
|
} />
|
||||||
<Route path="/meetings/center" element={
|
<Route path="/meetings/center" element={
|
||||||
user?.role_id === 1
|
user?.role_id === 1
|
||||||
? <Navigate to="/dashboard" replace />
|
? <Navigate to="/dashboard" replace />
|
||||||
: <MeetingCenterPage user={user} />
|
: withRouteSuspense(<MeetingCenterPage user={user} />, '加载会议中心中...')
|
||||||
} />
|
} />
|
||||||
<Route path="/meetings/history" element={<Navigate to="/meetings/center" replace />} />
|
<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/create" element={<Navigate to="/meetings/center" replace />} />
|
||||||
<Route path="/meetings/edit/:meeting_id" element={<Navigate to="/meetings/center" replace />} />
|
<Route path="/meetings/edit/:meeting_id" element={<Navigate to="/meetings/center" replace />} />
|
||||||
<Route path="/admin/management" element={
|
<Route path="/admin/management" element={
|
||||||
|
|
@ -256,18 +272,22 @@ function App() {
|
||||||
: <Navigate to="/dashboard" replace />
|
: <Navigate to="/dashboard" replace />
|
||||||
} />
|
} />
|
||||||
<Route path="/admin/management/:moduleKey" element={
|
<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={
|
<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={
|
<Route path="/personal-prompts" element={
|
||||||
user?.role_id === 1 ? <Navigate to="/dashboard" replace /> : <Navigate to="/prompt-config" replace />
|
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" element={withRouteSuspense(<KnowledgeBasePage user={user} />, '加载知识库中...')} />
|
||||||
<Route path="/knowledge-base/edit/:kb_id" element={<EditKnowledgeBase user={user} />} />
|
<Route path="/knowledge-base/edit/:kb_id" element={withRouteSuspense(<EditKnowledgeBase user={user} />, '加载知识库编辑页中...')} />
|
||||||
<Route path="/account-settings" element={<AccountSettings user={user} onUpdateUser={handleLogin} />} />
|
<Route path="/account-settings" element={withRouteSuspense(<AccountSettings user={user} onUpdateUser={handleLogin} />, '加载账号设置中...')} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Catch all */}
|
{/* Catch all */}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import React, { useState, useRef, useMemo } from 'react';
|
import React, { Suspense, lazy, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import CodeMirror from '@uiw/react-codemirror';
|
|
||||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
|
||||||
import { EditorView } from '@codemirror/view';
|
|
||||||
import {
|
import {
|
||||||
Space, Button, Tooltip, Card,
|
Space, Button, Tooltip, Card, Spin,
|
||||||
Divider, Dropdown, Typography
|
Divider, Dropdown, Typography
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
|
|
@ -13,7 +10,8 @@ import {
|
||||||
UnorderedListOutlined, LineOutlined, EyeOutlined,
|
UnorderedListOutlined, LineOutlined, EyeOutlined,
|
||||||
EditOutlined
|
EditOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import MarkdownRenderer from './MarkdownRenderer';
|
|
||||||
|
const MarkdownRenderer = lazy(() => import('./MarkdownRenderer'));
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
|
@ -28,11 +26,41 @@ const MarkdownEditor = ({
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
const imageInputRef = useRef(null);
|
const imageInputRef = useRef(null);
|
||||||
const [showPreview, setShowPreview] = useState(false);
|
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(() => [
|
const editorExtensions = useMemo(() => [
|
||||||
markdown({ base: markdownLanguage }),
|
...(editorRuntime ? [editorRuntime.markdown({ base: editorRuntime.markdownLanguage })] : []),
|
||||||
EditorView.lineWrapping,
|
editorRuntime?.EditorView?.lineWrapping,
|
||||||
EditorView.theme({
|
editorRuntime?.EditorView?.theme({
|
||||||
"&": {
|
"&": {
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
border: "1px solid #d9d9d9",
|
border: "1px solid #d9d9d9",
|
||||||
|
|
@ -50,7 +78,9 @@ const MarkdownEditor = ({
|
||||||
boxShadow: "0 0 0 2px rgba(22, 119, 255, 0.1)",
|
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 = '') => {
|
const insertMarkdown = (before, after = '', placeholder = '') => {
|
||||||
if (!editorRef.current?.view) return;
|
if (!editorRef.current?.view) return;
|
||||||
|
|
@ -133,17 +163,29 @@ const MarkdownEditor = ({
|
||||||
|
|
||||||
{showPreview ? (
|
{showPreview ? (
|
||||||
<Card bordered styles={{ body: { padding: 16, minHeight: height, overflowY: 'auto' } }} style={{ borderRadius: '0 0 8px 8px' }}>
|
<Card bordered styles={{ body: { padding: 16, minHeight: height, overflowY: 'auto' } }} style={{ borderRadius: '0 0 8px 8px' }}>
|
||||||
<MarkdownRenderer content={value} />
|
<Suspense fallback={<Spin tip="预览加载中..." />}>
|
||||||
|
<MarkdownRenderer content={value} />
|
||||||
|
</Suspense>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<CodeMirror
|
CodeMirror ? (
|
||||||
ref={editorRef}
|
<CodeMirror
|
||||||
value={value}
|
ref={editorRef}
|
||||||
onChange={onChange}
|
value={value}
|
||||||
extensions={editorExtensions}
|
onChange={onChange}
|
||||||
placeholder={placeholder}
|
extensions={editorExtensions}
|
||||||
basicSetup={{ lineNumbers: false, foldGutter: false }}
|
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) => {
|
<input ref={imageInputRef} type="file" accept="image/*" onChange={(e) => {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,68 @@
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import { Typography, Empty, Spin } from 'antd';
|
||||||
import remarkGfm from 'remark-gfm';
|
|
||||||
import rehypeRaw from 'rehype-raw';
|
|
||||||
import { Typography, Empty } from 'antd';
|
|
||||||
|
|
||||||
const { Paragraph } = Typography;
|
const { Paragraph } = Typography;
|
||||||
|
|
||||||
const MarkdownRenderer = ({ content, className = "", emptyMessage = "暂无内容" }) => {
|
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) {
|
if (!content) {
|
||||||
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={emptyMessage} />;
|
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 (
|
return (
|
||||||
<div className={`markdown-renderer-modern ${className}`} style={{ fontSize: '15px', lineHeight: 1.8, color: '#262626' }}>
|
<div className={`markdown-renderer-modern ${className}`} style={{ fontSize: '15px', lineHeight: 1.8, color: '#262626' }}>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
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 { Spin, Empty, Button, Space } from 'antd';
|
||||||
import { FullscreenOutlined, ZoomInOutlined, ZoomOutOutlined, SyncOutlined } from '@ant-design/icons';
|
import { FullscreenOutlined, ZoomInOutlined, ZoomOutOutlined, SyncOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
const transformer = new Transformer();
|
|
||||||
|
|
||||||
const hasRenderableSize = (element) => {
|
const hasRenderableSize = (element) => {
|
||||||
if (!element) return false;
|
if (!element) return false;
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
|
|
@ -16,35 +12,80 @@ const MindMap = ({ content }) => {
|
||||||
const svgRef = useRef(null);
|
const svgRef = useRef(null);
|
||||||
const markmapRef = useRef(null);
|
const markmapRef = useRef(null);
|
||||||
const latestRootRef = useRef(null);
|
const latestRootRef = useRef(null);
|
||||||
|
const runtimeRef = useRef(null);
|
||||||
|
const runtimePromiseRef = useRef(null);
|
||||||
const [loading, setLoading] = useState(true);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!content || !svgRef.current) return;
|
if (!content || !svgRef.current) return undefined;
|
||||||
|
|
||||||
|
let active = true;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
setLoadError(null);
|
||||||
const { root } = transformer.transform(content);
|
|
||||||
latestRootRef.current = root;
|
|
||||||
|
|
||||||
if (markmapRef.current) {
|
(async () => {
|
||||||
markmapRef.current.setData(root);
|
try {
|
||||||
} else {
|
const { transformer, Markmap } = await ensureRuntime();
|
||||||
markmapRef.current = Markmap.create(svgRef.current, {
|
if (!active || !svgRef.current) {
|
||||||
autoFit: false,
|
return;
|
||||||
duration: 500,
|
|
||||||
}, root);
|
|
||||||
}
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (svgRef.current && hasRenderableSize(svgRef.current)) {
|
|
||||||
markmapRef.current?.fit();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
} catch (error) {
|
const { root } = transformer.transform(content);
|
||||||
console.error('Markmap error:', error);
|
latestRootRef.current = root;
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
if (markmapRef.current) {
|
||||||
}
|
markmapRef.current.setData(root);
|
||||||
|
} else {
|
||||||
|
markmapRef.current = Markmap.create(svgRef.current, {
|
||||||
|
autoFit: false,
|
||||||
|
duration: 500,
|
||||||
|
}, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (svgRef.current && hasRenderableSize(svgRef.current)) {
|
||||||
|
markmapRef.current?.fit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Markmap error:', error);
|
||||||
|
if (active) {
|
||||||
|
setLoadError(error);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (active) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
}, [content]);
|
}, [content]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -59,10 +100,20 @@ const MindMap = ({ content }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!markmapRef.current && latestRootRef.current) {
|
if (!markmapRef.current && latestRootRef.current) {
|
||||||
markmapRef.current = Markmap.create(svgElement, {
|
ensureRuntime()
|
||||||
autoFit: false,
|
.then(({ Markmap }) => {
|
||||||
duration: 500,
|
if (markmapRef.current || !latestRootRef.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(() => {
|
requestAnimationFrame(() => {
|
||||||
|
|
@ -80,6 +131,10 @@ const MindMap = ({ content }) => {
|
||||||
|
|
||||||
if (!content) return <Empty description="暂无内容,无法生成思维导图" />;
|
if (!content) return <Empty description="暂无内容,无法生成思维导图" />;
|
||||||
|
|
||||||
|
if (loadError) {
|
||||||
|
return <Empty description="思维导图加载失败,请稍后重试" />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mindmap-container" style={{ position: 'relative', width: '100%', height: '100%', minHeight: 500 }}>
|
<div className="mindmap-container" style={{ position: 'relative', width: '100%', height: '100%', minHeight: 500 }}>
|
||||||
{loading && (
|
{loading && (
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,8 @@ import {
|
||||||
DesktopOutlined,
|
DesktopOutlined,
|
||||||
DeploymentUnitOutlined,
|
DeploymentUnitOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import UserManagement from '../pages/admin/UserManagement';
|
|
||||||
import PermissionManagement from '../pages/admin/PermissionManagement';
|
export const SYSTEM_MANAGEMENT_OVERVIEW_LOADER = () => import('../pages/admin/SystemManagementOverview');
|
||||||
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_MODULES = [
|
export const SYSTEM_MANAGEMENT_MODULES = [
|
||||||
{
|
{
|
||||||
|
|
@ -26,7 +19,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
|
||||||
desc: '账号、角色、密码重置',
|
desc: '账号、角色、密码重置',
|
||||||
path: '/admin/management/user-management',
|
path: '/admin/management/user-management',
|
||||||
icon: <UserOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
icon: <UserOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||||
component: UserManagement,
|
loader: () => import('../pages/admin/UserManagement'),
|
||||||
group: '基础治理',
|
group: '基础治理',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -35,7 +28,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
|
||||||
desc: '菜单与角色授权矩阵',
|
desc: '菜单与角色授权矩阵',
|
||||||
path: '/admin/management/permission-management',
|
path: '/admin/management/permission-management',
|
||||||
icon: <KeyOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
icon: <KeyOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||||
component: PermissionManagement,
|
loader: () => import('../pages/admin/PermissionManagement'),
|
||||||
group: '基础治理',
|
group: '基础治理',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -44,7 +37,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
|
||||||
desc: '码表、平台类型、扩展属性',
|
desc: '码表、平台类型、扩展属性',
|
||||||
path: '/admin/management/dict-management',
|
path: '/admin/management/dict-management',
|
||||||
icon: <DatabaseOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
icon: <DatabaseOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||||
component: DictManagement,
|
loader: () => import('../pages/admin/DictManagement'),
|
||||||
group: '基础治理',
|
group: '基础治理',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -53,7 +46,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
|
||||||
desc: 'ASR 热词与同步',
|
desc: 'ASR 热词与同步',
|
||||||
path: '/admin/management/hot-word-management',
|
path: '/admin/management/hot-word-management',
|
||||||
icon: <FontSizeOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
icon: <FontSizeOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||||
component: HotWordManagement,
|
loader: () => import('../pages/admin/HotWordManagement'),
|
||||||
group: '基础治理',
|
group: '基础治理',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -62,7 +55,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
|
||||||
desc: '系统参数统一配置',
|
desc: '系统参数统一配置',
|
||||||
path: '/admin/management/parameter-management',
|
path: '/admin/management/parameter-management',
|
||||||
icon: <SettingOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
icon: <SettingOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||||
component: ParameterManagement,
|
loader: () => import('../pages/admin/ParameterManagement'),
|
||||||
group: '平台配置',
|
group: '平台配置',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -71,7 +64,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
|
||||||
desc: '音频模型与 LLM 模型配置',
|
desc: '音频模型与 LLM 模型配置',
|
||||||
path: '/admin/management/model-management',
|
path: '/admin/management/model-management',
|
||||||
icon: <AppstoreOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
icon: <AppstoreOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||||
component: ModelManagement,
|
loader: () => import('../pages/admin/ModelManagement'),
|
||||||
group: '平台配置',
|
group: '平台配置',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -80,7 +73,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
|
||||||
desc: '版本、下载地址、发布状态',
|
desc: '版本、下载地址、发布状态',
|
||||||
path: '/admin/management/client-management',
|
path: '/admin/management/client-management',
|
||||||
icon: <DesktopOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
icon: <DesktopOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||||
component: ClientManagement,
|
loader: () => import('../pages/ClientManagement'),
|
||||||
group: '设备与应用',
|
group: '设备与应用',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -89,7 +82,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
|
||||||
desc: '外部系统入口与图标配置',
|
desc: '外部系统入口与图标配置',
|
||||||
path: '/admin/management/external-app-management',
|
path: '/admin/management/external-app-management',
|
||||||
icon: <AppstoreOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
icon: <AppstoreOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||||
component: ExternalAppManagement,
|
loader: () => import('../pages/admin/ExternalAppManagement'),
|
||||||
group: '设备与应用',
|
group: '设备与应用',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -98,7 +91,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
|
||||||
desc: '专用设备、激活和绑定状态',
|
desc: '专用设备、激活和绑定状态',
|
||||||
path: '/admin/management/terminal-management',
|
path: '/admin/management/terminal-management',
|
||||||
icon: <DeploymentUnitOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
icon: <DeploymentUnitOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||||
component: TerminalManagement,
|
loader: () => import('../pages/admin/TerminalManagement'),
|
||||||
group: '设备与应用',
|
group: '设备与应用',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { useMemo } from 'react';
|
import React, { Suspense, lazy, useMemo } from 'react';
|
||||||
import { Alert } from 'antd';
|
import { Alert } from 'antd';
|
||||||
import { Navigate, useParams } from 'react-router-dom';
|
import { Navigate, useParams } from 'react-router-dom';
|
||||||
import SystemManagementOverview from './admin/SystemManagementOverview';
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_SYSTEM_MANAGEMENT_MODULE,
|
DEFAULT_SYSTEM_MANAGEMENT_MODULE,
|
||||||
|
SYSTEM_MANAGEMENT_OVERVIEW_LOADER,
|
||||||
SYSTEM_MANAGEMENT_MODULE_MAP,
|
SYSTEM_MANAGEMENT_MODULE_MAP,
|
||||||
} from '../config/systemManagementModules';
|
} from '../config/systemManagementModules';
|
||||||
|
|
||||||
|
|
@ -13,15 +13,23 @@ const AdminManagement = () => {
|
||||||
|
|
||||||
const currentModule = useMemo(() => (
|
const currentModule = useMemo(() => (
|
||||||
currentModuleKey === DEFAULT_SYSTEM_MANAGEMENT_MODULE
|
currentModuleKey === DEFAULT_SYSTEM_MANAGEMENT_MODULE
|
||||||
? { component: SystemManagementOverview }
|
? { loader: SYSTEM_MANAGEMENT_OVERVIEW_LOADER }
|
||||||
: SYSTEM_MANAGEMENT_MODULE_MAP[currentModuleKey]
|
: SYSTEM_MANAGEMENT_MODULE_MAP[currentModuleKey]
|
||||||
), [currentModuleKey]);
|
), [currentModuleKey]);
|
||||||
|
|
||||||
|
const CurrentComponent = useMemo(() => {
|
||||||
|
if (!currentModule?.loader) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lazy(currentModule.loader);
|
||||||
|
}, [currentModule]);
|
||||||
|
|
||||||
if (!moduleKey) {
|
if (!moduleKey) {
|
||||||
return <Navigate to={`/admin/management/${DEFAULT_SYSTEM_MANAGEMENT_MODULE}`} replace />;
|
return <Navigate to={`/admin/management/${DEFAULT_SYSTEM_MANAGEMENT_MODULE}`} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentModule) {
|
if (!currentModule || !CurrentComponent) {
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
type="warning"
|
type="warning"
|
||||||
|
|
@ -32,8 +40,11 @@ const AdminManagement = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CurrentComponent = currentModule.component;
|
return (
|
||||||
return <CurrentComponent />;
|
<Suspense fallback={<Alert type="info" showIcon message="管理模块加载中..." />}>
|
||||||
|
<CurrentComponent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AdminManagement;
|
export default AdminManagement;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,78 @@
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
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/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
host: true, // Optional: Allows the server to be accessible externally
|
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
|
allowedHosts: ['dev.imeeting.unisspace.com','nonperforated-ivonne-fezzy.ngrok-free.dev'], // Add the problematic hostname here
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue