feat: 添加 Admin Dashboard — React 19 SPA,包含 7 个页面
- Dashboard: 健康状态轮询、状态卡片、内存统计、快捷操作 - Login: 二维码展示 + 3 秒自动轮询 + 倒计时 + 登出 - Browser: 探索/搜索/用户三标签页,Feed 网格、详情面板、评论树 - Publish: 图文/视频发布表单,支持标签、可见性、定时发布 - Interactions: 点赞/取消点赞、收藏、评论、回复 + 操作日志 - API Tester: 端点选择器、请求体编辑器、cURL 生成、响应查看、历史记录 - Settings: Token 配置、服务器 URL 设置 后端改动: - app.ts: 生产环境提供 dist/web/ 静态文件服务 + SPA fallback - Dockerfile: 添加 web 构建阶段 - package.json: 添加 build:web、build:all、dev:web 脚本 技术栈: React 19 + TypeScript + Vite 6 + Tailwind CSS(暗色主题) 产物: 85.5 KB gzip JS + 4 KB gzip CSS,零重型依赖
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import http from 'node:http';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import fs from 'node:fs';
|
||||
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||
@@ -147,6 +150,9 @@ export class AppServer {
|
||||
}
|
||||
}
|
||||
|
||||
// Serve the web dashboard (static SPA) in production.
|
||||
this.setupWebDashboard();
|
||||
|
||||
// Re-register the error handler so it sits after any plugin routes.
|
||||
this.app.use(errorHandler);
|
||||
|
||||
@@ -290,6 +296,46 @@ export class AppServer {
|
||||
});
|
||||
}
|
||||
|
||||
// -- Private: Web Dashboard (SPA static files) ----------------------------
|
||||
|
||||
private setupWebDashboard(): void {
|
||||
// Resolve the web dashboard dist directory relative to this file.
|
||||
// In the built output: dist/server/app.js → dist/web/ is at ../web
|
||||
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const webDir = path.resolve(thisDir, '..', 'web');
|
||||
|
||||
if (!fs.existsSync(webDir)) {
|
||||
logger.debug({ webDir }, 'Web dashboard dist not found, skipping static mount');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info({ webDir }, 'Mounting web dashboard');
|
||||
|
||||
// Serve static assets
|
||||
this.app.use(express.static(webDir, { index: false }));
|
||||
|
||||
// SPA fallback: any GET that doesn't match /api, /sse, /messages, /health
|
||||
// returns index.html so client-side routing works.
|
||||
this.app.get('*', (req, res, next) => {
|
||||
// Skip API / MCP / health routes
|
||||
if (
|
||||
req.path.startsWith('/api') ||
|
||||
req.path.startsWith('/sse') ||
|
||||
req.path.startsWith('/messages') ||
|
||||
req.path === '/health'
|
||||
) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const indexPath = path.join(webDir, 'index.html');
|
||||
if (fs.existsSync(indexPath)) {
|
||||
res.sendFile(indexPath);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async buildHealthResponse(): Promise<Record<string, unknown>> {
|
||||
// Memory usage
|
||||
const mem = process.memoryUsage();
|
||||
|
||||
Reference in New Issue
Block a user