feat: 全新的Vue3管理后台(admin-spa)和路由重构

🎨 新增功能:
- 使用Vue3 + Vite构建的全新管理后台界面
- 支持Tab切换的API统计页面(统计查询/使用教程)
- 优雅的胶囊式Tab切换设计
- 同步了PR #106的会话窗口管理功能
- 完整的响应式设计和骨架屏加载状态

🔧 路由调整:
- 新版管理后台部署在 /admin-next/ 路径
- 将根路径 / 重定向到 /admin-next/api-stats
- 将 /web 页面路由重定向到新版,保留 /web/auth/* 认证路由
- 将 /apiStats 页面路由重定向到新版,保留API端点

🗑️ 清理工作:
- 删除旧版 web/admin/ 静态文件
- 删除旧版 web/apiStats/ 静态文件
- 清理相关的文件服务代码

🐛 修复问题:
- 修复重定向循环问题
- 修复环境变量配置
- 修复路由404错误
- 优化构建配置

🚀 生成方式:使用 Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-07-29 12:40:51 +08:00
parent c98de2aca5
commit 414856f152
70 changed files with 18748 additions and 10314 deletions

View File

@@ -1,6 +1,4 @@
const express = require('express');
const path = require('path');
const fs = require('fs');
const redis = require('../models/redis');
const logger = require('../utils/logger');
const apiKeyService = require('../services/apiKeyService');
@@ -8,45 +6,9 @@ const CostCalculator = require('../utils/costCalculator');
const router = express.Router();
// 🛡️ 安全文件服务函数
function serveStaticFile(req, res, filename, contentType) {
const filePath = path.join(__dirname, '../../web/apiStats', filename);
try {
// 检查文件是否存在
if (!fs.existsSync(filePath)) {
logger.error(`❌ API Stats file not found: ${filePath}`);
return res.status(404).json({ error: 'File not found' });
}
// 读取并返回文件内容
const content = fs.readFileSync(filePath, 'utf8');
res.setHeader('Content-Type', contentType);
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.send(content);
logger.info(`📄 Served API Stats file: ${filename}`);
} catch (error) {
logger.error(`❌ Error serving API Stats file ${filename}:`, error);
res.status(500).json({ error: 'Internal server error' });
}
}
// 🏠 API Stats 主页面
// 🏠 重定向页面请求到新版 admin-spa
router.get('/', (req, res) => {
serveStaticFile(req, res, 'index.html', 'text/html; charset=utf-8');
});
// 📱 JavaScript 文件
router.get('/app.js', (req, res) => {
serveStaticFile(req, res, 'app.js', 'application/javascript; charset=utf-8');
});
// 🎨 CSS 文件
router.get('/style.css', (req, res) => {
serveStaticFile(req, res, 'style.css', 'text/css; charset=utf-8');
res.redirect(301, '/admin-next/api-stats');
});
// 🔑 获取 API Key 对应的 ID

View File

@@ -12,52 +12,10 @@ const router = express.Router();
// 🏠 服务静态文件
router.use('/assets', express.static(path.join(__dirname, '../../web/assets')));
// 🔒 Web管理界面文件白名单 - 仅允许这些特定文件
const ALLOWED_FILES = {
'index.html': {
path: path.join(__dirname, '../../web/admin/index.html'),
contentType: 'text/html; charset=utf-8'
},
'app.js': {
path: path.join(__dirname, '../../web/admin/app.js'),
contentType: 'application/javascript; charset=utf-8'
},
'style.css': {
path: path.join(__dirname, '../../web/admin/style.css'),
contentType: 'text/css; charset=utf-8'
},
};
// 🛡️ 安全文件服务函数
function serveWhitelistedFile(req, res, filename) {
const fileConfig = ALLOWED_FILES[filename];
if (!fileConfig) {
logger.security(`🚨 Attempted access to non-whitelisted file: ${filename}`);
return res.status(404).json({ error: 'File not found' });
}
try {
// 检查文件是否存在
if (!fs.existsSync(fileConfig.path)) {
logger.error(`❌ Whitelisted file not found: ${fileConfig.path}`);
return res.status(404).json({ error: 'File not found' });
}
// 读取并返回文件内容
const content = fs.readFileSync(fileConfig.path, 'utf8');
res.setHeader('Content-Type', fileConfig.contentType);
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.send(content);
logger.info(`📄 Served whitelisted file: ${filename}`);
} catch (error) {
logger.error(`❌ Error serving file ${filename}:`, error);
res.status(500).json({ error: 'Internal server error' });
}
}
// 🌐 页面路由重定向到新版 admin-spa
router.get('/', (req, res) => {
res.redirect(301, '/admin-next/api-stats');
});
// 🔐 管理员登录
router.post('/auth/login', async (req, res) => {
@@ -387,22 +345,4 @@ router.post('/auth/refresh', async (req, res) => {
}
});
// 🌐 Web管理界面路由 - 使用固定白名单
router.get('/', (req, res) => {
serveWhitelistedFile(req, res, 'index.html');
});
router.get('/app.js', (req, res) => {
serveWhitelistedFile(req, res, 'app.js');
});
router.get('/style.css', (req, res) => {
serveWhitelistedFile(req, res, 'style.css');
});
// 🔑 Gemini OAuth 回调页面
module.exports = router;